Use Gravatar icon on workspace join (#2324)

Signed-off-by: Denis Bunakalya <denis.bunakalya@xored.com>
Co-authored-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Denis Bunakalya 2022-10-31 08:34:42 +03:00 committed by GitHub
parent 71f4e38dd6
commit 5426a06346
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 622 additions and 223 deletions

View File

@ -209,6 +209,7 @@ specifiers:
'@types/body-parser': ~1.19.2
'@types/compression': ~1.7.2
'@types/cors': ^2.8.12
'@types/crypto-js': ^4.1.1
'@types/deep-equal': ^1.0.1
'@types/express': ^4.17.13
'@types/express-fileupload': ^1.1.7
@ -517,6 +518,7 @@ dependencies:
'@types/body-parser': 1.19.2
'@types/compression': 1.7.2
'@types/cors': 2.8.12
'@types/crypto-js': 4.1.1
'@types/deep-equal': 1.0.1
'@types/express': 4.17.13
'@types/express-fileupload': 1.2.2
@ -1989,6 +1991,10 @@ packages:
resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==}
dev: false
/@types/crypto-js/4.1.1:
resolution: {integrity: sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==}
dev: false
/@types/deep-equal/1.0.1:
resolution: {integrity: sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==}
dev: false
@ -2162,6 +2168,10 @@ packages:
'@types/koa': 2.13.4
dev: false
/@types/md5/2.3.2:
resolution: {integrity: sha512-v+JFDu96+UYJ3/UWzB0mEglIS//MZXgRaJ4ubUPwOM0gvLc/kcQ3TWNYwENEK7/EcXGQVrW8h/XqednSjBd/Og==}
dev: false
/@types/mime-types/2.1.1:
resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==}
dev: false
@ -3337,6 +3347,10 @@ packages:
engines: {node: '>=10'}
dev: false
/charenc/0.0.2:
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
dev: false
/chokidar/3.4.3:
resolution: {integrity: sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==}
engines: {node: '>= 8.10.0'}
@ -3678,6 +3692,10 @@ packages:
which: 2.0.2
dev: false
/crypt/0.0.2:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
dev: false
/crypto-browserify/3.12.0:
resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==}
dependencies:
@ -5522,6 +5540,10 @@ packages:
has-tostringtag: 1.0.0
dev: false
/is-buffer/1.1.6:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
dev: false
/is-callable/1.2.4:
resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==}
engines: {node: '>= 0.4'}
@ -6662,6 +6684,14 @@ packages:
safe-buffer: 5.2.1
dev: false
/md5/2.3.0:
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
dependencies:
charenc: 0.0.2
crypt: 0.0.2
is-buffer: 1.1.6
dev: false
/mdn-data/2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
dev: false
@ -9597,11 +9627,13 @@ packages:
version: 0.0.0
dependencies:
'@rushstack/heft': 0.47.9
'@types/crypto-js': 4.1.1
'@types/heft-jest': 1.0.3
'@types/minio': 7.0.13
'@types/ws': 8.5.3
'@typescript-eslint/eslint-plugin': 5.30.3_bd298502bfa44e376686f9e6b29811dd
'@typescript-eslint/parser': 5.30.3_eslint@8.19.0+typescript@4.7.4
crypto-js: 4.1.1
eslint: 8.19.0
eslint-config-standard-with-typescript: 21.0.1_7eafe142d23700af342fa58294c300cb
eslint-plugin-import: 2.26.0_eslint@8.19.0
@ -11011,7 +11043,7 @@ packages:
dev: false
file:projects/login-resources.tgz_1e3963ebf0ceeb25b2fa6a1cc87e253c:
resolution: {integrity: sha512-ourb/1VHLilp8HrWPNLlrkssVuI+AXJ8dGSluNj9gt/bsICIIG34OXk4n3HDC4EczDR/NaiwYcTV/sRMf14QeA==, tarball: file:projects/login-resources.tgz}
resolution: {integrity: sha512-C1yla+iOSpnssVUAZxKPWOnq2XofESZFR12ygul/97CiLyqebzXorPy+N5Ecwx6lIedjZS7c9qI0IW0ys2qbHA==, tarball: file:projects/login-resources.tgz}
id: file:projects/login-resources.tgz
name: '@rush-temp/login-resources'
version: 0.0.0
@ -11237,20 +11269,26 @@ packages:
dev: false
file:projects/model-contact.tgz_typescript@4.7.4:
resolution: {integrity: sha512-HR/Cu18s96RSrDeLTeIXBbhN2Z4+W23yiwV+2GQ0Bm/2sL3DWe0oB8P7b3ktz/Tk7bm3eEs/DTRmfzeZu5CUGg==, tarball: file:projects/model-contact.tgz}
resolution: {integrity: sha512-+q/ylD1M9fW5REV0OjmhQK95d+rsdWQqhn6UdBMJ2UGag4fmCFJk69kRIPqcx06yyJei+wbmXzUhyDMDqe3a9Q==, tarball: file:projects/model-contact.tgz}
id: file:projects/model-contact.tgz
name: '@rush-temp/model-contact'
version: 0.0.0
dependencies:
'@rushstack/heft': 0.47.9
'@types/crypto-js': 4.1.1
'@types/heft-jest': 1.0.3
'@types/md5': 2.3.2
'@types/node': 16.11.42
'@typescript-eslint/eslint-plugin': 5.30.3_bd298502bfa44e376686f9e6b29811dd
'@typescript-eslint/parser': 5.30.3_eslint@8.19.0+typescript@4.7.4
crypto-js: 4.1.1
eslint: 8.19.0
eslint-config-standard-with-typescript: 21.0.1_7eafe142d23700af342fa58294c300cb
eslint-plugin-import: 2.26.0_eslint@8.19.0
eslint-plugin-node: 11.1.0_eslint@8.19.0
eslint-plugin-promise: 6.0.0_eslint@8.19.0
md5: 2.3.0
md5.js: 1.3.5
prettier: 2.7.1
transitivePeerDependencies:
- supports-color
@ -12361,13 +12399,17 @@ packages:
dev: false
file:projects/presentation.tgz_1e3963ebf0ceeb25b2fa6a1cc87e253c:
resolution: {integrity: sha512-TH3Rv1vPOr3zJMGK4bhRfMUDk1KHsmRxEdv41pK0BsrzHlq24gYo+j6CSa92RrAbHtTVfnBzxXorpKib/d5i8A==, tarball: file:projects/presentation.tgz}
resolution: {integrity: sha512-tiDMzgSrAVMWR/oGpiY7OmFM8xFfofHJDA2Y/aSpfPN/An9ja1Kkt6T4dSPMLCk8vXdMUYYwJ1rMhHT+I+2nGA==, tarball: file:projects/presentation.tgz}
id: file:projects/presentation.tgz
name: '@rush-temp/presentation'
version: 0.0.0
dependencies:
'@types/crypto-js': 4.1.1
'@types/md5': 2.3.2
'@types/node': 16.11.42
'@typescript-eslint/eslint-plugin': 5.30.3_bd298502bfa44e376686f9e6b29811dd
'@typescript-eslint/parser': 5.30.3_eslint@8.19.0+typescript@4.7.4
crypto-js: 4.1.1
eslint: 8.19.0
eslint-config-standard-with-typescript: 21.0.1_7eafe142d23700af342fa58294c300cb
eslint-plugin-import: 2.26.0_eslint@8.19.0
@ -12375,6 +12417,7 @@ packages:
eslint-plugin-promise: 6.0.0_eslint@8.19.0
eslint-plugin-svelte3: 4.0.0_eslint@8.19.0+svelte@3.48.0
fast-equals: 2.0.4
md5: 2.3.0
prettier: 2.7.1
prettier-plugin-svelte: 2.7.0_prettier@2.7.1+svelte@3.48.0
sass: 1.53.0

