Introduce image cropper (#805)

Signed-off-by: Ilya Sumbatyants <ilya.sumb@gmail.com>
This commit is contained in:
Ilya Sumbatyants 2022-01-12 16:52:22 +07:00 committed by GitHub
parent 0567bdbb31
commit 2bccea9078
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 646 additions and 80 deletions

View File

@ -34,6 +34,8 @@ specifiers:
'@rush-temp/gmail': file:./projects/gmail.tgz
'@rush-temp/gmail-assets': file:./projects/gmail-assets.tgz
'@rush-temp/gmail-resources': file:./projects/gmail-resources.tgz
'@rush-temp/image-cropper': file:./projects/image-cropper.tgz
'@rush-temp/image-cropper-resources': file:./projects/image-cropper-resources.tgz
'@rush-temp/lead': file:./projects/lead.tgz
'@rush-temp/lead-assets': file:./projects/lead-assets.tgz
'@rush-temp/lead-resources': file:./projects/lead-resources.tgz
@ -127,6 +129,7 @@ specifiers:
commander: ^8.1.0
compression-webpack-plugin: ~9.0.0
cors: ^2.8.5
cropperjs: ~1.5.12
cross-env: ^7.0.3
css-loader: ^5.2.1
deep-equal: ^2.0.5
@ -163,6 +166,7 @@ specifiers:
sass: ^1.37.5
sass-loader: ^12.1.0
simplytyped: ^3.3.0
smartcrop: ~2.0.5
style-loader: ^3.2.1
svelte-check: ^2.2.10
svelte-preprocess: ^4.7.4
@ -211,6 +215,8 @@ dependencies:
'@rush-temp/gmail': file:projects/gmail.tgz
'@rush-temp/gmail-assets': file:projects/gmail-assets.tgz
'@rush-temp/gmail-resources': file:projects/gmail-resources.tgz_096c09b0b673a57c275d9767a12070b1
'@rush-temp/image-cropper': file:projects/image-cropper.tgz
'@rush-temp/image-cropper-resources': file:projects/image-cropper-resources.tgz_096c09b0b673a57c275d9767a12070b1
'@rush-temp/lead': file:projects/lead.tgz
'@rush-temp/lead-assets': file:projects/lead-assets.tgz
'@rush-temp/lead-resources': file:projects/lead-resources.tgz_096c09b0b673a57c275d9767a12070b1
@ -304,6 +310,7 @@ dependencies:
commander: 8.3.0
compression-webpack-plugin: 9.0.1_webpack@5.65.0
cors: 2.8.5
cropperjs: 1.5.12
cross-env: 7.0.3
css-loader: 5.2.7_webpack@5.65.0
deep-equal: 2.0.5
@ -340,6 +347,7 @@ dependencies:
sass: 1.45.0
sass-loader: 12.4.0_sass@1.45.0+webpack@5.65.0
simplytyped: 3.3.0_typescript@4.5.4
smartcrop: 2.0.5
style-loader: 3.3.1_webpack@5.65.0
svelte-check: 2.2.11_ac194b5590200ebf8338e0f86ec190f4
svelte-preprocess: 4.10.1_3ae2e5fc7d8fb60bbcea513ad0b15c0f
@ -3530,6 +3538,10 @@ packages:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: false
/cropperjs/1.5.12:
resolution: {integrity: sha512-re7UdjE5UnwdrovyhNzZ6gathI4Rs3KGCBSc8HCIjUo5hO42CtzyblmWLj6QWVw7huHyDMfpKxhiO2II77nhDw==}
dev: false
/cross-env/7.0.3:
resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==}
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
@ -8877,6 +8889,10 @@ packages:
is-fullwidth-code-point: 3.0.0
dev: false
/smartcrop/2.0.5:
resolution: {integrity: sha512-aXoHTM8XlC51g96kgZkYxZ2mx09/ibOrIVLiUNOFozV/MHmFSgEr1/5CKVBoFD5vd+re2wSy0xra21CyjRITzA==}
dev: false
/snapdragon-node/2.1.1:
resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==}
engines: {node: '>=0.10.0'}
@ -10926,7 +10942,7 @@ packages:
dev: false
file:projects/contact-resources.tgz_096c09b0b673a57c275d9767a12070b1:
resolution: {integrity: sha512-5c2Hkvtnj+3t/r1Z+RFQuoxDwYm6ndTPCxFns6PK3SqCs/KfWREism8tiswJXrB2FNSQkb2CE7yvsftKZFcbPQ==, tarball: file:projects/contact-resources.tgz}
resolution: {integrity: sha512-5m/Rr4eGcqsTpvxL8nYdwl7Vn74QrZXdcgIDGbOJaRWh8uXuQBsDIUgE4wCrL49sgOJQEq/dUIfJVA9ndBOd5A==, tarball: file:projects/contact-resources.tgz}
id: file:projects/contact-resources.tgz
name: '@rush-temp/contact-resources'
version: 0.0.0
@ -11363,6 +11379,63 @@ packages:
- supports-color
dev: false
file:projects/image-cropper-resources.tgz_096c09b0b673a57c275d9767a12070b1:
resolution: {integrity: sha512-ljTYKZOYl34hx76ETCFNAcExK+MP6WSS09tbRRixWxD7SYc7NmBeMntGyZTy3qNu4/jAwNaDAIH52kXQg9ktXQ==, tarball: file:projects/image-cropper-resources.tgz}
id: file:projects/image-cropper-resources.tgz
name: '@rush-temp/image-cropper-resources'
version: 0.0.0
dependencies:
'@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237
'@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4
cropperjs: 1.5.12
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_ce2fa0c4dfa1c256100cababd749a13a
eslint-plugin-import: 2.25.3_eslint@7.32.0
eslint-plugin-node: 11.1.0_eslint@7.32.0
eslint-plugin-promise: 5.2.0_eslint@7.32.0
eslint-plugin-svelte3: 3.2.1_eslint@7.32.0+svelte@3.44.3
prettier: 2.5.1
prettier-plugin-svelte: 2.5.1_prettier@2.5.1+svelte@3.44.3
sass: 1.45.0
smartcrop: 2.0.5
svelte: 3.44.3
svelte-check: 2.2.11_4374c622c67ed7479ff0e44c29d09bce
svelte-loader: 3.1.2_svelte@3.44.3
svelte-preprocess: 4.10.1_14d64cad431e31f100de7363af24a44f
typescript: 4.5.4
transitivePeerDependencies:
- '@babel/core'
- coffeescript
- less
- node-sass
- postcss
- postcss-load-config
- pug
- stylus
- sugarss
- supports-color
dev: false
file:projects/image-cropper.tgz:
resolution: {integrity: sha512-7Fj5//tMmR0uKUwcnFrk1KHmI+N+CLNmOiNPApLUtdK7q6clzFT8LMTDZapnbh93NdTCVXa+DxVLwzNGaeqsQA==, tarball: file:projects/image-cropper.tgz}
name: '@rush-temp/image-cropper'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.41.8
'@types/heft-jest': 1.0.2
'@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237
'@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_ce2fa0c4dfa1c256100cababd749a13a
eslint-plugin-import: 2.25.3_eslint@7.32.0
eslint-plugin-node: 11.1.0_eslint@7.32.0
eslint-plugin-promise: 5.2.0_eslint@7.32.0
prettier: 2.5.1
typescript: 4.5.4
transitivePeerDependencies:
- supports-color
dev: false
file:projects/lead-assets.tgz:
resolution: {integrity: sha512-cRYB8PutP6HmaJjoEMLIEyMQEhKAQaCu0w2NJMF5TUW9vokia/22TXsHo1+xEGI1rx2epywbGXet/fL40tdbDw==, tarball: file:projects/lead-assets.tgz}
name: '@rush-temp/lead-assets'
@ -12045,7 +12118,7 @@ packages:
dev: false
file:projects/presentation.tgz_096c09b0b673a57c275d9767a12070b1:
resolution: {integrity: sha512-tpa5gk8H/quPYXkpBhp5jS0bp/E+Jk7mTGhfE54q4RTr5LOYGTCg2HKmxZWqRV1N3g1Jk7nKAZHWMxMSGHCuKw==, tarball: file:projects/presentation.tgz}
resolution: {integrity: sha512-QhfoHLyegkl3JsleXaZkSWsVQgikJM9jdqkla2mRW3kHyQAYWPBITUnVVWu1qBADViJupNZdYmYEcm7oeMF5og==, tarball: file:projects/presentation.tgz}
id: file:projects/presentation.tgz
name: '@rush-temp/presentation'
version: 0.0.0
@ -12081,7 +12154,7 @@ packages:
dev: false
file:projects/prod.tgz_sass@1.45.0+typescript@4.5.4:
resolution: {integrity: sha512-XgKxpfDD6oNTIijCj64CrOXM6sYgZhDuDoy5wWIrr/4Y5QFn7TWpOnSjIgmIXMtLUCGk4L55CAZBK+okRGX25Q==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-7OeW4OKQlYw/FrF1rSmbwacpTXSVzNnIPCb/mIP/Q4Kc+uxroItvCRRYUqVH/V9u8W7nyTE7CjDM+KrADL/BAg==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz
name: '@rush-temp/prod'
version: 0.0.0

View File

@ -88,6 +88,8 @@
"@anticrm/lead-resources": "~0.6.0",
"@anticrm/gmail": "~0.6.0",
"@anticrm/gmail-assets": "~0.6.0",
"@anticrm/gmail-resources": "~0.6.0"
"@anticrm/gmail-resources": "~0.6.0",
"@anticrm/image-cropper": "~0.6.0",
"@anticrm/image-cropper-resources": "~0.6.0"
}
}

