mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-30 12:20:00 +00:00
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:
parent
71f4e38dd6
commit
5426a06346
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -4,5 +4,6 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,8 @@
|
||||
"NoMatchesFound": "Не найдено соответсвий",
|
||||
"NotInThis": "Не в этом {space}",
|
||||
"Add": "Добавить",
|
||||
"Edit": "Редактировать"
|
||||
"Edit": "Редактировать",
|
||||
"SelectAvatar": "Выбрать аватар",
|
||||
"GravatarsManaged": "Граватары управляются через"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
180
packages/presentation/src/components/SelectAvatarPopup.svelte
Normal file
180
packages/presentation/src/components/SelectAvatarPopup.svelte
Normal 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>
|
64
packages/presentation/src/gravatar.ts
Normal file
64
packages/presentation/src/gravatar.ts
Normal 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}`
|
||||
}
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -69,6 +69,9 @@
|
||||
"Email": "Email",
|
||||
"CreateEmployee": "Создать сотрудника",
|
||||
"Inactive": "Не активный",
|
||||
"Birthday": "День рождения"
|
||||
"Birthday": "День рождения",
|
||||
"UseImage": "Загрузить фото",
|
||||
"UseGravatar": "Использовать Gravatar",
|
||||
"UseColor": "Использовать цвет"
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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}
|
||||
|
@ -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 }
|
||||
}
|
||||
})
|
||||
|
@ -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>,
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user