View File

@ -18,6 +18,7 @@
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint": "^8.18.0",
"@types/crypto-js": "^4.1.1",
"@types/heft-jest": "^1.0.3",
"@typescript-eslint/parser": "^5.30.0",
"eslint-config-standard-with-typescript": "^21.0.1",
@ -38,6 +39,7 @@
"@hcengineering/platform": "^0.6.7",
"@hcengineering/contact": "~0.6.5",
"@hcengineering/contact-resources": "~0.6.0",
"@hcengineering/view": "^0.6.1"
"@hcengineering/view": "^0.6.1",
"crypto-js": "^4.1.1"
}
}

View File

@ -14,6 +14,8 @@
//
import {
AvatarProvider,
AvatarType,
Channel,
ChannelProvider,
Contact,
@ -49,11 +51,18 @@ import view, { actionTemplates, createAction, ViewAction } from '@hcengineering/
import workbench from '@hcengineering/model-workbench'
import type { Asset, IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { IconSize } from '@hcengineering/ui'
import contact from './plugin'
export const DOMAIN_CONTACT = 'contact' as Domain
export const DOMAIN_CHANNEL = 'channel' as Domain
@Model(contact.class.AvatarProvider, core.class.Doc, DOMAIN_MODEL)
export class TAvatarProvider extends TDoc implements AvatarProvider {
type!: AvatarType
getUrl!: (uri: string, size: IconSize) => string
}
@Model(contact.class.ChannelProvider, core.class.Doc, DOMAIN_MODEL)
export class TChannelProvider extends TDoc implements ChannelProvider {
label!: IntlString
@ -159,6 +168,7 @@ export class TPersons extends TSpace implements Persons {}
export function createModel (builder: Builder): void {
builder.createModel(
TAvatarProvider,
TChannelProvider,
TContact,
TPerson,

View File

@ -13,11 +13,12 @@
// limitations under the License.
//
import { Employee, EmployeeAccount } from '@hcengineering/contact'
import { Employee, EmployeeAccount, AvatarType } from '@hcengineering/contact'
import { AccountRole, DOMAIN_TX, TxCreateDoc, TxOperations } from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import contact from './index'
import MD5 from 'crypto-js/md5'
async function createSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
@ -84,6 +85,25 @@ async function setRole (client: MigrationClient): Promise<void> {
)
}
async function updateEmployeeAvatar (tx: TxOperations): Promise<void> {
const accounts = await tx.findAll(contact.class.EmployeeAccount, {})
const employees = await tx.findAll(contact.class.Employee, { _id: { $in: accounts.map((a) => a.employee) } })
const employeesById = new Map(employees.map((it) => [it._id, it]))
// set gravatar for users without avatar
const promises = accounts.map(async (account) => {
const employee = employeesById.get(account.employee)
if (employee === undefined) return
if (employee.avatar != null && employee.avatar !== undefined) return
const gravatarId = MD5(account.email.trim().toLowerCase()).toString()
await tx.update(employee, {
avatar: `${AvatarType.GRAVATAR}://${gravatarId}`
})
})
await Promise.all(promises)
}
export const contactOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await setActiveEmployeeTx(client)
@ -92,5 +112,6 @@ export const contactOperation: MigrateOperation = {
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createSpace(tx)
await updateEmployeeAvatar(tx)
}
}

View File

@ -4,5 +4,6 @@
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"esModuleInterop": true
}
}

View File

@ -25,6 +25,8 @@
"NoMatchesFound": "No matches found",
"NotInThis": "Not in this {space}",
"Add": "Add",
"Edit": "Edit"
"Edit": "Edit",
"SelectAvatar": "Select avatar",
"GravatarsManaged": "Gravatars are managed through"
}
}

View File

@ -25,6 +25,8 @@
"NoMatchesFound": "Не найдено соответсвий",
"NotInThis": "Не в этом {space}",
"Add": "Добавить",
"Edit": "Редактировать"
"Edit": "Редактировать",
"SelectAvatar": "Выбрать аватар",
"GravatarsManaged": "Граватары управляются через"
}
}

View File