View File

@ -29,6 +29,7 @@ import { attachmentId } from '@anticrm/attachment'
import { leadId } from '@anticrm/lead'
import { clientId } from '@anticrm/client'
import { gmailId } from '@anticrm/gmail'
import { imageCropperId } from '@anticrm/image-cropper'
import '@anticrm/login-assets'
import '@anticrm/task-assets'
@ -71,4 +72,5 @@ export async function configurePlatform() {
addLocation(telegramId, () => import(/* webpackChunkName: "telegram" */ '@anticrm/telegram-resources'))
addLocation(attachmentId, () => import(/* webpackChunkName: "attachment" */ '@anticrm/attachment-resources'))
addLocation(gmailId, () => import(/* webpackChunkName: "gmail" */ '@anticrm/gmail-resources'))
addLocation(imageCropperId, () => import(/* webpackChunkName: "image-cropper" */ '@anticrm/image-cropper-resources'))
}

View File

@ -37,6 +37,7 @@
"@anticrm/view": "~0.6.0",
"svelte": "^3.37.0",
"@anticrm/contact": "~0.6.2",
"@anticrm/login": "~0.6.1"
"@anticrm/login": "~0.6.1",
"@anticrm/image-cropper": "~0.6.0"
}
}

View File

@ -16,12 +16,22 @@
<script lang="ts">
import Avatar from './icons/Avatar.svelte'
import { getFileUrl } from '../utils'
import { getBlobURL, getFileUrl } from '../utils'
export let avatar: string | undefined = undefined
export let direct: Blob | undefined = undefined
export let size: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'
$: url = avatar ? getFileUrl(avatar) : undefined
let url: string | undefined
$: if (direct !== undefined) {
getBlobURL(direct).then((blobURL) => {
url = blobURL
})
} else if (avatar !== undefined) {
url = getFileUrl(avatar)
} else {
url = undefined
}
</script>
<div class="ava-{size} flex-center avatar-container" class:no-img={!url}>