@ -19,6 +19,7 @@
"@hcengineering/platform-rig": "~0.6.0",
"@typescript-eslint/eslint-plugin": "^5.30.0",
"@typescript-eslint/parser": "^5.30.0",
"@types/crypto-js": "^4.1.1",
"eslint-config-standard-with-typescript": "^21.0.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
@ -31,6 +32,7 @@
"typescript": "^4.3.5"
},
"dependencies": {
"@hcengineering/attachment": "~0.6.1",
"@hcengineering/platform": "^0.6.7",
"@hcengineering/core": "^0.6.17",
"@hcengineering/query": "~0.6.1",
@ -41,7 +43,8 @@
"@hcengineering/login": "~0.6.1",
"@hcengineering/image-cropper": "~0.6.0",
"@hcengineering/client": "^0.6.3",
"@hcengineering/setting": "~0.6.1",
"fast-equals": "^2.0.3",
"@hcengineering/setting": "~0.6.1"
"crypto-js": "^4.1.1"
}
}

View File

@ -13,10 +13,11 @@
// limitations under the License.
-->
<script lang="ts">
import { Asset } from '@hcengineering/platform'
import { AvatarType, AvatarProvider } from '@hcengineering/contact'
import { Asset, getResource } from '@hcengineering/platform'
import { AnySvelteComponent, Icon, IconSize } from '@hcengineering/ui'
import { getBlobURL, getFileUrl } from '../utils'
import Avatar from './icons/Avatar.svelte'
import { getBlobURL, getAvatarProviderId } from '../utils'
import AvatarIcon from './icons/Avatar.svelte'
export let avatar: string | null | undefined = undefined
export let direct: Blob | undefined = undefined
@ -24,25 +25,52 @@
export let icon: Asset | AnySvelteComponent | undefined = undefined
let url: string | undefined
$: if (direct !== undefined) {
getBlobURL(direct).then((blobURL) => {
url = blobURL
})
} else if (avatar !== undefined && avatar !== null) {
url = getFileUrl(avatar, size)
let avatarProvider: AvatarProvider | undefined
async function update (size: IconSize, avatar?: string | null, direct?: Blob) {
if (direct !== undefined) {
getBlobURL(direct).then((blobURL) => {
url = blobURL
avatarProvider = undefined
})
} else if (avatar) {
const avatarProviderId = getAvatarProviderId(avatar)
avatarProvider = avatarProviderId && (await getResource(avatarProviderId))
if (!avatarProvider || avatarProvider.type === AvatarType.COLOR) {
url = undefined
} else if (avatarProvider.type === AvatarType.IMAGE) {
url = avatarProvider.getUrl(avatar, size)
} else {
const uri = avatar.split('://')[1]
url = avatarProvider.getUrl(uri, size)
}
} else {
url = undefined
avatarProvider = undefined
}
}
$: update(size, avatar, direct)
let style = ''
$: if (!avatar || avatarProvider?.type !== AvatarType.COLOR) {
style = ''
} else {
url = undefined
const uri = avatar.split('://')[1]
const color = avatarProvider.getUrl(uri, size)
style = `background-color: ${color}`
}
</script>
<div class="ava-{size} flex-center avatar-container" class:no-img={!url}>
<div class="ava-{size} flex-center avatar-container" class:no-img={!url} {style}>
{#if url}
{#if size === 'large' || size === 'x-large'}
<img class="ava-{size} ava-blur" src={url} alt={''} />
{/if}
<img class="ava-{size} ava-mask" src={url} alt={''} />
{:else}
<Icon icon={icon ?? Avatar} {size} />
<Icon icon={icon ?? AvatarIcon} {size} />
{/if}
</div>

View File

@ -14,69 +14,63 @@
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import attachment from '@hcengineering/attachment'
import { AnySvelteComponent, IconSize, showPopup } from '@hcengineering/ui'
import { AvatarType } from '@hcengineering/contact'
import { Asset, getResource } from '@hcengineering/platform'
import Avatar from './Avatar.svelte'
import EditAvatarPopup from './EditAvatarPopup.svelte'
import { getFileUrl } from '../utils'
import { Asset } from '@hcengineering/platform'
import AvatarComponent from './Avatar.svelte'
import SelectAvatarPopup from './SelectAvatarPopup.svelte'
export let avatar: string | null | undefined = undefined
export let avatar: string | null | undefined
export let email: string | undefined = undefined
export let id: string
export let size: IconSize
export let direct: Blob | undefined = undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined
const dispatch = createEventDispatcher()
const [schema, uri] = avatar?.split('://') || []
let inputRef: HTMLInputElement
const targetMimes = ['image/png', 'image/jpg', 'image/jpeg']
async function onClick () {
let file: Blob
if (direct !== undefined) {
file = direct
} else if (avatar != null) {
const url = getFileUrl(avatar, 'full')
file = await (await fetch(url)).blob()
} else {
return inputRef.click()
let selectedAvatarType: AvatarType | undefined = avatar?.includes('://') ? (schema as AvatarType) : AvatarType.IMAGE
let selectedAvatar: string | null | undefined = selectedAvatarType === AvatarType.IMAGE ? avatar : uri
export async function createAvatar (): Promise<string | undefined> {
if (selectedAvatarType === AvatarType.IMAGE && direct !== undefined) {
const uploadFile = await getResource(attachment.helper.UploadFile)
const file = new File([direct], 'avatar')
return await uploadFile(file)
}
if (selectedAvatarType && selectedAvatar) {
return `${selectedAvatarType}://${selectedAvatar}`
}
showPopup(EditAvatarPopup, { file }, undefined, (blob) => {
if (blob === undefined) {
return
}
if (blob === null) {
direct = undefined
dispatch('remove')
} else {
direct = blob
dispatch('done', { file: new File([blob], 'avatar') })
}
})
}
function onSelect (e: any) {
const file = e.target?.files[0] as File | undefined
if (file === undefined || !targetMimes.includes(file.type)) {
return
export async function removeAvatar (avatar: string) {
if (!avatar.includes('://')) {
const deleteFile = await getResource(attachment.helper.DeleteFile)
await deleteFile(avatar)
}
}
showPopup(EditAvatarPopup, { file }, undefined, (blob) => {
if (blob === undefined) {
return
}
if (blob === null) {
direct = undefined
dispatch('remove')
} else {
direct = blob
dispatch('done', { file: new File([blob], file.name) })
}
})
e.target.value = null
function handlePopupSubmit (submittedAvatarType?: AvatarType, submittedAvatar?: string, submittedDirect?: Blob) {
selectedAvatarType = submittedAvatarType
selectedAvatar = submittedAvatar
direct = submittedDirect
dispatch('done')
}
const dispatch = createEventDispatcher()
async function showSelectionPopup (e: MouseEvent) {
showPopup(SelectAvatarPopup, { avatar, email, id, icon, onSubmit: handlePopupSubmit })
}
</script>
<div class="cursor-pointer" on:click={onClick}>
<Avatar {avatar} {direct} {size} {icon} />
<input style="display: none;" type="file" bind:this={inputRef} on:change={onSelect} accept={targetMimes.join(',')} />
<div class="cursor-pointer" on:click|self={showSelectionPopup}>
<AvatarComponent
avatar={selectedAvatarType === AvatarType.IMAGE ? selectedAvatar : `${selectedAvatarType}://${selectedAvatar}`}
{direct}
{size}
{icon}
/>
</div>

View File

@ -0,0 +1,180 @@
<!--
// Copyright © 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 { DropdownLabelsIntl, AnySvelteComponent, showPopup, Label } from '@hcengineering/ui'
import { AvatarType } from '@hcengineering/contact'
import { Asset } from '@hcengineering/platform'
import presentation from '..'
import { getAvatarTypeDropdownItems, getFileUrl, getAvatarColorForId } from '../utils'
import { buildGravatarId } from '../gravatar'
import Card from './Card.svelte'
import AvatarComponent from './Avatar.svelte'
import EditAvatarPopup from './EditAvatarPopup.svelte'
export let avatar: string | undefined
export let email: string | undefined
export let id: string
export let file: Blob | undefined
export let icon: Asset | AnySvelteComponent | undefined
export let onSubmit: (avatarType?: AvatarType, avatar?: string, file?: Blob) => void
const [schema, uri] = avatar?.split('://') || []
const initialSelectedType = (() => {
if (!avatar) {
return AvatarType.COLOR
}
return avatar.includes('://') ? (schema as AvatarType) : AvatarType.IMAGE
})()
const initialSelectedAvatar = (() => {
if (!avatar) {
return getAvatarColorForId(id)
}
return avatar.includes('://') ? uri : avatar
})()
let selectedAvatarType: AvatarType = initialSelectedType
let selectedAvatar: string = initialSelectedAvatar
let selectedFile: Blob | undefined = file
const dispatch = createEventDispatcher()
function submit () {
onSubmit(selectedAvatarType, selectedAvatar, selectedAvatarType === AvatarType.IMAGE ? selectedFile : undefined)
}
let inputRef: HTMLInputElement
const targetMimes = ['image/png', 'image/jpg', 'image/jpeg']
function handleDropdownSelection (e: any) {
if (selectedAvatarType === AvatarType.GRAVATAR && email) {
selectedAvatar = buildGravatarId(email)
} else if (selectedAvatarType === AvatarType.IMAGE) {
if (selectedFile) {
return
}
if (file) {
selectedFile = file
} else if (avatar && !avatar.includes('://')) {
selectedAvatar = avatar
} else {
inputRef.click()
}
} else {
selectedAvatar = getAvatarColorForId(id)
}
}
async function handleImageAvatarClick () {
let editableFile: Blob
if (selectedFile !== undefined) {
editableFile = selectedFile
} else if (selectedAvatar) {
const url = getFileUrl(selectedAvatar, 'full')
editableFile = await (await fetch(url)).blob()
} else {
return inputRef.click()
}
showCropper(editableFile)
}
function showCropper (editableFile: Blob) {
showPopup(EditAvatarPopup, { file: editableFile }, undefined, (blob) => {
if (blob === undefined) {
if (!selectedFile && (!avatar || avatar.includes('://'))) {
selectedAvatarType = AvatarType.COLOR
selectedAvatar = getAvatarColorForId(id)
}
return
}
if (blob === null) {
selectedAvatarType = AvatarType.COLOR
selectedAvatar = getAvatarColorForId(id)
selectedFile = undefined
} else {
selectedFile = blob
}
})
}
function onSelectFile (e: any) {
const targetFile = e.target?.files[0] as File | undefined
if (targetFile === undefined || !targetMimes.includes(targetFile.type)) {
return
}
showCropper(targetFile)
e.target.value = null
document.body.onfocus = null
}
function handleFileSelectionCancel () {
document.body.onfocus = null
if (!inputRef.value.length) {
if (!selectedFile) {
selectedAvatarType = AvatarType.COLOR
selectedAvatar = getAvatarColorForId(id)
}
}
}
</script>
<Card
label={presentation.string.SelectAvatar}
okLabel={presentation.string.Save}
canSave={selectedAvatarType !== initialSelectedType ||
selectedAvatar !== initialSelectedAvatar ||
selectedFile !== file ||
!avatar}
okAction={submit}
on:close={() => {
dispatch('close')
}}
>
<DropdownLabelsIntl
items={getAvatarTypeDropdownItems(!!email)}
label={presentation.string.SelectAvatar}
bind:selected={selectedAvatarType}
on:selected={handleDropdownSelection}
/>
{#if selectedAvatarType === AvatarType.IMAGE}
<div class="cursor-pointer" on:click|self={handleImageAvatarClick}>
<AvatarComponent avatar={selectedAvatar} direct={selectedFile} size={'x-large'} {icon} />
</div>
{:else}
<AvatarComponent avatar={`${selectedAvatarType}://${selectedAvatar}`} size={'x-large'} {icon} />
{/if}
{#if selectedAvatarType === AvatarType.GRAVATAR}
<span>
<Label label={presentation.string.GravatarsManaged} />
<a target="”_blank”" href="//gravatar.com">Gravatar.com</a>
</span>
{/if}
<input
style="display: none;"
type="file"
bind:this={inputRef}
on:change={onSelectFile}
on:click={() => (document.body.onfocus = handleFileSelectionCancel)}
accept={targetMimes.join(',')}
/>
</Card>

View File

@ -0,0 +1,64 @@
//
// Copyright © 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.
//
import { IconSize } from '@hcengineering/ui'
import MD5 from 'crypto-js/md5'
/**
* @public
*/
export type GravatarPlaceholderType =
| '404'
| 'mp'
| 'identicon'
| 'monsterid'
| 'wavatar'
| 'retro'
| 'robohash'
| 'blank'
/**
* @public
*/
export function buildGravatarId (email: string): string {
return MD5(email.trim().toLowerCase()).toString()
}
/**
* @public
*/
export function getGravatarUrl (
gravatarId: string,
size: IconSize = 'full',
placeholder: GravatarPlaceholderType = 'identicon'
): string {
let width = 64
switch (size) {
case 'inline':
case 'tiny':
case 'x-small':
case 'small':
case 'medium':
width = 64
break
case 'large':
width = 256
break
case 'x-large':
width = 512
break
}
return `https://gravatar.com/avatar/${gravatarId}?s=${width}&d=${placeholder}`
}

View File

@ -47,6 +47,7 @@ export { connect, versionError } from './connect'
export { default } from './plugin'
export * from './types'
export * from './utils'
export * from './gravatar'
export { presentationId }
addStringsLoader(presentationId, async (lang: string) => {

View File

@ -54,7 +54,9 @@ export default plugin(presentationId, {
NoMatchesFound: '' as IntlString,
NotInThis: '' as IntlString,
Add: '' as IntlString,
Edit: '' as IntlString
Edit: '' as IntlString,
SelectAvatar: '' as IntlString,
GravatarsManaged: '' as IntlString
},
metadata: {
RequiredVersion: '' as Metadata<string>

View File

@ -34,11 +34,12 @@ import core, {
TxResult
} from '@hcengineering/core'
import login from '@hcengineering/login'
import { getMetadata } from '@hcengineering/platform'
import { getMetadata, Resource } from '@hcengineering/platform'
import { LiveQuery as LQ } from '@hcengineering/query'
import { onDestroy } from 'svelte'
import { deepEqual } from 'fast-equals'
import { IconSize } from '@hcengineering/ui'
import { IconSize, DropdownIntlItem } from '@hcengineering/ui'
import contact, { AvatarType, AvatarProvider } from '@hcengineering/contact'
let liveQuery: LQ
let client: TxOperations
@ -233,3 +234,66 @@ export function getAttributePresenterClass (
}
return { attrClass, category }
}
export function getAvatarTypeDropdownItems (hasEmail: boolean): DropdownIntlItem[] {
return [
{
id: AvatarType.COLOR,
label: contact.string.UseColor
},
{
id: AvatarType.IMAGE,
label: contact.string.UseImage
},
...(hasEmail
? [
{
id: AvatarType.GRAVATAR,
label: contact.string.UseGravatar
}
]
: [])
]
}
const AVATAR_COLORS = [
'#4674ca', // blue
'#315cac', // blue_dark
'#57be8c', // green
'#3fa372', // green_dark
'#f9a66d', // yellow_orange
'#ec5e44', // red
'#e63717', // red_dark
'#f868bc', // pink
'#6c5fc7', // purple
'#4e3fb4', // purple_dark
'#57b1be', // teal
'#847a8c' // gray
]
export function getAvatarColorForId (id: string): string {
let hash = 0
for (let i = 0; i < id.length; i++) {
hash += id.charCodeAt(i)
}
return AVATAR_COLORS[hash % AVATAR_COLORS.length]
}
export function getAvatarProviderId (avatar?: string | null): Resource<AvatarProvider> | undefined {
if (avatar === null || avatar === undefined || avatar === '') {
return
}
if (!avatar.includes('://')) {
return contact.avatarProvider.Image
}
const [schema] = avatar.split('://')
switch (schema) {
case AvatarType.GRAVATAR:
return contact.avatarProvider.Gravatar
case AvatarType.COLOR:
return contact.avatarProvider.Color
}
}

View File

@ -37,11 +37,13 @@
const client = getClient()
const dispatch = createEventDispatcher()
async function getAvatar (_id?: Ref<Employee>): Promise<string | undefined | null> {
if (_id === undefined) return (await client.findOne(contact.class.Employee, { _id }))?.avatar
async function getEmployee (_id?: Ref<Employee>): Promise<Employee | undefined> {
if (_id) {
return await client.findOne(contact.class.Employee, { _id })
}
}
function getEmployee (message: ChunterMessage): EmployeeAccount | undefined {
function getEmployeeAccount (message: ChunterMessage): EmployeeAccount | undefined {
return employeeAcounts?.find((e) => e._id === message.createBy)
}
</script>
@ -50,10 +52,10 @@
{#each pinnedMessages as message}
<div class="message">
<div class="header">
{#await getEmployee(message) then employeeAccount}
{#await getAvatar(employeeAccount?.employee) then avatar}
{#await getEmployeeAccount(message) then employeeAccount}
{#await getEmployee(employeeAccount?.employee) then employee}
<div class="avatar">
<Avatar size={'medium'} {avatar} />
<Avatar size={'medium'} avatar={employee?.avatar} />
</div>
{/await}
<span class="name">

View File

@ -69,6 +69,9 @@
"Email": "Email",
"CreateEmployee": "Create an employee",
"Inactive": "Inactive",
"Birthday": "Birthday"
"Birthday": "Birthday",
"UseImage": "Upload an image",
"UseGravatar": "Use Gravatar",
"UseColor": "Use color"
}
}

View File

@ -69,6 +69,9 @@
"Email": "Email",
"CreateEmployee": "Создать сотрудника",
"Inactive": "Не активный",
"Birthday": "День рождения"
"Birthday": "День рождения",
"UseImage": "Загрузить фото",
"UseGravatar": "Использовать Gravatar",
"UseColor": "Использовать цвет"
}
}

View File

@ -13,10 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import attachment from '@hcengineering/attachment'
import { Channel, combineName, Employee, findPerson, Person } from '@hcengineering/contact'
import core, { AccountRole, AttachedData, Data, generateId, Ref } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { Card, EditableAvatar, getClient } from '@hcengineering/presentation'
import { EditBox, IconInfo, Label, createFocusManager, FocusHandler } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
@ -24,6 +22,8 @@
import contact from '../plugin'
import PersonPresenter from './PersonPresenter.svelte'
let avatarEditor: EditableAvatar
let firstName = ''
let lastName = ''
let email = ''
@ -39,18 +39,6 @@
const dispatch = createEventDispatcher()
const client = getClient()
let avatar: File | undefined
function onAvatarDone (e: any) {
const { file } = e.detail
avatar = file
}
function removeAvatar (): void {
avatar = undefined
}
async function createPerson () {
const name = combineName(firstName, lastName)
const person: Data<Employee> = {
@ -59,10 +47,7 @@
active: true
}
if (avatar !== undefined) {
const uploadFile = await getResource(attachment.helper.UploadFile)
person.avatar = await uploadFile(avatar)
}
person.avatar = await avatarEditor.createAvatar()
await client.createDoc(contact.class.Employee, contact.space.Contacts, person, id)
@ -133,7 +118,7 @@
</div>
</div>
<div class="ml-4">
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} on:remove={removeAvatar} />
<EditableAvatar avatar={object.avatar} {email} {id} size={'large'} bind:this={avatarEditor} />
</div>
</div>
<svelte:fragment slot="pool">

View File

@ -13,10 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import attachment from '@hcengineering/attachment'
import { Channel, combineName, findPerson, Person } from '@hcengineering/contact'
import { AttachedData, Data, generateId } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { Card, EditableAvatar, getClient } from '@hcengineering/presentation'
import { EditBox, IconInfo, Label, createFocusManager, FocusHandler } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
@ -24,6 +22,8 @@
import contact from '../plugin'
import PersonPresenter from './PersonPresenter.svelte'
let avatarEditor: EditableAvatar
let firstName = ''
let lastName = ''
@ -38,28 +38,13 @@
const dispatch = createEventDispatcher()
const client = getClient()
let avatar: File | undefined
function onAvatarDone (e: any) {
const { file } = e.detail
avatar = file
}
function removeAvatar (): void {
avatar = undefined
}
async function createPerson () {
const person: Data<Person> = {
name: combineName(firstName, lastName),
city: object.city
}
if (avatar !== undefined) {
const uploadFile = await getResource(attachment.helper.UploadFile)
person.avatar = await uploadFile(avatar)
}
person.avatar = await avatarEditor.createAvatar()
await client.createDoc(contact.class.Person, contact.space.Contacts, person, id)
@ -128,7 +113,7 @@
</div>
</div>
<div class="ml-4">
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} on:remove={removeAvatar} />
<EditableAvatar avatar={object.avatar} {id} size={'large'} bind:this={avatarEditor} />
</div>
</div>
<svelte:fragment slot="pool">

View File

@ -14,10 +14,8 @@
// limitations under the License.
-->
<script lang="ts">
import attachment from '@hcengineering/attachment'
import { combineName, EmployeeAccount, getFirstName, getLastName, Person } from '@hcengineering/contact'
import { combineName, Employee, EmployeeAccount, getFirstName, getLastName, Person } from '@hcengineering/contact'
import { AccountRole, getCurrentAccount, Ref, Space } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { AttributeEditor, Avatar, createQuery, EditableAvatar, getClient } from '@hcengineering/presentation'
import setting, { IntegrationType } from '@hcengineering/setting'
import { EditBox, createFocusManager, FocusHandler } from '@hcengineering/ui'
@ -31,6 +29,8 @@
const hierarchy = client.getHierarchy()
const account = getCurrentAccount() as EmployeeAccount
let avatarEditor: EditableAvatar
$: editable =
!hierarchy.isDerived(object._class, contact.class.Employee) ||
account.role === AccountRole.Owner ||
@ -40,6 +40,13 @@
$: setName(object)
let email: string | undefined
$: if (editable && hierarchy.isDerived(object._class, contact.class.Employee)) {
client.findOne(contact.class.EmployeeAccount, { employee: (object as Employee)._id }).then((acc) => {
email = acc?.email
})
}
function setName (object: Person) {
firstName = getFirstName(object.name)
lastName = getLastName(object.name)
@ -73,29 +80,15 @@
onMount(sendOpen)
async function onAvatarDone (e: any) {
const uploadFile = await getResource(attachment.helper.UploadFile)
const deleteFile = await getResource(attachment.helper.DeleteFile)
const { file: avatar } = e.detail
if (object.avatar != null) {
await deleteFile(object.avatar)
await avatarEditor.removeAvatar(object.avatar)
}
const uuid = await uploadFile(avatar)
const avatar = await avatarEditor.createAvatar()
await client.updateDoc(object._class, object.space, object._id, {
avatar: uuid
avatar: avatar
})
}
async function removeAvatar (): Promise<void> {
const deleteFile = await getResource(attachment.helper.DeleteFile)
if (object.avatar != null) {
await client.updateDoc(object._class, object.space, object._id, {
avatar: null
})
await deleteFile(object.avatar)
}
}
const manager = createFocusManager()
</script>
@ -106,7 +99,14 @@
<div class="mr-8">
{#key object}
{#if editable}
<EditableAvatar avatar={object.avatar} size={'x-large'} on:done={onAvatarDone} on:remove={removeAvatar} />
<EditableAvatar
avatar={object.avatar}
{email}
id={object._id}
size={'x-large'}
bind:this={avatarEditor}
on:done={onAvatarDone}
/>
{:else}
<Avatar avatar={object.avatar} size={'x-large'} />
{/if}

View File

@ -14,11 +14,19 @@
// limitations under the License.
//
import { Channel, Contact, Employee, formatName } from '@hcengineering/contact'
import { AvatarType, Channel, Contact, Employee, formatName } from '@hcengineering/contact'
import { Class, Client, DocumentQuery, Ref, RelatedDocument, WithLookup } from '@hcengineering/core'
import { leaveWorkspace } from '@hcengineering/login-resources'
import { Resources } from '@hcengineering/platform'
import { Avatar, getClient, MessageBox, ObjectSearchResult, UserInfo } from '@hcengineering/presentation'
import {
Avatar,
getClient,
MessageBox,
ObjectSearchResult,
UserInfo,
getFileUrl,
getGravatarUrl
} from '@hcengineering/presentation'
import { showPopup } from '@hcengineering/ui'
import Channels from './components/Channels.svelte'
import ChannelsDropdown from './components/ChannelsDropdown.svelte'
@ -159,5 +167,10 @@ export default async (): Promise<Resources> => ({
query: string,
filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }
) => await queryContact(contact.class.Organization, client, query, filter)
},
avatarProvider: {
Image: { type: AvatarType.IMAGE, getUrl: getFileUrl },
Gravatar: { type: AvatarType.GRAVATAR, getUrl: getGravatarUrl },
Color: { type: AvatarType.COLOR, getUrl: (uri: string) => uri }
}
})

View File

@ -27,9 +27,9 @@ import {
Timestamp,
UXObject
} from '@hcengineering/core'
import type { Asset, Plugin } from '@hcengineering/platform'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
import type { AnyComponent, IconSize } from '@hcengineering/ui'
import { ViewAction, Viewlet } from '@hcengineering/view'
/**
@ -69,6 +69,23 @@ export interface Channel extends AttachedDoc {
lastMessage?: Timestamp
}
/**
* @public
*/
export enum AvatarType {
COLOR = 'color',
IMAGE = 'image',
GRAVATAR = 'gravatar'
}
/**
* @public
*/
export interface AvatarProvider extends Doc {
type: AvatarType
getUrl: (uri: string, size: IconSize) => string
}
/**
* @public
*/
@ -167,6 +184,7 @@ export const contactId = 'contact' as Plugin
*/
const contactPlugin = plugin(contactId, {
class: {
AvatarProvider: '' as Ref<Class<AvatarProvider>>,
ChannelProvider: '' as Ref<Class<ChannelProvider>>,
Channel: '' as Ref<Class<Channel>>,
Contact: '' as Ref<Class<Contact>>,
@ -195,6 +213,11 @@ const contactPlugin = plugin(contactId, {
Facebook: '' as Ref<ChannelProvider>,
Homepage: '' as Ref<ChannelProvider>
},
avatarProvider: {
Color: '' as Resource<AvatarProvider>,
Image: '' as Resource<AvatarProvider>,
Gravatar: '' as Resource<AvatarProvider>
},
icon: {
ContactApplication: '' as Asset,
Phone: '' as Asset,
@ -226,7 +249,10 @@ const contactPlugin = plugin(contactId, {
PersonAlreadyExists: '' as IntlString,
Person: '' as IntlString,
Employee: '' as IntlString,
CreateOrganization: '' as IntlString
CreateOrganization: '' as IntlString,
UseImage: '' as IntlString,
UseGravatar: '' as IntlString,
UseColor: '' as IntlString
},
viewlet: {
TableMember: '' as Ref<Viewlet>,

View File

@ -20,42 +20,28 @@
import { Department } from '@hcengineering/hr'
import core, { getCurrentAccount, Ref, Space } from '@hcengineering/core'
import hr from '../plugin'
import { getResource } from '@hcengineering/platform'
import attachment from '@hcengineering/attachment'
import { ChannelsEditor } from '@hcengineering/contact-resources'
import setting, { IntegrationType } from '@hcengineering/setting'
export let object: Department
let avatarEditor: EditableAvatar
const dispatch = createEventDispatcher()
const client = getClient()
async function onAvatarDone (e: any) {
async function onAvatarDone () {
if (object === undefined) return
const uploadFile = await getResource(attachment.helper.UploadFile)
const deleteFile = await getResource(attachment.helper.DeleteFile)
const { file: avatar } = e.detail
if (object.avatar != null) {
await deleteFile(object.avatar)
await avatarEditor.removeAvatar(object.avatar)
}
const uuid = await uploadFile(avatar)
const avatar = await avatarEditor.createAvatar()
await client.updateDoc(object._class, object.space, object._id, {
avatar: uuid
avatar: avatar
})
}
async function removeAvatar (): Promise<void> {
if (object === undefined) return
const deleteFile = await getResource(attachment.helper.DeleteFile)
if (object.avatar != null) {
await client.updateDoc(object._class, object.space, object._id, {
avatar: null
})
await deleteFile(object.avatar)
}
}
async function nameChange (): Promise<void> {
if (object === undefined) return
await client.update(object, {
@ -92,10 +78,11 @@
{#key object}
<EditableAvatar
avatar={object.avatar}
id={object._id}
size={'x-large'}
icon={hr.icon.Department}
bind:this={avatarEditor}
on:done={onAvatarDone}
on:remove={removeAvatar}
/>
{/key}
</div>

View File

@ -13,14 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import attachment from '@hcengineering/attachment'
import { Channel, combineName, Contact, findContacts } from '@hcengineering/contact'
import { ChannelsDropdown } from '@hcengineering/contact-resources'
import PersonPresenter from '@hcengineering/contact-resources/src/components/PersonPresenter.svelte'
import contact from '@hcengineering/contact-resources/src/plugin'
import { AttachedData, Class, Data, Doc, generateId, MixinData, Ref, WithLookup } from '@hcengineering/core'
import type { Customer } from '@hcengineering/lead'
import { getResource } from '@hcengineering/platform'
import { Card, EditableAvatar, getClient } from '@hcengineering/presentation'
import {
Button,
@ -44,6 +42,8 @@
return firstName === '' && lastName === ''
}
let avatarEditor: EditableAvatar
let object: Customer = {
_class: contact.class.Person
} as Customer
@ -65,8 +65,7 @@
city: object.city
}
if (avatar !== undefined) {
const uploadFile = await getResource(attachment.helper.UploadFile)
candidate.avatar = await uploadFile(avatar)
candidate.avatar = await avatarEditor.createAvatar()
}
const candidateData: MixinData<Contact, Customer> = {
description: object.description
@ -108,15 +107,6 @@
}
}
function onAvatarDone (e: any) {
const { file } = e.detail
avatar = file
}
function removeAvatar (): void {
avatar = undefined
}
const targets = [
client.getModel().getObject(contact.class.Person),
client.getModel().getObject(contact.class.Organization)
@ -222,11 +212,11 @@
</div>
<div class="ml-4 flex">
<EditableAvatar
bind:direct={avatar}
avatar={object.avatar}
id={customerId}
size={'large'}
on:remove={removeAvatar}
on:done={onAvatarDone}
bind:this={avatarEditor}
bind:direct={avatar}
/>
</div>
</div>

View File

@ -69,6 +69,8 @@
return firstName === '' && lastName === '' && resume.uuid === undefined
}
let avatarEditor: EditableAvatar
let object: Candidate = {} as Candidate
const resume = {} as {
@ -126,8 +128,7 @@
city: object.city
}
if (avatar !== undefined) {
const uploadFile = await getResource(attachment.helper.UploadFile)
candidate.avatar = await uploadFile(avatar)
candidate.avatar = await avatarEditor.createAvatar()
}
const candidateData: MixinData<Person, Candidate> = {
title: object.title,
@ -256,7 +257,7 @@
object.city = doc.city
}
if (isUndef(object.avatar ?? undefined) && doc.avatar !== undefined) {
if (!object.avatar && doc.avatar !== undefined) {
// We had avatar, let's try to upload it.
const data = atob(doc.avatar)
let n = data.length
@ -366,12 +367,6 @@
manager.setFocusPos(102)
}
function onAvatarDone (e: any) {
const { file } = e.detail
avatar = file
}
function addTagRef (tag: TagElement): void {
skills = [
...skills,
@ -401,10 +396,6 @@
matchedChannels = p.channels
})
function removeAvatar (): void {
avatar = undefined
}
const manager = createFocusManager()
</script>
@ -465,11 +456,11 @@
</div>
<div class="ml-4">
<EditableAvatar
bind:this={avatarEditor}
bind:direct={avatar}
avatar={object.avatar}
id={candidateId}
size={'large'}
on:remove={removeAvatar}
on:done={onAvatarDone}
/>
</div>
</div>

View File

@ -20,22 +20,24 @@
import contact, { Employee, EmployeeAccount, getFirstName, getLastName } from '@hcengineering/contact'
import contactRes from '@hcengineering/contact-resources/src/plugin'
import { getCurrentAccount } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import attachment from '@hcengineering/attachment'
import { changeName, leaveWorkspace } from '@hcengineering/login-resources'
import { ChannelsEditor } from '@hcengineering/contact-resources'
import MessageBox from '@hcengineering/presentation/src/components/MessageBox.svelte'
const client = getClient()
let avatarEditor: EditableAvatar
let employee: Employee | undefined
let firstName: string
let lastName: string
const employeeQ = createQuery()
const account = getCurrentAccount() as EmployeeAccount
employeeQ.query(
contact.class.Employee,
{
_id: (getCurrentAccount() as EmployeeAccount).employee
_id: account.employee
},
(res) => {
employee = res[0]
@ -47,30 +49,16 @@
async function onAvatarDone (e: any) {
if (employee === undefined) return
const uploadFile = await getResource(attachment.helper.UploadFile)
const deleteFile = await getResource(attachment.helper.DeleteFile)
const { file: avatar } = e.detail
if (employee.avatar != null) {
await deleteFile(employee.avatar)
if (employee.avatar) {
await avatarEditor.removeAvatar(employee.avatar)
}
const uuid = await uploadFile(avatar)
const avatar = await avatarEditor.createAvatar()
await client.updateDoc(employee._class, employee.space, employee._id, {
avatar: uuid
avatar: avatar
})
}
async function removeAvatar (): Promise<void> {
if (employee === undefined) return
const deleteFile = await getResource(attachment.helper.DeleteFile)
if (employee.avatar != null) {
await client.updateDoc(employee._class, employee.space, employee._id, {
avatar: null
})
await deleteFile(employee.avatar)
}
}
const manager = createFocusManager()
async function leave (): Promise<void> {
@ -101,7 +89,14 @@
{#if employee}
<div class="flex flex-grow w-full">
<div class="mr-8">
<EditableAvatar avatar={employee.avatar} size={'x-large'} on:done={onAvatarDone} on:remove={removeAvatar} />
<EditableAvatar
avatar={employee.avatar}
email={account.email}
id={employee._id}
size={'x-large'}
bind:this={avatarEditor}
on:done={onAvatarDone}
/>
</div>
<div class="flex-grow flex-col">
<EditBox

View File

@ -13,7 +13,7 @@
// limitations under the f.
//
import contact, { combineName, Employee } from '@hcengineering/contact'
import contact, { AvatarType, combineName, Employee } from '@hcengineering/contact'
import core, { AccountRole, Ref, TxOperations } from '@hcengineering/core'
import platform, {
getMetadata,
@ -28,7 +28,7 @@ import platform, {
} from '@hcengineering/platform'
import { decodeToken, generateToken } from '@hcengineering/server-token'
import toolPlugin, { connect, initModel, upgradeModel, version } from '@hcengineering/server-tool'
import { pbkdf2Sync, randomBytes } from 'crypto'
import { createHash, pbkdf2Sync, randomBytes } from 'crypto'
import { Binary, Db, ObjectId } from 'mongodb'
const WORKSPACE_COLLECTION = 'workspace'
@ -483,10 +483,12 @@ export async function assignWorkspace (db: Db, email: string, workspace: string)
if (account !== null) await createEmployeeAccount(account, workspace)
}
async function createEmployee (ops: TxOperations, name: string): Promise<Ref<Employee>> {
async function createEmployee (ops: TxOperations, name: string, email: string): Promise<Ref<Employee>> {
const gravatarId = createHash('md5').update(email.trim().toLowerCase()).digest('hex')
return await ops.createDoc(contact.class.Employee, contact.space.Employee, {
name,
city: '',
avatar: `${AvatarType.GRAVATAR}://${gravatarId}`,
active: true
})
}
@ -502,7 +504,7 @@ async function createEmployeeAccount (account: Account, workspace: string): Prom
const existingAccount = await ops.findOne(contact.class.EmployeeAccount, { email: account.email })
if (existingAccount === undefined) {
const employee = await createEmployee(ops, name)
const employee = await createEmployee(ops, name, account.email)
await ops.createDoc(contact.class.EmployeeAccount, core.space.Model, {
email: account.email,
@ -514,7 +516,7 @@ async function createEmployeeAccount (account: Account, workspace: string): Prom
const employee = await ops.findOne(contact.class.Employee, { _id: existingAccount.employee })
if (employee === undefined) {
// Employee was deleted, let's restore it.
const employeeId = await createEmployee(ops, name)
const employeeId = await createEmployee(ops, name, account.email)
await ops.updateDoc(contact.class.EmployeeAccount, existingAccount.space, existingAccount._id, {
employee: employeeId