View File

@ -0,0 +1,88 @@
<!--
// Copyright © 2021, 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { getResource } from '@anticrm/platform'
import { Button } from '@anticrm/ui'
import imageCropper from '@anticrm/image-cropper'
export let file: File
const dispatch = createEventDispatcher()
const CropperP = getResource(imageCropper.component.Cropper)
let cropper: any
async function onCrop () {
const res = await cropper.crop()
dispatch('close', res)
}
</script>
<div class="overlay" on:click={() => { dispatch('close') }} />
<div class="root">
{#await CropperP then Cropper}
<div class="cropper">
<Cropper bind:this={cropper} image={file} />
</div>
<div class="footer ml-6 mr-6 mt-4 mb-4">
<Button label={"Save"} primary on:click={onCrop} />
</div>
{/await}
</div>
<style lang="scss">
.overlay {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: var(--theme-menu-color);
opacity: .7;
}
.root {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: auto;
width: 75vw;
height: 75vh;
transform: translate(-50%, -50%);
background: var(--theme-bg-color);
border-radius: 1.25rem;
box-shadow: 0px 44px 154px rgba(0, 0, 0, .75);
display: grid;
grid-template-rows: minmax(min-content, 1fr) auto;
}
.cropper {
width: inherit;
}
.footer {
display: flex;
flex-direction: row-reverse;
}
</style>

View File

@ -0,0 +1,58 @@
<!--
// Copyright © 2021, 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { showPopup } from '@anticrm/ui'
import Avatar from './Avatar.svelte'
import EditAvatarPopup from './EditAvatarPopup.svelte'
export let avatar: string | undefined = undefined
export let size: 'x-small' | 'small' | 'medium' | 'large' | 'x-large'
const dispatch = createEventDispatcher()
let inputRef: HTMLInputElement
const targetMimes = ['image/png', 'image/jpg', 'image/jpeg']
function onClick () {
inputRef.click()
}
let direct: Blob | undefined
function onSelect (e: any) {
const file = e.target?.files[0] as File | undefined
if (file === undefined || !targetMimes.includes(file.type)) {
return
}
showPopup(EditAvatarPopup, { file }, undefined, (blob) => {
if (blob === undefined) {
return
}
direct = blob
dispatch('done', { file: new File([blob], file.name) })
})
e.target.value = null;
}
</script>
<div class="cursor-pointer" on:click={onClick}>
<Avatar {avatar} {direct} {size} />
<input style="display: none;" type="file" bind:this={inputRef} on:change={onSelect} accept={targetMimes.join(',')}/>
</div>

View File

@ -23,6 +23,7 @@ export * from './attributes'
export { default as UserBox } from './components/UserBox.svelte'
export { default as UserInfo } from './components/UserInfo.svelte'
export { default as Avatar } from './components/Avatar.svelte'
export { default as EditableAvatar } from './components/EditableAvatar.svelte'
export { default as MessageViewer } from './components/MessageViewer.svelte'
export { default as AttributesBar } from './components/AttributesBar.svelte'
export { default as AttributeBarEditor } from './components/AttributeBarEditor.svelte'

View File

@ -114,6 +114,15 @@ export function getFileUrl (file: string): string {
return url
}
export async function getBlobURL (blob: Blob): Promise<string> {
return await new Promise((resolve) => {
const reader = new FileReader()
reader.addEventListener('load', () => resolve(reader.result as string), false)
reader.readAsDataURL(blob)
})
}
/**
* @public
*/

View File

@ -38,7 +38,7 @@
async function createAttachment (file: File) {
loading++
try {
const uuid = await uploadFile(space, file, objectId)
const uuid = await uploadFile(file, space, objectId)
console.log('uploaded file uuid', uuid)
client.addCollection(attachment.class.Attachment, space, objectId, _class, 'attachments', {
name: file.name,

View File

@ -17,6 +17,7 @@ import AttachmentsPresenter from './components/AttachmentsPresenter.svelte'
import AttachmentPresenter from './components/AttachmentPresenter.svelte'
import TxAttachmentCreate from './components/activity/TxAttachmentCreate.svelte'
import Attachments from './components/Attachments.svelte'
import { uploadFile } from './utils'
export { Attachments, AttachmentsPresenter }
@ -28,5 +29,8 @@ export default async () => ({
},
activity: {
TxAttachmentCreate
},
helper: {
UploadFile: uploadFile
}
})

View File

@ -18,14 +18,19 @@ import type { Doc, Ref, Space } from '@anticrm/core'
import login from '@anticrm/login'
import { getMetadata } from '@anticrm/platform'
export async function uploadFile (space: Ref<Space>, file: File, attachedTo: Ref<Doc>): Promise<string> {
export async function uploadFile (file: File, space?: Ref<Space>, attachedTo?: Ref<Doc>): Promise<string> {
console.log(file)
const uploadUrl = getMetadata(login.metadata.UploadUrl)
const data = new FormData()
data.append('file', file)
const url = `${uploadUrl as string}?space=${space}&name=${encodeURIComponent(file.name)}&attachedTo=${attachedTo}`
const params = [['space', space], ['attachedTo', attachedTo]]
.filter((x): x is [string, Ref<any>] => x[1] !== undefined)
.map(([name, value]) => `${name}=${value}`)
.join('&')
const url = `${uploadUrl as string}?name=${encodeURIComponent(file.name)}&${params}`
const resp = await fetch(url, {
method: 'POST',
headers: {

View File

@ -14,8 +14,8 @@
// limitations under the License.
//
import type { Ref, Class, AttachedDoc } from '@anticrm/core'
import { plugin } from '@anticrm/platform'
import type { Ref, Class, AttachedDoc, Space, Doc } from '@anticrm/core'
import { plugin, Resource } from '@anticrm/platform'
import type { Asset, Plugin } from '@anticrm/platform'
import { AnyComponent } from '@anticrm/ui'
@ -44,5 +44,8 @@ export default plugin(attachmentId, {
},
class: {
Attachment: '' as Ref<Class<Attachment>>
},
helper: {
UploadFile: '' as Resource<(file: File, space?: Ref<Space>, attachedTo?: Ref<Doc>) => Promise<string>>
}
})

View File

@ -41,6 +41,7 @@
"@anticrm/view": "~0.6.0",
"@anticrm/attachment-resources": "~0.6.0",
"@anticrm/panel": "~0.6.0",
"@anticrm/view-resources": "~0.6.0"
"@anticrm/view-resources": "~0.6.0",
"@anticrm/attachment": "~0.6.1"
}
}

View File

@ -16,9 +16,11 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import type { Data } from '@anticrm/core'
import { getResource } from '@anticrm/platform';
import { getClient, Card, Channels, Avatar } from '@anticrm/presentation'
import { getClient, Card, Channels, EditableAvatar } from '@anticrm/presentation'
import attachment from '@anticrm/attachment'
import { EditBox, showPopup, CircleButton, IconEdit, IconAdd, Label } from '@anticrm/ui'
import SocialEditor from './SocialEditor.svelte'
@ -37,10 +39,25 @@
const dispatch = createEventDispatcher()
const client = getClient()
let avatar: File | undefined
function onAvatarDone (e: any) {
const { file } = e.detail
avatar = file
}
async function createPerson () {
const uploadFile = await getResource(attachment.helper.UploadFile)
const avatarUUID = avatar !== undefined
? await uploadFile(avatar)
: undefined
console.warn('avatar', avatar, avatarUUID)
const person: Data<Person> = {
name: combineName(firstName, lastName),
city: object.city,
avatar: avatarUUID,
channels: object.channels
}
@ -61,7 +78,7 @@
>
<div class="flex-row-center">
<div class="mr-4">
<Avatar avatar={object.avatar} size={'large'} />
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
</div>
<div class="flex-col">
<div class="fs-title"><EditBox placeholder="John" maxWidth="12rem" bind:value={firstName} label={undefined} /></div>

View File

@ -16,8 +16,10 @@
<script lang="ts">
import { createEventDispatcher, onMount, afterUpdate } from 'svelte'
import { getCurrentAccount, Ref, Space } from '@anticrm/core'
import { getClient, createQuery, Channels, Avatar, AttributeEditor } from '@anticrm/presentation'
import { CircleButton, EditBox, showPopup, IconAdd, Label, IconActivity } from '@anticrm/ui'
import { getClient, createQuery, Channels, EditableAvatar, AttributeEditor } from '@anticrm/presentation'
import { getResource } from '@anticrm/platform'
import attachment from '@anticrm/attachment'
import setting from '@anticrm/setting'
import { IntegrationType } from '@anticrm/setting'
import contact from '../plugin'
@ -61,12 +63,22 @@
const sendOpen = () => dispatch('open', { ignoreKeys: ['comments', 'name', 'channels', 'city'] })
onMount(sendOpen)
afterUpdate(sendOpen)
async function onAvatarDone (e: any) {
const uploadFile = await getResource(attachment.helper.UploadFile)
const { file: avatar } = e.detail
const uuid = await uploadFile(avatar)
await client.updateDoc(object._class, object.space, object._id, {
avatar: uuid
})
}
</script>
{#if object !== undefined}
<div class="flex-row-streach flex-grow">
<div class="mr-8">
<Avatar avatar={object.avatar} size={'x-large'} />
<EditableAvatar avatar={object.avatar} size={'x-large'} on:done={onAvatarDone} />
</div>
<div class="flex-grow flex-col">
<div class="flex-grow flex-col">

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@anticrm/platform-rig/profiles/ui/config/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname },
settings: {
'svelte3/ignore-styles': () => true
}
}

View File

@ -0,0 +1,38 @@
{
"name": "@anticrm/image-cropper-resources",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "echo 'no build for ui'",
"build:docs": "api-extractor run --local",
"lint": "svelte-check && eslint",
"lint:fix": "eslint --fix src",
"format": "prettier --write --plugin-search-dir=. src && eslint --fix src"
},
"devDependencies": {
"@anticrm/platform-rig": "~0.6.0",
"svelte-loader": "^3.1.2",
"sass": "^1.37.5",
"svelte-preprocess": "^4.7.4",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint": "^7.32.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint-config-standard-with-typescript": "^21.0.1",
"eslint-plugin-svelte3": "~3.2.1",
"prettier-plugin-svelte": "^2.2.0",
"prettier": "^2.4.1",
"svelte-check": "^2.2.10",
"typescript": "^4.3.5"
},
"dependencies": {
"svelte": "^3.37.0",
"@anticrm/platform": "~0.6.5",
"cropperjs": "~1.5.12",
"smartcrop": "~2.0.5"
}
}

View File

@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}

View File

@ -0,0 +1,102 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import Cropper from 'cropperjs'
import smartcrop from 'smartcrop'
export let image: File
export let cropSize = 1200
let imgRef: HTMLImageElement
let cropper: Cropper | undefined
async function init () {
const bitmap = await createImageBitmap(image)
const canvas = document.createElement('canvas')
canvas.height = bitmap.height
canvas.width = bitmap.width
const ctx = canvas.getContext('2d')
if (ctx == null) {
return
}
ctx.drawImage(bitmap, 0, 0)
imgRef.src = canvas.toDataURL('image/jpeg', 1.0)
imgRef.width = bitmap.width
imgRef.height = bitmap.height
const initialArea = (await smartcrop.crop(canvas, { width: 100, height: 100 })).topCrop
const cropperInst = new Cropper(imgRef, {
aspectRatio: 1,
viewMode: 1,
autoCrop: true,
rotatable: false,
ready: () => {
const imgData = cropperInst.getImageData()
const xC = imgData.width / bitmap.width
const yC = imgData.height / bitmap.height
cropperInst.setCropBoxData({
left: initialArea.x * xC,
top: initialArea.y * yC,
height: initialArea.height * yC,
width: initialArea.width * yC
})
cropper = cropperInst
}
})
}
export async function crop () {
if (cropper === undefined) {
return
}
const res = cropper.getCroppedCanvas({ maxWidth: cropSize, maxHeight: cropSize, imageSmoothingQuality: 'high' })
return new Promise(
(resolve) => res.toBlob(
(blob) => {
resolve(blob)
},
'image/jpeg',
0.95))
}
</script>
<div class="w-full h-full flex">
<img class="image" bind:this={imgRef} alt="img"/>
{#await init()}
Waiting...
{/await}
</div>
<style lang="scss">
@import 'cropperjs/dist/cropper.min.css';
:global(.cropper-view-box, .cropper-face) {
border-radius: 50%;
}
.image {
max-width: 100%;
object-fit: contain;
display: none;
}
</style>

View File

@ -0,0 +1,25 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Resources } from '@anticrm/platform'
import Cropper from './components/Cropper.svelte'
export default async (): Promise<Resources> => ({
component: {
Cropper
}
})

View File

@ -0,0 +1,5 @@
const sveltePreprocess = require('svelte-preprocess')
module.exports = {
preprocess: sveltePreprocess()
};

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "esnext",
"module": "esnext",
"declaration": true,
"outDir": "./lib",
"strict": true,
"esModuleInterop": true,
"lib": [
"esnext",
"dom"
]
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@anticrm/platform-rig/profiles/default/config/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,18 @@
// The "rig.json" file directs tools to look for their config files in an external package.
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
/**
* (Required) The name of the rig package to inherit from.
* It should be an NPM package name with the "-rig" suffix.
*/
"rigPackageName": "@anticrm/platform-rig"
/**
* (Optional) Selects a config profile from the rig package. The name must consist of
* lowercase alphanumeric words separated by hyphens, for example "sample-profile".
* If omitted, then the "default" profile will be used."
*/
// "rigProfile": "your-profile-name"
}

View File

@ -0,0 +1,32 @@
{
"name": "@anticrm/image-cropper",
"version": "0.6.0",
"main": "lib/index.js",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "heft build",
"build:watch": "tsc",
"lint:fix": "eslint --fix src",
"lint": "eslint src",
"format": "prettier --write src && eslint --fix src"
},
"devDependencies": {
"@anticrm/platform-rig": "~0.6.0",
"@types/heft-jest": "^1.0.2",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint": "^7.32.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint-config-standard-with-typescript": "^21.0.1",
"prettier": "^2.4.1",
"@rushstack/heft": "^0.41.1",
"typescript": "^4.3.5"
},
"dependencies": {
"@anticrm/platform": "~0.6.5",
"@anticrm/ui": "~0.6.0"
}
}

View File

@ -0,0 +1,29 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { plugin } from '@anticrm/platform'
import type { Plugin } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
/**
* @public
*/
export const imageCropperId = 'image-cropper' as Plugin
export default plugin(imageCropperId, {
component: {
Cropper: '' as AnyComponent
}
})

View File

@ -0,0 +1,9 @@
{
"extends": "./node_modules/@anticrm/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"lib": ["esnext", "dom"]
}
}

View File

@ -18,13 +18,12 @@
import contact, { combineName, Person } from '@anticrm/contact'
import type { Data, MixinData, Ref } from '@anticrm/core'
import { generateId } from '@anticrm/core'
import { setPlatformStatus, unknownError } from '@anticrm/platform'
import { Avatar, Card, Channels, getClient, PDFViewer } from '@anticrm/presentation'
import { getResource, setPlatformStatus, unknownError } from '@anticrm/platform'
import { EditableAvatar, Card, Channels, getClient, PDFViewer } from '@anticrm/presentation'
import type { Candidate } from '@anticrm/recruit'
import { CircleButton, EditBox, IconAdd, IconFile as FileIcon, Label, Link, showPopup, Spinner } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
import { uploadFile } from '../utils'
import FileUpload from './icons/FileUpload.svelte'
import YesNo from './YesNo.svelte'
@ -50,10 +49,15 @@
const candidateId = generateId()
async function createCandidate () {
const uploadFile = await getResource(attachment.helper.UploadFile)
const avatarUUID = avatar !== undefined
? await uploadFile(avatar)
: undefined
const candidate: Data<Person> = {
name: combineName(firstName, lastName),
city: object.city,
channels: object.channels
channels: object.channels,
avatar: avatarUUID
}
const candidateData: MixinData<Person, Candidate> = {
title: object.title,
@ -86,7 +90,9 @@
async function createAttachment (file: File) {
loading = true
try {
resume.uuid = await uploadFile(space, file, candidateId)
const uploadFile = await getResource(attachment.helper.UploadFile)
resume.uuid = await uploadFile(file, space, candidateId)
resume.name = file.name
resume.size = file.size
resume.type = file.type
@ -111,6 +117,15 @@
const file = inputFile.files?.[0]
if (file !== undefined) { createAttachment(file) }
}
let avatar: File | undefined
function onAvatarDone (e: any) {
const { file } = e.detail
avatar = file
}
</script>
<!-- <DialogHeader {space} {object} {newValue} {resume} create={true} on:save={createCandidate}/> -->
@ -124,7 +139,7 @@
<!-- <StatusComponent slot="error" status={{ severity: Severity.ERROR, code: 'Cant save the object because it already exists' }} /> -->
<div class="flex-row-center">
<div class="mr-4">
<Avatar avatar={object.avatar} size={'large'} />
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone}/>
</div>
<div class="flex-col">
<div class="fs-title"><EditBox placeholder="John" maxWidth="10rem" bind:value={firstName}/></div>

View File

@ -1,43 +0,0 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { Ref, Doc, Space } from '@anticrm/core'
import { getMetadata, PlatformError } from '@anticrm/platform'
import login from '@anticrm/login'
export async function uploadFile(space: Ref<Space>, file: File, attachedTo: Ref<Doc>): Promise<string> {
console.log(file)
const uploadUrl = getMetadata(login.metadata.UploadUrl)
const data = new FormData()
data.append('file', file)
const url = `${uploadUrl}?space=${space}&name=${encodeURIComponent(file.name)}&attachedTo=${attachedTo}`
const resp = await fetch(url, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + getMetadata(login.metadata.LoginToken)
},
body: data
})
if (resp.status !== 200) {
throw new Error('Can\'t upload file.')
}
const uuid = await resp.text()
console.log(uuid)
return uuid
}

View File

@ -691,6 +691,16 @@
"projectFolder": "models/demo",
"shouldPublish": true
},
{
"packageName": "@anticrm/image-cropper",
"projectFolder": "plugins/image-cropper",
"shouldPublish": true
},
{
"packageName": "@anticrm/image-cropper-resources",
"projectFolder": "plugins/image-cropper-resources",
"shouldPublish": true
},
{
"packageName": "@anticrm/dev-server-chunter-resources",
"projectFolder": "dev/server-chunter-resources",

View File

@ -159,9 +159,9 @@ export function start (config: { transactorEndpoint: string, elasticUrl: string,
const uuid = await minioUpload(config.minio, payload.workspace, file)
console.log('uploaded uuid', uuid)
const name = req.query.name as string
const space = req.query.space as Ref<Space>
const attachedTo = req.query.attachedTo as Ref<Doc>
const name = req.query.name as string | undefined
const space = req.query.space as Ref<Space> | undefined
const attachedTo = req.query.attachedTo as Ref<Doc> | undefined
// const name = req.query.name as string
// await createAttachment(
@ -175,6 +175,7 @@ export function start (config: { transactorEndpoint: string, elasticUrl: string,
// fileId
// )
if (name !== undefined && space !== undefined && attachedTo !== undefined) {
const elastic = await createElasticAdapter(config.elasticUrl, payload.workspace)
const indexedDoc: IndexedDoc = {
@ -188,6 +189,7 @@ export function start (config: { transactorEndpoint: string, elasticUrl: string,
}
await elastic.index(indexedDoc)
}
res.status(200).send(uuid)
} catch (error) {