UBERF-7011: Switch to Ref<Blob> (#5661)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-05-27 14:25:57 +07:00 committed by GitHub
parent 27a98e0641
commit 8c6a5f4e9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
158 changed files with 1795 additions and 1049 deletions

View File

@ -11,6 +11,9 @@ dependencies:
'@aws-sdk/client-s3': '@aws-sdk/client-s3':
specifier: ^3.575.0 specifier: ^3.575.0
version: 3.577.0 version: 3.577.0
'@aws-sdk/s3-request-presigner':
specifier: ^3.582.0
version: 3.582.0
'@elastic/elasticsearch': '@elastic/elasticsearch':
specifier: ^7.14.0 specifier: ^7.14.0
version: 7.17.13 version: 7.17.13
@ -1857,6 +1860,21 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: false dev: false
/@aws-sdk/middleware-sdk-s3@3.582.0:
resolution: {integrity: sha512-PJqQpLoLaZPRI4L/XZUeHkd9UVK8VAr9R38wv0osGeMTvzD9iwzzk0I2TtBqFda/5xEB1YgVYZwyqvmStXmttg==}
engines: {node: '>=16.0.0'}
dependencies:
'@aws-sdk/types': 3.577.0
'@aws-sdk/util-arn-parser': 3.568.0
'@smithy/node-config-provider': 3.0.0
'@smithy/protocol-http': 4.0.0
'@smithy/signature-v4': 3.0.0
'@smithy/smithy-client': 3.0.1
'@smithy/types': 3.0.0
'@smithy/util-config-provider': 3.0.0
tslib: 2.6.2
dev: false
/@aws-sdk/middleware-signing@3.577.0: /@aws-sdk/middleware-signing@3.577.0:
resolution: {integrity: sha512-QS/dh3+NqZbXtY0j/DZ867ogP413pG5cFGqBy9OeOhDMsolcwLrQbi0S0c621dc1QNq+er9ffaMhZ/aPkyXXIg==} resolution: {integrity: sha512-QS/dh3+NqZbXtY0j/DZ867ogP413pG5cFGqBy9OeOhDMsolcwLrQbi0S0c621dc1QNq+er9ffaMhZ/aPkyXXIg==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@ -1902,6 +1920,20 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: false dev: false
/@aws-sdk/s3-request-presigner@3.582.0:
resolution: {integrity: sha512-h2tn0IjJ3Tsnh0Ep8FUqYwAJIjursK68gegrWEUpf7oeJlJer5gaNlD5CXCeRHwyhNiA1uzHaX4BjjyeKHl0Kw==}
engines: {node: '>=16.0.0'}
dependencies:
'@aws-sdk/signature-v4-multi-region': 3.582.0
'@aws-sdk/types': 3.577.0
'@aws-sdk/util-format-url': 3.577.0
'@smithy/middleware-endpoint': 3.0.0
'@smithy/protocol-http': 4.0.0
'@smithy/smithy-client': 3.0.1
'@smithy/types': 3.0.0
tslib: 2.6.2
dev: false
/@aws-sdk/signature-v4-multi-region@3.577.0: /@aws-sdk/signature-v4-multi-region@3.577.0:
resolution: {integrity: sha512-mMykGRFBYmlDcMhdbhNM0z1JFUaYYZ8r9WV7Dd0T2PWELv2brSAjDAOBHdJLHObDMYRnM6H0/Y974qTl3icEcQ==} resolution: {integrity: sha512-mMykGRFBYmlDcMhdbhNM0z1JFUaYYZ8r9WV7Dd0T2PWELv2brSAjDAOBHdJLHObDMYRnM6H0/Y974qTl3icEcQ==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@ -1914,6 +1946,18 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: false dev: false
/@aws-sdk/signature-v4-multi-region@3.582.0:
resolution: {integrity: sha512-aFCOjjNqEX2l+V8QjOWy5F7CtHIC/RlYdBuv3No6yxn+pMvVUUe6zdMk2yHWcudVpHWsyvcZzAUBliAPeFLPsQ==}
engines: {node: '>=16.0.0'}
dependencies:
'@aws-sdk/middleware-sdk-s3': 3.582.0
'@aws-sdk/types': 3.577.0
'@smithy/protocol-http': 4.0.0
'@smithy/signature-v4': 3.0.0
'@smithy/types': 3.0.0
tslib: 2.6.2
dev: false
/@aws-sdk/token-providers@3.577.0(@aws-sdk/client-sso-oidc@3.577.0): /@aws-sdk/token-providers@3.577.0(@aws-sdk/client-sso-oidc@3.577.0):
resolution: {integrity: sha512-0CkIZpcC3DNQJQ1hDjm2bdSy/Xjs7Ny5YvSsacasGOkNfk+FdkiQy6N67bZX3Zbc9KIx+Nz4bu3iDeNSNplnnQ==} resolution: {integrity: sha512-0CkIZpcC3DNQJQ1hDjm2bdSy/Xjs7Ny5YvSsacasGOkNfk+FdkiQy6N67bZX3Zbc9KIx+Nz4bu3iDeNSNplnnQ==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@ -1953,6 +1997,16 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: false dev: false
/@aws-sdk/util-format-url@3.577.0:
resolution: {integrity: sha512-SyEGC2J+y/krFRuPgiF02FmMYhqbiIkOjDE6k4nYLJQRyS6XEAGxZoG+OHeOVEM+bsDgbxokXZiM3XKGu6qFIg==}
engines: {node: '>=16.0.0'}
dependencies:
'@aws-sdk/types': 3.577.0
'@smithy/querystring-builder': 3.0.0
'@smithy/types': 3.0.0
tslib: 2.6.2
dev: false
/@aws-sdk/util-locate-window@3.568.0: /@aws-sdk/util-locate-window@3.568.0:
resolution: {integrity: sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==} resolution: {integrity: sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@ -19662,7 +19716,7 @@ packages:
dev: false dev: false
file:projects/drive-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2): file:projects/drive-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-E2zO+OiX83Ig7B8ZJHiKJZydZf0RqPxXrHBnGl99l3biAcxNId4UJryMUM8KC7PQQw16ue/bERRU1dOBIs9Hng==, tarball: file:projects/drive-resources.tgz} resolution: {integrity: sha512-BxbFyIUUuLwK5yzY3OTgPRmKE1fZxRUW9ZJ3iC/7nxvu5b32end9aJqm2fwZwNQZJxxeyw1AMbf9t7EDiadfMg==, tarball: file:projects/drive-resources.tgz}
id: file:projects/drive-resources.tgz id: file:projects/drive-resources.tgz
name: '@rush-temp/drive-resources' name: '@rush-temp/drive-resources'
version: 0.0.0 version: 0.0.0
@ -20629,7 +20683,7 @@ packages:
dev: false dev: false
file:projects/minio.tgz(esbuild@0.20.1)(ts-node@10.9.2): file:projects/minio.tgz(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-9iaGGSGj2mgvQwhe6oLTf5RzaROFLBsi+kU/nMGXxw3m7pvmELcgVW2Ijk4R+P/AHBZF5TDp0jWdFif8WWvwXg==, tarball: file:projects/minio.tgz} resolution: {integrity: sha512-wjtXX+XX515IIjugAtzTrJN8TyMp3iPEgBlQBXvxMuOpIBEmLQLnuOOD4LN7S9sdKYLXOP0IgmDVwyDp0bZvyw==, tarball: file:projects/minio.tgz}
id: file:projects/minio.tgz id: file:projects/minio.tgz
name: '@rush-temp/minio' name: '@rush-temp/minio'
version: 0.0.0 version: 0.0.0
@ -22817,12 +22871,13 @@ packages:
dev: false dev: false
file:projects/s3.tgz(esbuild@0.20.1)(ts-node@10.9.2): file:projects/s3.tgz(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-2gkJlE5D6k8qqIbcjFSxW2ytwlziBej2ZJz7KRaEiAU5uDaMCiL378VaVvS1DmvPvPxmHJ7swZ5IkL/WRqxwqg==, tarball: file:projects/s3.tgz} resolution: {integrity: sha512-545xE/hFO0K5PpSsSnM7hTqQilfo1Psno9mMs6XSYEC8SZQFX/mV1j0/W9l3++yX7lZQwxHKHBv/morA8v/RAA==, tarball: file:projects/s3.tgz}
id: file:projects/s3.tgz id: file:projects/s3.tgz
name: '@rush-temp/s3' name: '@rush-temp/s3'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@aws-sdk/client-s3': 3.577.0 '@aws-sdk/client-s3': 3.577.0
'@aws-sdk/s3-request-presigner': 3.582.0
'@types/jest': 29.5.12 '@types/jest': 29.5.12
'@types/node': 20.11.19 '@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)

View File

@ -17,42 +17,42 @@ import attachment from '@hcengineering/attachment'
import chunter, { type ChatMessage } from '@hcengineering/chunter' import chunter, { type ChatMessage } from '@hcengineering/chunter'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import core, { import core, {
ClassifierKind,
DOMAIN_STATUS,
DOMAIN_TX, DOMAIN_TX,
type MeasureContext,
SortingOrder, SortingOrder,
TxOperations, TxOperations,
TxProcessor, TxProcessor,
generateId, generateId,
getObjectValue, getObjectValue,
toIdMap,
type BackupClient, type BackupClient,
type Class,
type Client as CoreClient, type Client as CoreClient,
type Doc, type Doc,
type Domain, type Domain,
type MeasureContext,
type Ref, type Ref,
type TxCreateDoc,
type WorkspaceId,
type StatusCategory,
type TxMixin,
type TxCUD,
type TxUpdateDoc,
DOMAIN_STATUS,
type Status, type Status,
toIdMap, type StatusCategory,
type Class, type TxCUD,
ClassifierKind type TxCreateDoc,
type TxMixin,
type TxUpdateDoc,
type WorkspaceId
} from '@hcengineering/core' } from '@hcengineering/core'
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
import recruitModel, { defaultApplicantStatuses } from '@hcengineering/model-recruit'
import { getWorkspaceDB } from '@hcengineering/mongo' import { getWorkspaceDB } from '@hcengineering/mongo'
import recruit, { type Applicant, type Vacancy } from '@hcengineering/recruit' import recruit, { type Applicant, type Vacancy } from '@hcengineering/recruit'
import recruitModel, { defaultApplicantStatuses } from '@hcengineering/model-recruit'
import { type StorageAdapter } from '@hcengineering/server-core' import { type StorageAdapter } from '@hcengineering/server-core'
import { connect } from '@hcengineering/server-tool' import { connect } from '@hcengineering/server-tool'
import tags, { type TagCategory, type TagElement, type TagReference } from '@hcengineering/tags' import tags, { type TagCategory, type TagElement, type TagReference } from '@hcengineering/tags'
import task, { type Task, type ProjectType, type TaskType } from '@hcengineering/task' import task, { type ProjectType, type Task, type TaskType } from '@hcengineering/task'
import tracker from '@hcengineering/tracker' import tracker from '@hcengineering/tracker'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { MongoClient } from 'mongodb' import { MongoClient } from 'mongodb'
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import { DOMAIN_SPACE } from '@hcengineering/model-core'
export async function cleanWorkspace ( export async function cleanWorkspace (
ctx: MeasureContext, ctx: MeasureContext,
@ -77,7 +77,7 @@ export async function cleanWorkspace (
const contacts = await ops.findAll(contact.class.Contact, {}) const contacts = await ops.findAll(contact.class.Contact, {})
const files = new Set( const files = new Set(
attachments.map((it) => it.file).concat(contacts.map((it) => it.avatar).filter((it) => it) as string[]) attachments.map((it) => it.file as string).concat(contacts.map((it) => it.avatar).filter((it) => it) as string[])
) )
const minioList = await storageAdapter.listStream(ctx, workspaceId) const minioList = await storageAdapter.listStream(ctx, workspaceId)
@ -177,7 +177,7 @@ export async function fixMinioBW (
break break
} }
if (obj.modifiedOn < from) continue if (obj.modifiedOn < from) continue
if ((obj._id as string).includes('%size%')) { if ((obj._id as string).includes('%preview%')) {
await storageService.remove(ctx, workspaceId, [obj._id]) await storageService.remove(ctx, workspaceId, [obj._id])
removed++ removed++
if (removed % 100 === 0) { if (removed % 100 === 0) {

View File

@ -15,18 +15,18 @@
import activity from '@hcengineering/activity' import activity from '@hcengineering/activity'
import type { Attachment, AttachmentMetadata, Photo, SavedAttachments } from '@hcengineering/attachment' import type { Attachment, AttachmentMetadata, Photo, SavedAttachments } from '@hcengineering/attachment'
import { type Domain, IndexKind, type Ref } from '@hcengineering/core' import { IndexKind, type Blob, type Domain, type Ref } from '@hcengineering/core'
import { import {
type Builder,
Index, Index,
Model, Model,
Prop, Prop,
TypeAttachment, TypeBlob,
TypeBoolean, TypeBoolean,
TypeRef, TypeRef,
TypeString, TypeString,
TypeTimestamp, TypeTimestamp,
UX UX,
type Builder
} from '@hcengineering/model' } from '@hcengineering/model'
import core, { TAttachedDoc } from '@hcengineering/model-core' import core, { TAttachedDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference' import preference, { TPreference } from '@hcengineering/model-preference'
@ -46,8 +46,8 @@ export class TAttachment extends TAttachedDoc implements Attachment {
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
name!: string name!: string
@Prop(TypeAttachment(), attachment.string.File) @Prop(TypeBlob(), attachment.string.File)
file!: string file!: Ref<Blob>
@Prop(TypeString(), attachment.string.Size) @Prop(TypeString(), attachment.string.Size)
size!: number size!: number

View File

@ -36,6 +36,7 @@ import {
DOMAIN_MODEL, DOMAIN_MODEL,
DateRangeMode, DateRangeMode,
IndexKind, IndexKind,
type Blob,
type Class, type Class,
type Domain, type Domain,
type Markup, type Markup,
@ -50,10 +51,11 @@ import {
Model, Model,
Prop, Prop,
ReadOnly, ReadOnly,
TypeAttachment, TypeBlob,
TypeBoolean, TypeBoolean,
TypeCollaborativeMarkup, TypeCollaborativeMarkup,
TypeDate, TypeDate,
TypeRecord,
TypeRef, TypeRef,
TypeString, TypeString,
TypeTimestamp, TypeTimestamp,
@ -104,10 +106,23 @@ export class TContact extends TDoc implements Contact {
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
name!: string name!: string
@Prop(TypeAttachment(), contact.string.Avatar) @Prop(TypeString(), contact.string.Avatar)
@Index(IndexKind.FullText) @Index(IndexKind.FullText)
@Hidden() @Hidden()
avatar?: string | null avatarType!: AvatarType
@Prop(TypeBlob(), contact.string.Avatar)
@Index(IndexKind.FullText)
@Hidden()
avatar!: Ref<Blob> | null | undefined
@Prop(TypeRecord(), contact.string.Avatar)
@Index(IndexKind.FullText)
@Hidden()
avatarProps?: {
color?: string
url?: string
}
@Prop(Collection(contact.class.Channel), contact.string.ContactInfo) @Prop(Collection(contact.class.Channel), contact.string.ContactInfo)
channels?: number channels?: number
@ -709,6 +724,16 @@ export function createModel (builder: Builder): void {
contact.avatarProvider.Gravatar contact.avatarProvider.Gravatar
) )
builder.createDoc(
contact.class.AvatarProvider,
core.space.Model,
{
type: AvatarType.EXTERNAL,
getUrl: contact.function.GetExternalUrl
},
contact.avatarProvider.Color
)
builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectPresenter, { builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.PersonPresenter presenter: contact.component.PersonPresenter
}) })

View File

@ -1,12 +1,14 @@
// //
import { DOMAIN_TX, type Space, TxOperations, type Class, type Doc, type Domain, type Ref } from '@hcengineering/core' import { DOMAIN_TX, TxOperations, type Class, type Doc, type Domain, type Ref, type Space } from '@hcengineering/core'
import { import {
createDefaultSpace, createDefaultSpace,
tryMigrate, tryMigrate,
tryUpgrade, tryUpgrade,
type MigrateOperation, type MigrateOperation,
type MigrateUpdate,
type MigrationClient, type MigrationClient,
type MigrationDocumentQuery,
type MigrationUpgradeClient, type MigrationUpgradeClient,
type ModelLogger type ModelLogger
} from '@hcengineering/model' } from '@hcengineering/model'
@ -14,6 +16,7 @@ import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
import core from '@hcengineering/model-core' import core from '@hcengineering/model-core'
import { DOMAIN_VIEW } from '@hcengineering/model-view' import { DOMAIN_VIEW } from '@hcengineering/model-view'
import { AvatarType, type Contact } from '@hcengineering/contact'
import contact, { DOMAIN_CONTACT, contactId } from './index' import contact, { DOMAIN_CONTACT, contactId } from './index'
async function createEmployeeEmail (client: TxOperations): Promise<void> { async function createEmployeeEmail (client: TxOperations): Promise<void> {
@ -48,6 +51,55 @@ async function createEmployeeEmail (client: TxOperations): Promise<void> {
} }
} }
const colorPrefix = 'color://'
const gravatarPrefix = 'gravatar://'
async function migrateAvatars (client: MigrationClient): Promise<void> {
const classes = client.hierarchy.getDescendants(contact.class.Contact)
const i = await client.traverse<Contact>(DOMAIN_CONTACT, {
_class: { $in: classes },
avatar: { $regex: 'color|gravatar://.*' }
})
while (true) {
const docs = await i.next(50)
if (docs === null || docs?.length === 0) {
break
}
const updates: { filter: MigrationDocumentQuery<Contact>, update: MigrateUpdate<Contact> }[] = []
for (const d of docs) {
if (d.avatar?.startsWith(colorPrefix) ?? false) {
d.avatarProps = { color: d.avatar?.slice(colorPrefix.length) ?? '' }
updates.push({
filter: { _id: d._id },
update: {
avatarType: AvatarType.COLOR,
avatar: null,
avatarProps: { color: d.avatar?.slice(colorPrefix.length) ?? '' }
}
})
} else if (d.avatar?.startsWith(gravatarPrefix) ?? false) {
updates.push({
filter: { _id: d._id },
update: {
avatarType: AvatarType.GRAVATAR,
avatar: null,
avatarProps: { url: d.avatar?.slice(gravatarPrefix.length) ?? '' }
}
})
}
}
if (updates.length > 0) {
await client.bulk(DOMAIN_CONTACT, updates)
}
}
await client.update(
DOMAIN_CONTACT,
{ _class: { $in: classes }, avatarKind: { $exists: false } },
{ avatarKind: AvatarType.IMAGE }
)
}
export const contactOperation: MigrateOperation = { export const contactOperation: MigrateOperation = {
async migrate (client: MigrationClient, logger: ModelLogger): Promise<void> { async migrate (client: MigrationClient, logger: ModelLogger): Promise<void> {
await tryMigrate(client, contactId, [ await tryMigrate(client, contactId, [
@ -183,6 +235,12 @@ export const contactOperation: MigrateOperation = {
} }
) )
} }
},
{
state: 'avatars',
func: async (client) => {
await migrateAvatars(client)
}
} }
]) ])
}, },

View File

@ -213,8 +213,8 @@ export class TTypeString extends TType {}
export class TTypeRecord extends TType {} export class TTypeRecord extends TType {}
@UX(core.string.String) @UX(core.string.String)
@Model(core.class.TypeAttachment, core.class.Type) @Model(core.class.TypeBlob, core.class.Type)
export class TTypeAttachment extends TType {} export class TTypeBlob extends TType {}
@UX(core.string.Hyperlink) @UX(core.string.Hyperlink)
@Model(core.class.TypeHyperlink, core.class.Type) @Model(core.class.TypeHyperlink, core.class.Type)

View File

@ -60,7 +60,7 @@ import {
TRefTo, TRefTo,
TType, TType,
TTypeAny, TTypeAny,
TTypeAttachment, TTypeBlob,
TTypeBoolean, TTypeBoolean,
TTypeCollaborativeDoc, TTypeCollaborativeDoc,
TTypeCollaborativeDocVersion, TTypeCollaborativeDocVersion,
@ -153,7 +153,7 @@ export function createModel (builder: Builder): void {
TTypeString, TTypeString,
TTypeRank, TTypeRank,
TTypeRecord, TTypeRecord,
TTypeAttachment, TTypeBlob,
TTypeHyperlink, TTypeHyperlink,
TCollection, TCollection,
TVersion, TVersion,

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
// //
import { type Blob, type Class, type Doc, type Ref, DOMAIN_MODEL } from '@hcengineering/core' import { DOMAIN_MODEL, type Blob, type Class, type Doc, type Ref } from '@hcengineering/core'
import { type Builder, Model, Prop, TypeRef, TypeString } from '@hcengineering/model' import { Model, Prop, TypeRef, TypeString, type Builder } from '@hcengineering/model'
import core, { TDoc } from '@hcengineering/model-core' import core, { TDoc } from '@hcengineering/model-core'
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform' import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
// Import types to prevent .svelte components to being exposed to type typescript. // Import types to prevent .svelte components to being exposed to type typescript.
@ -27,12 +27,12 @@ import {
type ComponentPointExtension, type ComponentPointExtension,
type CreateExtensionKind, type CreateExtensionKind,
type DocAttributeRule, type DocAttributeRule,
type DocRules,
type DocCreateExtension, type DocCreateExtension,
type DocCreateFunction, type DocCreateFunction,
type DocRules,
type FilePreviewExtension, type FilePreviewExtension,
type ObjectSearchContext,
type ObjectSearchCategory, type ObjectSearchCategory,
type ObjectSearchContext,
type ObjectSearchFactory type ObjectSearchFactory
} from '@hcengineering/presentation/src/types' } from '@hcengineering/presentation/src/types'
import { type AnyComponent, type ComponentExtensionId } from '@hcengineering/ui/src/types' import { type AnyComponent, type ComponentExtensionId } from '@hcengineering/ui/src/types'

View File

@ -45,8 +45,8 @@ export function createModel (builder: Builder): void {
builder.mixin(contact.class.Contact, core.class.Class, serverCore.mixin.SearchPresenter, { builder.mixin(contact.class.Contact, core.class.Class, serverCore.mixin.SearchPresenter, {
searchConfig: { searchConfig: {
iconConfig: { iconConfig: {
component: contact.component.Avatar, component: contact.component.AvatarRef,
props: ['avatar', 'name'] props: ['_id']
}, },
title: { props: ['name'] } title: { props: ['name'] }
}, },

View File

@ -57,8 +57,8 @@ export function createModel (builder: Builder): void {
builder.mixin(recruit.class.Applicant, core.class.Class, serverCore.mixin.SearchPresenter, { builder.mixin(recruit.class.Applicant, core.class.Class, serverCore.mixin.SearchPresenter, {
searchConfig: { searchConfig: {
iconConfig: { iconConfig: {
component: contact.component.Avatar, component: contact.component.AvatarRef,
props: [{ avatar: ['attachedTo', 'avatar'] }, { name: ['attachedTo', 'name'] }] props: [{ _id: ['attachedTo'] }]
}, },
shortTitle: 'identifier', shortTitle: 'identifier',
title: { title: {

View File

@ -15,7 +15,7 @@
import activity from '@hcengineering/activity' import activity from '@hcengineering/activity'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import { AccountRole, DOMAIN_MODEL, type Account, type Domain, type Ref } from '@hcengineering/core' import { AccountRole, DOMAIN_MODEL, type Account, type Blob, type Domain, type Ref } from '@hcengineering/core'
import { Mixin, Model, type Builder, UX } from '@hcengineering/model' import { Mixin, Model, type Builder, UX } from '@hcengineering/model'
import core, { TClass, TConfiguration, TDoc } from '@hcengineering/model-core' import core, { TClass, TConfiguration, TDoc } from '@hcengineering/model-core'
import view, { createAction } from '@hcengineering/model-view' import view, { createAction } from '@hcengineering/model-view'
@ -105,7 +105,7 @@ export class TInviteSettings extends TConfiguration implements InviteSettings {
@Model(setting.class.WorkspaceSetting, core.class.Doc, DOMAIN_SETTING) @Model(setting.class.WorkspaceSetting, core.class.Doc, DOMAIN_SETTING)
export class TWorkspaceSetting extends TDoc implements WorkspaceSetting { export class TWorkspaceSetting extends TDoc implements WorkspaceSetting {
icon?: string icon?: Ref<Blob>
} }
@Mixin(setting.mixin.SpaceTypeEditor, core.class.Class) @Mixin(setting.mixin.SpaceTypeEditor, core.class.Class)

View File

@ -460,7 +460,7 @@ export function createModel (builder: Builder): void {
view.component.StringEditor, view.component.StringEditor,
view.component.StringEditorPopup view.component.StringEditorPopup
) )
classPresenter(builder, core.class.TypeAttachment, view.component.StringPresenter) classPresenter(builder, core.class.TypeBlob, view.component.StringPresenter)
classPresenter( classPresenter(
builder, builder,
core.class.TypeHyperlink, core.class.TypeHyperlink,

View File

@ -559,6 +559,22 @@ export interface Blob extends Doc {
size: number size: number
} }
/**
* For every blob will automatically add a lookup.
*
* It extends Blob to allow for $lookup operations work as expected.
*/
export interface BlobLookup extends Blob {
// An URL document could be downloaded from, with ${id} to put blobId into
downloadUrl: string
// A URL document could be updated at
uploadUrl?: string
// A URL document could be previewed at
previewUrl?: string
// A formats preview is available at
previewFormats?: string[]
}
/** /**
* @public * @public
* *

View File

@ -111,7 +111,7 @@ export default plugin(coreId, {
Account: '' as Ref<Class<Account>>, Account: '' as Ref<Class<Account>>,
Type: '' as Ref<Class<Type<any>>>, Type: '' as Ref<Class<Type<any>>>,
TypeString: '' as Ref<Class<Type<string>>>, TypeString: '' as Ref<Class<Type<string>>>,
TypeAttachment: '' as Ref<Class<Type<string>>>, TypeBlob: '' as Ref<Class<Type<Ref<Blob>>>>,
TypeIntlString: '' as Ref<Class<Type<IntlString>>>, TypeIntlString: '' as Ref<Class<Type<IntlString>>>,
TypeHyperlink: '' as Ref<Class<Type<Hyperlink>>>, TypeHyperlink: '' as Ref<Class<Type<Hyperlink>>>,
TypeNumber: '' as Ref<Class<Type<number>>>, TypeNumber: '' as Ref<Class<Type<number>>>,

View File

@ -13,20 +13,8 @@
// limitations under the License. // limitations under the License.
// //
import { LoadModelResponse } from '.' import type { Doc, Domain, Ref } from './classes'
import type { Class, Doc, Domain, Ref, Timestamp } from './classes'
import { Hierarchy } from './hierarchy'
import { MeasureContext, type FullParamsType, type ParamsType } from './measurements' import { MeasureContext, type FullParamsType, type ParamsType } from './measurements'
import { ModelDb } from './memdb'
import type {
DocumentQuery,
FindOptions,
FindResult,
SearchOptions,
SearchQuery,
SearchResult,
TxResult
} from './storage'
import type { Tx } from './tx' import type { Tx } from './tx'
/** /**
@ -78,24 +66,3 @@ export interface LowLevelStorage {
// Remove a list of documents. // Remove a list of documents.
clean: (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => Promise<void> clean: (ctx: MeasureContext, domain: Domain, docs: Ref<Doc>[]) => Promise<void>
} }
/**
* @public
*/
export interface ServerStorage extends LowLevelStorage {
hierarchy: Hierarchy
modelDb: ModelDb
findAll: <T extends Doc>(
ctx: MeasureContext,
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T> & {
domain?: Domain // Allow to find for Doc's in specified domain only.
prefix?: string
}
) => Promise<FindResult<T>>
searchFulltext: (ctx: MeasureContext, query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
tx: (ctx: SessionOperationContext, tx: Tx) => Promise<TxResult>
apply: (ctx: SessionOperationContext, tx: Tx[], broadcast: boolean) => Promise<TxResult>
close: () => Promise<void>
loadModel: (last: Timestamp, hash?: string) => Promise<Tx[] | LoadModelResponse>
}

View File

@ -214,7 +214,7 @@ export function extractDocKey (key: string): {
export function isFullTextAttribute (attr: AnyAttribute): boolean { export function isFullTextAttribute (attr: AnyAttribute): boolean {
return ( return (
attr.index === IndexKind.FullText || attr.index === IndexKind.FullText ||
attr.type._class === core.class.TypeAttachment || attr.type._class === core.class.TypeBlob ||
attr.type._class === core.class.EnumOf || attr.type._class === core.class.EnumOf ||
attr.type._class === core.class.TypeCollaborativeDoc attr.type._class === core.class.TypeCollaborativeDoc
) )

View File

@ -387,8 +387,8 @@ export function TypeString (): Type<string> {
/** /**
* @public * @public
*/ */
export function TypeAttachment (): Type<string> { export function TypeBlob (): Type<string> {
return { _class: core.class.TypeAttachment, label: core.string.String } return { _class: core.class.TypeBlob, label: core.string.String }
} }
/** /**

View File

@ -13,20 +13,20 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'
import { type Blob, type Ref } from '@hcengineering/core' import { type Blob, type Ref } from '@hcengineering/core'
import { Label, Dialog, Button, Component } from '@hcengineering/ui' import { Button, Component, Dialog, Label } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import presentation from '../plugin' import presentation from '../plugin'
import { getPreviewType, previewTypes } from '../file' import { getPreviewType, previewTypes } from '../file'
import { BlobMetadata, FilePreviewExtension } from '../types' import { BlobMetadata, FilePreviewExtension } from '../types'
import { getFileUrl } from '../utils' import { getBlobHref, getFileUrl } from '../utils'
import ActionContext from './ActionContext.svelte' import ActionContext from './ActionContext.svelte'
import Download from './icons/Download.svelte' import Download from './icons/Download.svelte'
export let file: Ref<Blob> | undefined export let file: Blob | Ref<Blob> | undefined
export let name: string export let name: string
export let contentType: string export let contentType: string
export let metadata: BlobMetadata | undefined export let metadata: BlobMetadata | undefined
@ -57,9 +57,8 @@
} else { } else {
previewType = undefined previewType = undefined
} }
let download: HTMLAnchorElement let download: HTMLAnchorElement
$: src = file === undefined ? '' : getFileUrl(file, 'full', name) $: src = file === undefined ? '' : typeof file === 'string' ? getFileUrl(file, name) : getBlobHref(file, file._id)
</script> </script>
<ActionContext context={{ mode: 'browser' }} /> <ActionContext context={{ mode: 'browser' }} />

View File

@ -14,14 +14,15 @@
--> -->
<script lang="ts"> <script lang="ts">
// import { Doc } from '@hcengineering/core' // import { Doc } from '@hcengineering/core'
import type { Blob, Ref } from '@hcengineering/core'
import { Button, Dialog, Label, Spinner } from '@hcengineering/ui' import { Button, Dialog, Label, Spinner } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte' import { createEventDispatcher, onMount } from 'svelte'
import presentation from '..' import presentation from '..'
import { getFileUrl } from '../utils' import { getBlobHref, getFileUrl } from '../utils'
import Download from './icons/Download.svelte'
import ActionContext from './ActionContext.svelte' import ActionContext from './ActionContext.svelte'
import Download from './icons/Download.svelte'
export let file: string | undefined export let file: Blob | Ref<Blob> | undefined
export let name: string export let name: string
export let contentType: string | undefined export let contentType: string | undefined
// export let popupOptions: PopupOptions // export let popupOptions: PopupOptions
@ -44,7 +45,8 @@
} }
}) })
let download: HTMLAnchorElement let download: HTMLAnchorElement
$: src = file === undefined ? '' : getFileUrl(file, 'full', name) $: src = file === undefined ? '' : typeof file === 'string' ? getFileUrl(file, name) : getBlobHref(file, file._id)
$: isImage = contentType !== undefined && contentType.startsWith('image/') $: isImage = contentType !== undefined && contentType.startsWith('image/')
let frame: HTMLIFrameElement | undefined = undefined let frame: HTMLIFrameElement | undefined = undefined

View File

@ -13,14 +13,14 @@
// limitations under the License. // limitations under the License.
// //
import { type Blob, type Ref, concatLink } from '@hcengineering/core' import { concatLink, type Blob, type Ref } from '@hcengineering/core'
import { PlatformError, Severity, Status, getMetadata, getResource } from '@hcengineering/platform' import { PlatformError, Severity, Status, getMetadata, getResource } from '@hcengineering/platform'
import { type PopupAlignment } from '@hcengineering/ui' import { type PopupAlignment } from '@hcengineering/ui'
import { writable } from 'svelte/store' import { writable } from 'svelte/store'
import { type BlobMetadata, type FilePreviewExtension } from './types'
import { createQuery } from './utils'
import plugin from './plugin' import plugin from './plugin'
import type { BlobMetadata, FilePreviewExtension } from './types'
import { createQuery } from './utils'
/** /**
* @public * @public

View File

@ -22,6 +22,7 @@ import core, {
type AnyAttribute, type AnyAttribute,
type ArrOf, type ArrOf,
type AttachedDoc, type AttachedDoc,
type BlobLookup,
type Class, type Class,
type Client, type Client,
type Collection, type Collection,
@ -34,23 +35,25 @@ import core, {
type MeasureDoneOperation, type MeasureDoneOperation,
type Mixin, type Mixin,
type Obj, type Obj,
type Blob as PlatformBlob,
type Ref, type Ref,
type RefTo, type RefTo,
type SearchOptions, type SearchOptions,
type SearchQuery, type SearchQuery,
type SearchResult, type SearchResult,
type Space,
type Tx, type Tx,
type TxResult, type TxResult,
type TypeAny, type TypeAny,
type WithLookup, type WithLookup
type Space
} from '@hcengineering/core' } from '@hcengineering/core'
import { getMetadata, getResource } from '@hcengineering/platform' import { getMetadata, getResource } from '@hcengineering/platform'
import { LiveQuery as LQ } from '@hcengineering/query' import { LiveQuery as LQ } from '@hcengineering/query'
import { type AnyComponent, type AnySvelteComponent, type IconSize } from '@hcengineering/ui' import { workspaceId, type AnyComponent, type AnySvelteComponent } from '@hcengineering/ui'
import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view' import view, { type AttributeCategory, type AttributeEditor } from '@hcengineering/view'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { onDestroy } from 'svelte' import { onDestroy } from 'svelte'
import { get } from 'svelte/store'
import { type KeyedAttribute } from '..' import { type KeyedAttribute } from '..'
import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline' import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline'
import plugin from './plugin' import plugin from './plugin'
@ -357,19 +360,117 @@ export function createQuery (dontDestroy?: boolean): LiveQuery {
return new LiveQuery(dontDestroy) return new LiveQuery(dontDestroy)
} }
function getSrcSet (_blob: PlatformBlob, width?: number): string {
let result = ''
const blob = _blob as BlobLookup
if (blob.previewUrl === undefined) {
return ''
}
for (const f of blob.previewFormats ?? []) {
if (result.length > 0) {
result += ', '
}
const fu = blob.previewUrl.replaceAll(':format', f)
if (width !== undefined) {
result +=
fu.replaceAll(':size', `${width}`) +
', ' +
fu.replaceAll(':size', `${width * 2}`) +
', ' +
fu.replaceAll(':size', `${width * 3}`)
} else {
result += fu.replaceAll(':size', `${-1}`)
}
}
return result
}
/** /**
* @public * @public
*/ */
export function getFileUrl (file: string, size: IconSize = 'full', filename?: string): string { export function getFileUrlSrcSet (
file: Ref<PlatformBlob>,
width?: number,
formats: string[] = supportedFormats
): string {
if (file.includes('://')) { if (file.includes('://')) {
return file return file
} }
const uploadUrl = getMetadata(plugin.metadata.UploadURL) const uploadUrl = getMetadata(plugin.metadata.UploadURL)
if (filename !== undefined) { let result = ''
return `${uploadUrl as string}/${filename}?file=${file}&size=${size as string}`
for (const f of formats) {
if (result.length > 0) {
result += ', '
}
if (width !== undefined) {
const fu = `${uploadUrl as string}/${get(workspaceId)}?file=${file}.${f}&size=:size`
result +=
fu.replaceAll(':size', `${width}`) +
', ' +
fu.replaceAll(':size', `${width * 2}`) +
', ' +
fu.replaceAll(':size', `${width * 3}`)
} else {
result += `${uploadUrl as string}/${get(workspaceId)}?file=${file}.${f}`
}
} }
return `${uploadUrl as string}?file=${file}&size=${size as string}` return result
}
export function getBlobHref (_blob: PlatformBlob | undefined, file: Ref<PlatformBlob>, filename?: string): string {
const blob = _blob as BlobLookup
return blob?.downloadUrl ?? getFileUrl(file, filename)
}
export function getBlobSrcSet (_blob: PlatformBlob | undefined, file: Ref<PlatformBlob>, width?: number): string {
return _blob !== undefined ? getSrcSet(_blob, width) : getFileUrlSrcSet(file, width)
}
/**
* @public
*/
export function getFileUrl (file: Ref<PlatformBlob>, filename?: string): string {
if (file.includes('://')) {
return file
}
const uploadUrl = getMetadata(plugin.metadata.UploadURL)
if (filename !== undefined) {
return `${uploadUrl as string}/${get(workspaceId)}/${encodeURIComponent(filename)}?file=${file}`
}
return `${uploadUrl as string}/${get(workspaceId)}?file=${file}`
}
type SupportedFormat = 'jpeg' | 'avif' | 'heif' | 'webp' | 'png'
const supportedFormats: SupportedFormat[] = ['avif', 'webp', 'heif', 'jpeg', 'png']
export function sizeToWidth (size: string): number | undefined {
let width: number | undefined
switch (size) {
case 'inline':
case 'tiny':
case 'card':
case 'x-small':
case 'smaller':
case 'small':
width = 32
break
case 'medium':
width = 64
break
case 'large':
width = 256
break
case 'x-large':
width = 512
break
case '2x-large':
width = 1024
break
}
return width
} }
/** /**
@ -561,3 +662,14 @@ export function isAdminUser (): boolean {
export function isSpace (space: Doc): space is Space { export function isSpace (space: Doc): space is Space {
return getClient().getHierarchy().isDerived(space._class, core.class.Space) return getClient().getHierarchy().isDerived(space._class, core.class.Space)
} }
export function setPresentationCookie (token: string, workspaceId: string): void {
function setToken (path: string): void {
document.cookie =
encodeURIComponent(plugin.metadata.Token.replaceAll(':', '-')) +
'=' +
encodeURIComponent(token) +
`; path=${path}`
}
setToken('/files/' + workspaceId)
}

View File

@ -13,10 +13,19 @@
// limitations under the License. // limitations under the License.
// //
import { type Blob, type MeasureContext, type WorkspaceId } from '@hcengineering/core' import {
type Blob,
type DocumentUpdate,
type MeasureContext,
type Ref,
type WorkspaceId,
type WorkspaceIdWithUrl
} from '@hcengineering/core'
import type { BlobLookup } from '@hcengineering/core/src/classes'
import { type Readable } from 'stream' import { type Readable } from 'stream'
export type ListBlobResult = Omit<Blob, 'contentType' | 'version'> export type ListBlobResult = Omit<Blob, 'contentType' | 'version'>
export interface UploadedObjectInfo { export interface UploadedObjectInfo {
etag: string etag: string
versionId: string | null versionId: string | null
@ -27,6 +36,11 @@ export interface BlobStorageIterator {
close: () => Promise<void> close: () => Promise<void>
} }
export interface BlobLookupResult {
lookups: BlobLookup[]
updates?: Map<Ref<Blob>, DocumentUpdate<BlobLookup>>
}
export interface StorageAdapter { export interface StorageAdapter {
initialize: (ctx: MeasureContext, workspaceId: WorkspaceId) => Promise<void> initialize: (ctx: MeasureContext, workspaceId: WorkspaceId) => Promise<void>
@ -56,6 +70,9 @@ export interface StorageAdapter {
offset: number, offset: number,
length?: number length?: number
) => Promise<Readable> ) => Promise<Readable>
// Lookup will extend Blob with lookup information.
lookup: (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]) => Promise<BlobLookupResult>
} }
export interface StorageAdapterEx extends StorageAdapter { export interface StorageAdapterEx extends StorageAdapter {
@ -133,6 +150,10 @@ export class DummyStorageAdapter implements StorageAdapter, StorageAdapterEx {
): Promise<UploadedObjectInfo> { ): Promise<UploadedObjectInfo> {
throw new Error('not implemented') throw new Error('not implemented')
} }
async lookup (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]): Promise<BlobLookupResult> {
return { lookups: [] }
}
} }
export function createDummyStorageAdapter (): StorageAdapter { export function createDummyStorageAdapter (): StorageAdapter {

View File

@ -15,23 +15,23 @@
// //
--> -->
<script lang="ts"> <script lang="ts">
import { type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
import { type DocumentId, type PlatformDocumentId } from '@hcengineering/collaborator-client' import { type DocumentId, type PlatformDocumentId } from '@hcengineering/collaborator-client'
import { type Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
import { IntlString, getMetadata, translate } from '@hcengineering/platform' import { IntlString, getMetadata, translate } from '@hcengineering/platform'
import { markupToJSON } from '@hcengineering/text'
import presentation, { getFileUrl, getImageSize } from '@hcengineering/presentation' import presentation, { getFileUrl, getImageSize } from '@hcengineering/presentation'
import view from '@hcengineering/view' import { markupToJSON } from '@hcengineering/text'
import { import {
AnySvelteComponent, AnySvelteComponent,
Button, Button,
IconSize, IconSize,
Loading, Loading,
PopupAlignment, PopupAlignment,
ThrottledCaller,
getEventPositionElement, getEventPositionElement,
getPopupPositionElement, getPopupPositionElement,
ThrottledCaller,
themeStore themeStore
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core' import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
import Collaboration, { isChangeOrigin } from '@tiptap/extension-collaboration' import Collaboration, { isChangeOrigin } from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor' import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
@ -39,8 +39,8 @@
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte' import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
import { Doc as YDoc } from 'yjs' import { Doc as YDoc } from 'yjs'
import { deleteAttachment } from '../command/deleteAttachment'
import { Completion } from '../Completion' import { Completion } from '../Completion'
import { deleteAttachment } from '../command/deleteAttachment'
import { textEditorCommandHandler } from '../commands' import { textEditorCommandHandler } from '../commands'
import { EditorKit } from '../kits/editor-kit' import { EditorKit } from '../kits/editor-kit'
import textEditorPlugin from '../plugin' import textEditorPlugin from '../plugin'
@ -64,13 +64,13 @@
import { noSelectionRender, renderCursor } from './editor/collaboration' import { noSelectionRender, renderCursor } from './editor/collaboration'
import { defaultEditorAttributes } from './editor/editorProps' import { defaultEditorAttributes } from './editor/editorProps'
import { EmojiExtension } from './extension/emoji' import { EmojiExtension } from './extension/emoji'
import { ImageUploadExtension } from './extension/imageUploadExt'
import { type FileAttachFunction } from './extension/types'
import { FileUploadExtension } from './extension/fileUploadExt' import { FileUploadExtension } from './extension/fileUploadExt'
import { LeftMenuExtension } from './extension/leftMenu' import { ImageUploadExtension } from './extension/imageUploadExt'
import { InlineCommandsExtension } from './extension/inlineCommands' import { InlineCommandsExtension } from './extension/inlineCommands'
import { InlinePopupExtension } from './extension/inlinePopup' import { InlinePopupExtension } from './extension/inlinePopup'
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar' import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
import { LeftMenuExtension } from './extension/leftMenu'
import { type FileAttachFunction } from './extension/types'
import { completionConfig, inlineCommandsConfig } from './extensions' import { completionConfig, inlineCommandsConfig } from './extensions'
export let collaborativeDoc: CollaborativeDoc export let collaborativeDoc: CollaborativeDoc
@ -288,7 +288,7 @@
optionalExtensions.push( optionalExtensions.push(
ImageUploadExtension.configure({ ImageUploadExtension.configure({
attachFile, attachFile,
uploadUrl: getMetadata(presentation.metadata.UploadURL) getFileUrl
}) })
) )
} }
@ -339,10 +339,7 @@
return return
} }
const size = await getImageSize( const size = await getImageSize(file, getFileUrl(attached.file))
file,
getFileUrl(attached.file, 'full', getMetadata(presentation.metadata.UploadURL))
)
editor.commands.insertContent( editor.commands.insertContent(
{ {

View File

@ -54,7 +54,7 @@
function openOriginalImage (): void { function openOriginalImage (): void {
const attributes = textEditor.getAttributes('image') const attributes = textEditor.getAttributes('image')
const fileId = attributes['file-id'] ?? attributes.src const fileId = attributes['file-id'] ?? attributes.src
const url = getFileUrl(fileId, 'full') const url = getFileUrl(fileId)
window.open(url, '_blank') window.open(url, '_blank')
} }

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Markup } from '@hcengineering/core' import { Markup } from '@hcengineering/core'
import { IntlString, getMetadata } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import presentation, { MessageViewer, getFileUrl, getImageSize } from '@hcengineering/presentation' import presentation, { MessageViewer, getFileUrl, getImageSize } from '@hcengineering/presentation'
import { EmptyMarkup } from '@hcengineering/text' import { EmptyMarkup } from '@hcengineering/text'
import { import {
@ -17,21 +17,21 @@
registerFocus, registerFocus,
resizeObserver resizeObserver
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import type { AnyExtension } from '@tiptap/core' import type { AnyExtension } from '@tiptap/core'
import { createEventDispatcher } from 'svelte'
import { Completion } from '../Completion' import { Completion } from '../Completion'
import textEditorPlugin from '../plugin' import textEditorPlugin from '../plugin'
import StyledTextEditor from './StyledTextEditor.svelte' import StyledTextEditor from './StyledTextEditor.svelte'
import { completionConfig, inlineCommandsConfig } from './extensions' import { RefAction } from '../types'
import { addTableHandler } from '../utils'
import { EmojiExtension } from './extension/emoji' import { EmojiExtension } from './extension/emoji'
import { FocusExtension } from './extension/focus' import { FocusExtension } from './extension/focus'
import { ImageUploadExtension } from './extension/imageUploadExt' import { ImageUploadExtension } from './extension/imageUploadExt'
import { InlineCommandsExtension } from './extension/inlineCommands' import { InlineCommandsExtension } from './extension/inlineCommands'
import { type FileAttachFunction } from './extension/types' import { type FileAttachFunction } from './extension/types'
import { RefAction } from '../types' import { completionConfig, inlineCommandsConfig } from './extensions'
import { addTableHandler } from '../utils'
export let label: IntlString | undefined = undefined export let label: IntlString | undefined = undefined
export let content: Markup export let content: Markup
@ -177,7 +177,7 @@
function configureExtensions (): AnyExtension[] { function configureExtensions (): AnyExtension[] {
const imageUploadPlugin = ImageUploadExtension.configure({ const imageUploadPlugin = ImageUploadExtension.configure({
attachFile, attachFile,
uploadUrl: getMetadata(presentation.metadata.UploadURL) getFileUrl
}) })
const completionPlugin = Completion.configure({ const completionPlugin = Completion.configure({
@ -251,10 +251,7 @@
return return
} }
const size = await getImageSize( const size = await getImageSize(file, getFileUrl(attached.file))
file,
getFileUrl(attached.file, 'full', getMetadata(presentation.metadata.UploadURL))
)
textEditor.editorHandler.insertContent( textEditor.editorHandler.insertContent(
{ {

View File

@ -79,7 +79,7 @@ export const FileExtension = FileNode.extend<FileOptions>({
const fileType = HTMLAttributes['data-file-type'] const fileType = HTMLAttributes['data-file-type']
let href: string = '' let href: string = ''
if (id != null) { if (id != null) {
href = getFileUrl(id, 'full', fileName) href = getFileUrl(id, fileName)
} }
const linkAttributes = { const linkAttributes = {
class: 'file-name', class: 'file-name',

View File

@ -14,7 +14,7 @@
// //
import { FilePreviewPopup } from '@hcengineering/presentation' import { FilePreviewPopup } from '@hcengineering/presentation'
import { ImageNode, type ImageOptions as ImageNodeOptions } from '@hcengineering/text' import { ImageNode, type ImageOptions as ImageNodeOptions } from '@hcengineering/text'
import { type IconSize, getIconSize2x, showPopup } from '@hcengineering/ui' import { showPopup } from '@hcengineering/ui'
import { mergeAttributes, nodeInputRule } from '@tiptap/core' import { mergeAttributes, nodeInputRule } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state' import { Plugin, PluginKey } from '@tiptap/pm/state'
@ -26,9 +26,7 @@ export type ImageAlignment = 'center' | 'left' | 'right'
/** /**
* @public * @public
*/ */
export interface ImageOptions extends ImageNodeOptions { export interface ImageOptions extends ImageNodeOptions {}
uploadUrl: string
}
export interface ImageAlignmentOptions { export interface ImageAlignmentOptions {
align?: ImageAlignment align?: ImageAlignment
@ -63,11 +61,6 @@ declare module '@tiptap/core' {
*/ */
export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/ export const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/
// This is a simplified version of getFileUrl from presentation plugin, which we cannot use
export function getFileUrl (fileId: string, size: IconSize = 'full', uploadUrl: string): string {
return `${uploadUrl}?file=${fileId}&size=${size as string}`
}
/** /**
* @public * @public
*/ */
@ -76,7 +69,8 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
return { return {
inline: true, inline: true,
HTMLAttributes: {}, HTMLAttributes: {},
uploadUrl: '' getFileUrl: () => '',
getFileUrlSrcSet: () => ''
} }
}, },
@ -105,33 +99,32 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
this.options.HTMLAttributes, this.options.HTMLAttributes,
HTMLAttributes HTMLAttributes
) )
const getFileUrl = this.options.getFileUrl
const uploadUrl = this.options.uploadUrl ?? '' const getFileUrlSrcSet = this.options.getFileUrlSrcSet
const id = imgAttributes['file-id'] const id = imgAttributes['file-id']
if (id != null) { if (id != null) {
imgAttributes.src = getFileUrl(id, 'full', uploadUrl) imgAttributes.src = getFileUrl(id)
let width: IconSize | undefined let width: number | undefined
// TODO: Use max width of component may be?
switch (imgAttributes.width) { switch (imgAttributes.width) {
case '32px': case '32px':
width = 'small' width = 32
break break
case '64px': case '64px':
width = 'medium' width = 64
break break
case '128px': case '128px':
width = 128
break
case '256px': case '256px':
width = 'large' width = 256
break break
case '512px': case '512px':
width = 'x-large' width = 512
break break
} }
if (width !== undefined) { imgAttributes.srcset = getFileUrlSrcSet(id, width)
imgAttributes.src = getFileUrl(id, width, uploadUrl)
imgAttributes.srcset =
getFileUrl(id, width, uploadUrl) + ' 1x,' + getFileUrl(id, getIconSize2x(width), uploadUrl) + ' 2x'
}
imgAttributes.class = 'text-editor-image' imgAttributes.class = 'text-editor-image'
imgAttributes.contentEditable = false imgAttributes.contentEditable = false
} }

View File

@ -12,21 +12,21 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getImageSize } from '@hcengineering/presentation' import { getImageSize } from '@hcengineering/presentation'
import { Extension } from '@tiptap/core' import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from '@tiptap/pm/state' import { Plugin, PluginKey } from '@tiptap/pm/state'
import { type EditorView } from '@tiptap/pm/view' import { type EditorView } from '@tiptap/pm/view'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getFileUrl } from './imageExt'
import { type FileAttachFunction } from './types' import { type FileAttachFunction } from './types'
import type { Blob, Ref } from '@hcengineering/core'
/** /**
* @public * @public
*/ */
export interface ImageUploadOptions { export interface ImageUploadOptions {
attachFile?: FileAttachFunction attachFile?: FileAttachFunction
uploadUrl: string getFileUrl: (fileId: Ref<Blob>) => string
} }
function getType (type: string): 'image' | 'other' { function getType (type: string): 'image' | 'other' {
@ -43,13 +43,13 @@ function getType (type: string): 'image' | 'other' {
export const ImageUploadExtension = Extension.create<ImageUploadOptions>({ export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
addOptions () { addOptions () {
return { return {
uploadUrl: '' getFileUrl: () => ''
} }
}, },
addProseMirrorPlugins () { addProseMirrorPlugins () {
const attachFile = this.options.attachFile const attachFile = this.options.attachFile
const uploadUrl = this.options.uploadUrl const getFileUrl = this.options.getFileUrl
function handleDrop ( function handleDrop (
view: EditorView, view: EditorView,
@ -61,9 +61,6 @@ export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
for (const uri of uris) { for (const uri of uris) {
if (uri !== '') { if (uri !== '') {
const url = new URL(uri) const url = new URL(uri)
if (uploadUrl === undefined || !url.href.includes(uploadUrl)) {
continue
}
const _file = (url.searchParams.get('file') ?? '').split('/').join('') const _file = (url.searchParams.get('file') ?? '').split('/').join('')
@ -77,7 +74,7 @@ export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
if (type === 'image') { if (type === 'image') {
const node = view.state.schema.nodes.image.create({ const node = view.state.schema.nodes.image.create({
'file-id': _file, 'file-id': _file,
src: getFileUrl(_file, 'full', uploadUrl) src: getFileUrl(_file as Ref<Blob>)
}) })
const transaction = view.state.tr.insert(pos?.pos ?? 0, node) const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
view.dispatch(transaction) view.dispatch(transaction)
@ -95,7 +92,7 @@ export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
const file = files.item(i) const file = files.item(i)
if (file != null && file.type.startsWith('image/')) { if (file != null && file.type.startsWith('image/')) {
result = true result = true
void handleImageUpload(file, view, pos, attachFile, uploadUrl) void handleImageUpload(file, view, pos, attachFile, getFileUrl)
} }
} }
} }
@ -139,7 +136,7 @@ async function handleImageUpload (
view: EditorView, view: EditorView,
pos: { pos: number, inside: number } | null, pos: { pos: number, inside: number } | null,
attachFile: FileAttachFunction, attachFile: FileAttachFunction,
uploadUrl: string getFileUrl: (fileId: Ref<Blob>) => string
): Promise<void> { ): Promise<void> {
const attached = await attachFile(file) const attached = await attachFile(file)
@ -152,7 +149,7 @@ async function handleImageUpload (
} }
try { try {
const url = getFileUrl(attached.file, 'full', uploadUrl) const url = getFileUrl(attached.file)
const size = await getImageSize(file, url) const size = await getImageSize(file, url)
const node = view.state.schema.nodes.image.create({ const node = view.state.schema.nodes.image.create({
'file-id': attached.file, 'file-id': attached.file,

View File

@ -13,7 +13,9 @@
// limitations under the License. // limitations under the License.
// //
import type { Blob, Ref } from '@hcengineering/core'
/** /**
* @public * @public
*/ */
export type FileAttachFunction = (file: File) => Promise<{ file: string, type: string } | undefined> export type FileAttachFunction = (file: File) => Promise<{ file: Ref<Blob>, type: string } | undefined>

View File

@ -22,14 +22,13 @@ import TaskList from '@tiptap/extension-task-list'
import { DefaultKit, type DefaultKitOptions } from './default-kit' import { DefaultKit, type DefaultKitOptions } from './default-kit'
import { getMetadata } from '@hcengineering/platform' import { getFileUrl, getFileUrlSrcSet } from '@hcengineering/presentation'
import presentation from '@hcengineering/presentation'
import { CodeBlockExtension, codeBlockOptions } from '@hcengineering/text' import { CodeBlockExtension, codeBlockOptions } from '@hcengineering/text'
import { CodemarkExtension } from '../components/extension/codemark' import { CodemarkExtension } from '../components/extension/codemark'
import { FileExtension, type FileOptions } from '../components/extension/fileExt'
import { ImageExtension, type ImageOptions } from '../components/extension/imageExt'
import { NodeUuidExtension } from '../components/extension/nodeUuid' import { NodeUuidExtension } from '../components/extension/nodeUuid'
import { Table, TableCell, TableRow } from '../components/extension/table' import { Table, TableCell, TableRow } from '../components/extension/table'
import { type ImageOptions, ImageExtension } from '../components/extension/imageExt'
import { type FileOptions, FileExtension } from '../components/extension/fileExt'
const headingLevels: Level[] = [1, 2, 3] const headingLevels: Level[] = [1, 2, 3]
@ -105,7 +104,8 @@ export const EditorKit = Extension.create<EditorKitOptions>({
? [ ? [
ImageExtension.configure({ ImageExtension.configure({
inline: true, inline: true,
uploadUrl: getMetadata(presentation.metadata.UploadURL) ?? '', getFileUrl,
getFileUrlSrcSet,
...this.options.image ...this.options.image
}) })
] ]

View File

@ -18,6 +18,8 @@ import { collaborativeDocParse, concatLink } from '@hcengineering/core'
import { ObservableV2 as Observable } from 'lib0/observable' import { ObservableV2 as Observable } from 'lib0/observable'
import { type Doc as YDoc, applyUpdate } from 'yjs' import { type Doc as YDoc, applyUpdate } from 'yjs'
import { type DocumentId, parseDocumentId } from '@hcengineering/collaborator-client' import { type DocumentId, parseDocumentId } from '@hcengineering/collaborator-client'
import { workspaceId } from '@hcengineering/ui'
import { get } from 'svelte/store'
interface EVENTS { interface EVENTS {
synced: (...args: any[]) => void synced: (...args: any[]) => void
@ -29,7 +31,7 @@ async function fetchContent (doc: YDoc, name: string): Promise<void> {
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
try { try {
const res = await fetch(concatLink(frontUrl, `/files?file=${name}`)) const res = await fetch(concatLink(frontUrl, `/files/${get(workspaceId)}?file=${name}`))
if (res.ok) { if (res.ok) {
const blob = await res.blob() const blob = await res.blob()

View File

@ -14,6 +14,7 @@
// //
import { Node, mergeAttributes } from '@tiptap/core' import { Node, mergeAttributes } from '@tiptap/core'
import { getDataAttribute } from './utils' import { getDataAttribute } from './utils'
import type { Ref, Blob } from '@hcengineering/core'
/** /**
* @public * @public
@ -21,12 +22,8 @@ import { getDataAttribute } from './utils'
export interface ImageOptions { export interface ImageOptions {
inline: boolean inline: boolean
HTMLAttributes: Record<string, any> HTMLAttributes: Record<string, any>
uploadUrl?: string getFileUrl: (fileId: Ref<Blob>, filename?: string) => string
} getFileUrlSrcSet: (fileId: Ref<Blob>, size?: number) => string
// This is a simplified version of getFileUrl from presentation plugin, which we cannot use
function getFileUrl (uploadUrl: string, fileId: string, size: string = 'full'): string {
return `${uploadUrl}?file=${fileId}&size=${size}`
} }
/** /**
@ -39,7 +36,8 @@ export const ImageNode = Node.create<ImageOptions>({
return { return {
inline: true, inline: true,
HTMLAttributes: {}, HTMLAttributes: {},
uploadUrl: '' getFileUrl: () => '',
getFileUrlSrcSet: () => ''
} }
}, },
@ -107,8 +105,8 @@ export const ImageNode = Node.create<ImageOptions>({
const fileId = imgAttributes['file-id'] const fileId = imgAttributes['file-id']
if (fileId != null) { if (fileId != null) {
const uploadUrl = this.options.uploadUrl ?? '' imgAttributes.src = this.options.getFileUrl(fileId)
imgAttributes.src = getFileUrl(uploadUrl, fileId) imgAttributes.srcset = this.options.getFileUrlSrcSet(fileId)
} }
return ['div', divAttributes, ['img', imgAttributes]] return ['div', divAttributes, ['img', imgAttributes]]

View File

@ -23,7 +23,7 @@
personByIdStore, personByIdStore,
SystemAvatar SystemAvatar
} from '@hcengineering/contact-resources' } from '@hcengineering/contact-resources'
import core, { Account, Doc, Ref, Timestamp } from '@hcengineering/core' import core, { Account, Doc, Ref, Timestamp, type WithLookup } from '@hcengineering/core'
import { Icon, Label, resizeObserver, TimeSince, tooltip } from '@hcengineering/ui' import { Icon, Label, resizeObserver, TimeSince, tooltip } from '@hcengineering/ui'
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform' import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
import activity, { ActivityMessage, ActivityMessagePreviewType } from '@hcengineering/activity' import activity, { ActivityMessage, ActivityMessagePreviewType } from '@hcengineering/activity'
@ -47,7 +47,7 @@
const limit = 300 const limit = 300
let isActionsOpened = false let isActionsOpened = false
let person: Person | undefined = undefined let person: WithLookup<Person> | undefined = undefined
let width: number let width: number
@ -59,7 +59,7 @@
_id: Ref<Account> | undefined, _id: Ref<Account> | undefined,
accountById: Map<Ref<PersonAccount>, PersonAccount>, accountById: Map<Ref<PersonAccount>, PersonAccount>,
personById: Map<Ref<Person>, Person> personById: Map<Ref<Person>, Person>
): Person | undefined { ): WithLookup<Person> | undefined {
if (_id === undefined) { if (_id === undefined) {
return undefined return undefined
} }
@ -116,7 +116,7 @@
{#if headerObject} {#if headerObject}
<Icon icon={headerIcon ?? classIcon(client, headerObject._class) ?? activity.icon.Activity} size="small" /> <Icon icon={headerIcon ?? classIcon(client, headerObject._class) ?? activity.icon.Activity} size="small" />
{:else if person} {:else if person}
<Avatar size="card" avatar={person.avatar} name={person.name} /> <Avatar size="card" {person} name={person.name} />
{:else} {:else}
<SystemAvatar size="card" /> <SystemAvatar size="card" />
{/if} {/if}

View File

@ -96,7 +96,7 @@
<div class="flex-row-center"> <div class="flex-row-center">
<div class="avatars"> <div class="avatars">
{#each displayPersons as person} {#each displayPersons as person}
<Avatar size="x-small" avatar={person.avatar} name={person.name} /> <Avatar size="x-small" {person} name={person.name} />
{/each} {/each}
</div> </div>

View File

@ -80,7 +80,7 @@
{#if value.icon} {#if value.icon}
<SystemAvatar size="medium" icon={value.icon} iconProps={value.iconProps} /> <SystemAvatar size="medium" icon={value.icon} iconProps={value.iconProps} />
{:else if person} {:else if person}
<Avatar size="medium" avatar={person.avatar} name={person.name} /> <Avatar size="medium" {person} name={person.name} />
{:else} {:else}
<SystemAvatar size="medium" /> <SystemAvatar size="medium" />
{/if} {/if}

View File

@ -144,7 +144,7 @@
{#if $$slots.icon} {#if $$slots.icon}
<slot name="icon" /> <slot name="icon" />
{:else if person} {:else if person}
<Avatar size="medium" avatar={person.avatar} name={person.name} /> <Avatar size="medium" {person} name={person.name} />
{:else} {:else}
<SystemAvatar size="medium" /> <SystemAvatar size="medium" />
{/if} {/if}

View File

@ -14,21 +14,31 @@
--> -->
<script lang="ts"> <script lang="ts">
import { type Attachment } from '@hcengineering/attachment' import { type Attachment } from '@hcengineering/attachment'
import { getResource } from '@hcengineering/platform' import { getResource, getEmbeddedLabel } from '@hcengineering/platform'
import { import {
FilePreviewPopup, FilePreviewPopup,
getFileUrl,
previewTypes, previewTypes,
canPreviewFile, canPreviewFile,
getPreviewAlignment getPreviewAlignment,
getBlobHref
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import { Action as UIAction, ActionIcon, IconMoreH, IconOpen, Menu, closeTooltip, showPopup } from '@hcengineering/ui' import {
Action as UIAction,
ActionIcon,
IconMoreH,
IconOpen,
Menu,
closeTooltip,
showPopup,
tooltip
} from '@hcengineering/ui'
import view, { Action } from '@hcengineering/view' import view, { Action } from '@hcengineering/view'
import attachmentPlugin from '../plugin' import attachmentPlugin from '../plugin'
import FileDownload from './icons/FileDownload.svelte' import FileDownload from './icons/FileDownload.svelte'
import type { WithLookup } from '@hcengineering/core'
export let attachment: Attachment export let attachment: WithLookup<Attachment>
export let isSaved = false export let isSaved = false
export let removable = false export let removable = false
@ -61,7 +71,7 @@
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: attachment.file, file: attachment.$lookup?.file ?? attachment.file,
name: attachment.name, name: attachment.name,
contentType: attachment.type ?? '', contentType: attachment.type ?? '',
metadata: attachment.metadata metadata: attachment.metadata
@ -123,9 +133,10 @@
<div class="flex"> <div class="flex">
<a <a
class="mr-1 flex-row-center gap-2 p-1" class="mr-1 flex-row-center gap-2 p-1"
href={getFileUrl(attachment.file, 'full', attachment.name)} href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)}
download={attachment.name} download={attachment.name}
bind:this={download} bind:this={download}
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
on:click|stopPropagation on:click|stopPropagation
> >
{#if canPreview} {#if canPreview}

View File

@ -14,12 +14,12 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Attachment } from '@hcengineering/attachment' import { Attachment } from '@hcengineering/attachment'
import type { Doc, Ref } from '@hcengineering/core' import core, { type Doc, type Ref, type WithLookup } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation' import { createQuery } from '@hcengineering/presentation'
import attachment from '../plugin' import attachment from '../plugin'
import AttachmentList from './AttachmentList.svelte'
import { AttachmentImageSize } from '../types' import { AttachmentImageSize } from '../types'
import AttachmentList from './AttachmentList.svelte'
export let value: Doc & { attachments?: number } export let value: Doc & { attachments?: number }
export let attachments: Attachment[] | undefined = undefined export let attachments: Attachment[] | undefined = undefined
@ -30,7 +30,7 @@
const savedAttachmentsQuery = createQuery() const savedAttachmentsQuery = createQuery()
let savedAttachmentsIds: Ref<Attachment>[] = [] let savedAttachmentsIds: Ref<Attachment>[] = []
let resAttachments: Attachment[] = [] let resAttachments: WithLookup<Attachment>[] = []
$: updateQuery(value, attachments) $: updateQuery(value, attachments)
@ -48,6 +48,11 @@
}, },
(res) => { (res) => {
resAttachments = res resAttachments = res
},
{
lookup: {
file: core.class.Blob
}
} }
) )
} else { } else {

View File

@ -14,12 +14,13 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Attachment } from '@hcengineering/attachment' import type { Attachment } from '@hcengineering/attachment'
import { showPopup, closeTooltip } from '@hcengineering/ui' import type { WithLookup } from '@hcengineering/core'
import { FilePreviewPopup, getFileUrl } from '@hcengineering/presentation' import { FilePreviewPopup, getBlobHref } from '@hcengineering/presentation'
import { getType } from '../utils' import { closeTooltip, showPopup } from '@hcengineering/ui'
import filesize from 'filesize' import filesize from 'filesize'
import { getType } from '../utils'
export let value: Attachment export let value: WithLookup<Attachment>
const maxLength: number = 18 const maxLength: number = 18
const trimFilename = (fname: string): string => const trimFilename = (fname: string): string =>
@ -44,7 +45,7 @@
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: value.file, file: value.$lookup?.file ?? value.file,
name: value.name, name: value.name,
contentType: value.type, contentType: value.type,
metadata: value.metadata metadata: value.metadata
@ -60,7 +61,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="cellImagePreview" on:click={openAttachment}> <div class="cellImagePreview" on:click={openAttachment}>
<img class={'img-fit'} src={getFileUrl(value.file, 'full', value.name)} alt={value.name} /> <img class={'img-fit'} src={getBlobHref(value.$lookup?.file, value.file, value.name)} alt={value.name} />
</div> </div>
{:else} {:else}
<div class="cellMiscPreview"> <div class="cellMiscPreview">
@ -71,7 +72,7 @@
{extensionIconLabel(value.name)} {extensionIconLabel(value.name)}
</div> </div>
{:else} {:else}
<a class="no-line" href={getFileUrl(value.file, 'full', value.name)} download={value.name}> <a class="no-line" href={getBlobHref(value.$lookup?.file, value.file, value.name)} download={value.name}>
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div> <div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a> </a>
{/if} {/if}
@ -86,7 +87,7 @@
{extensionIconLabel(value.name)} {extensionIconLabel(value.name)}
</div> </div>
{:else} {:else}
<a class="no-line" href={getFileUrl(value.file, 'full', value.name)} download={value.name}> <a class="no-line" href={getBlobHref(value.$lookup?.file, value.file, value.name)} download={value.name}>
<div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div> <div class="flex-center extensionIcon">{extensionIconLabel(value.name)}</div>
</a> </a>
{/if} {/if}
@ -99,7 +100,9 @@
</div> </div>
{:else} {:else}
<div class="eCellInfoFilename"> <div class="eCellInfoFilename">
<a href={getFileUrl(value.file, 'full', value.name)} download={value.name}>{trimFilename(value.name)}</a> <a href={getBlobHref(value.$lookup?.file, value.file, value.name)} download={value.name}
>{trimFilename(value.name)}</a
>
</div> </div>
{/if} {/if}
<div class="eCellInfoFilesize">{filesize(value.size)}</div> <div class="eCellInfoFilesize">{filesize(value.size)}</div>

View File

@ -13,13 +13,14 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { getIconSize2x, IconSize } from '@hcengineering/ui'
import { getFileUrl } from '@hcengineering/presentation'
import type { Attachment } from '@hcengineering/attachment' import type { Attachment } from '@hcengineering/attachment'
import { getBlobHref, getBlobSrcSet, getFileUrlSrcSet, sizeToWidth } from '@hcengineering/presentation'
import { IconSize } from '@hcengineering/ui'
import type { WithLookup } from '@hcengineering/core'
import { AttachmentImageSize } from '../types' import { AttachmentImageSize } from '../types'
export let value: Attachment export let value: WithLookup<Attachment>
export let size: AttachmentImageSize = 'auto' export let size: AttachmentImageSize = 'auto'
interface Dimensions { interface Dimensions {
@ -104,15 +105,13 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<img <img
src={getFileUrl(value.file, urlSize)} src={getBlobHref(value.$lookup?.file, value.file, value.name)}
style:object-fit={getObjectFit(dimensions)} style:object-fit={getObjectFit(dimensions)}
width={dimensions.width} width={dimensions.width}
height={dimensions.height} height={dimensions.height}
srcset={`${getFileUrl(value.file, urlSize, value.name)} 1x, ${getFileUrl( srcset={value.$lookup?.file !== undefined
value.file, ? getBlobSrcSet(value.$lookup?.file, value.file, sizeToWidth(urlSize))
getIconSize2x(urlSize), : getFileUrlSrcSet(value.file, sizeToWidth(urlSize))}
value.name
)} 2x`}
alt={value.name} alt={value.name}
/> />

View File

@ -14,13 +14,13 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Attachment } from '@hcengineering/attachment' import { Attachment } from '@hcengineering/attachment'
import { Ref } from '@hcengineering/core' import { Ref, type WithLookup } from '@hcengineering/core'
import { Scroller } from '@hcengineering/ui' import { Scroller } from '@hcengineering/ui'
import AttachmentPreview from './AttachmentPreview.svelte' import AttachmentPreview from './AttachmentPreview.svelte'
import { AttachmentImageSize } from '../types' import { AttachmentImageSize } from '../types'
export let attachments: Attachment[] = [] export let attachments: WithLookup<Attachment>[] = []
export let savedAttachmentsIds: Ref<Attachment>[] = [] export let savedAttachmentsIds: Ref<Attachment>[] = []
export let imageSize: AttachmentImageSize | undefined = undefined export let imageSize: AttachmentImageSize | undefined = undefined
export let videoPreload = true export let videoPreload = true

View File

@ -14,12 +14,13 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { Doc } from '@hcengineering/core'
import { Attachment } from '@hcengineering/attachment' import { Attachment } from '@hcengineering/attachment'
import { createQuery, getClient, getFileMetadata, uploadFile } from '@hcengineering/presentation' import { createQuery, getClient, getFileMetadata, uploadFile } from '@hcengineering/presentation'
import { ActionIcon, IconAdd, Label, Loading } from '@hcengineering/ui' import { ActionIcon, IconAdd, Label, Loading } from '@hcengineering/ui'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import type { Doc, WithLookup } from '@hcengineering/core'
import core from '@hcengineering/core'
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { AttachmentPresenter } from '..' import { AttachmentPresenter } from '..'
import attachment from '../plugin' import attachment from '../plugin'
@ -30,7 +31,7 @@
const client = getClient() const client = getClient()
let docs: Attachment[] = [] let docs: WithLookup<Attachment>[] = []
let progress = false let progress = false
@ -42,6 +43,11 @@
}, },
(res) => { (res) => {
docs = res docs = res
},
{
lookup: {
file: core.class.Blob
}
} }
) )

View File

@ -14,24 +14,26 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import filesize from 'filesize'
import { createEventDispatcher } from 'svelte'
import type { Attachment } from '@hcengineering/attachment' import type { Attachment } from '@hcengineering/attachment'
import core from '@hcengineering/core' import core, { type WithLookup } from '@hcengineering/core'
import { showPopup, closeTooltip, Label, getIconSize2x, Loading } from '@hcengineering/ui'
import presentation, { import presentation, {
FilePreviewPopup, FilePreviewPopup,
canPreviewFile, canPreviewFile,
getFileUrl, getBlobHref,
getBlobSrcSet,
getPreviewAlignment, getPreviewAlignment,
previewTypes previewTypes,
sizeToWidth
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import { Label, closeTooltip, showPopup } from '@hcengineering/ui'
import { permissionsStore } from '@hcengineering/view-resources' import { permissionsStore } from '@hcengineering/view-resources'
import filesize from 'filesize'
import { createEventDispatcher } from 'svelte'
import { getType } from '../utils' import { getType } from '../utils'
import AttachmentName from './AttachmentName.svelte' import AttachmentName from './AttachmentName.svelte'
export let value: Attachment | undefined export let value: WithLookup<Attachment> | undefined
export let removable: boolean = false export let removable: boolean = false
export let showPreview = false export let showPreview = false
export let preview = false export let preview = false
@ -82,7 +84,7 @@
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: value.file, file: value.$lookup?.file ?? value.file,
name: value.name, name: value.name,
contentType: value.type, contentType: value.type,
metadata: value.metadata metadata: value.metadata
@ -100,24 +102,6 @@
let download: HTMLAnchorElement let download: HTMLAnchorElement
$: imgStyle = getImageStyle(value)
function getImageStyle (value?: Attachment): string {
if (value === undefined) return ''
return isImage(value.type)
? `background-image: url(${getFileUrl(value.file, 'large')});
background-image: -webkit-image-set(
${getFileUrl(value.file, 'large')} 1x,
${getFileUrl(value.file, getIconSize2x('large'))} 2x
);
background-image: image-set(
${getFileUrl(value.file, 'large')} 1x,
${getFileUrl(value.file, getIconSize2x('large'))} 2x
);`
: ''
}
function dragStart (event: DragEvent): void { function dragStart (event: DragEvent): void {
if (value === undefined) return if (value === undefined) return
event.dataTransfer?.setData('application/contentType', value.type) event.dataTransfer?.setData('application/contentType', value.type)
@ -132,25 +116,21 @@
<a <a
class="no-line" class="no-line"
style:flex-shrink={0} style:flex-shrink={0}
href={getFileUrl(value.file, 'full', value.name)} href={getBlobHref(value.$lookup?.file, value.file, value.name)}
download={value.name} download={value.name}
on:click={clickHandler} on:click={clickHandler}
on:mousedown={middleClickHandler} on:mousedown={middleClickHandler}
on:dragstart={dragStart} on:dragstart={dragStart}
> >
{#if showPreview} {#if showPreview && isImage(value.type)}
<div <img
src={getBlobHref(value.$lookup?.file, value.file)}
srcset={getBlobSrcSet(value.$lookup?.file, value.file, sizeToWidth('large'))}
class="flex-center icon" class="flex-center icon"
class:svg={value.type === 'image/svg+xml'} class:svg={value.type === 'image/svg+xml'}
class:image={isImage(value.type)} class:image={isImage(value.type)}
style={imgStyle} alt={value.name}
> />
{#if progress}
<div class="flex p-3">
<Loading />
</div>
{:else if !isImage(value.type)}{iconLabel(value.name)}{/if}
</div>
{:else} {:else}
<div class="flex-center icon"> <div class="flex-center icon">
{iconLabel(value.name)} {iconLabel(value.name)}
@ -160,7 +140,7 @@
<div class="flex-col info-container"> <div class="flex-col info-container">
<div class="name"> <div class="name">
<a <a
href={getFileUrl(value.file, 'full', value.name)} href={getBlobHref(value.$lookup?.file, value.file, value.name)}
download={value.name} download={value.name}
on:click={clickHandler} on:click={clickHandler}
on:mousedown={middleClickHandler} on:mousedown={middleClickHandler}
@ -174,7 +154,7 @@
<span></span> <span></span>
<a <a
class="no-line colorInherit" class="no-line colorInherit"
href={getFileUrl(value.file, 'full', value.name)} href={getBlobHref(value.$lookup?.file, value.file, value.name)}
download={value.name} download={value.name}
bind:this={download} bind:this={download}
> >

View File

@ -27,8 +27,9 @@
import { AttachmentImageSize } from '../types' import { AttachmentImageSize } from '../types'
import AttachmentImagePreview from './AttachmentImagePreview.svelte' import AttachmentImagePreview from './AttachmentImagePreview.svelte'
import AttachmentVideoPreview from './AttachmentVideoPreview.svelte' import AttachmentVideoPreview from './AttachmentVideoPreview.svelte'
import type { WithLookup } from '@hcengineering/core'
export let value: Attachment export let value: WithLookup<Attachment>
export let isSaved: boolean = false export let isSaved: boolean = false
export let listProvider: ListSelectionProvider | undefined = undefined export let listProvider: ListSelectionProvider | undefined = undefined
export let imageSize: AttachmentImageSize = 'auto' export let imageSize: AttachmentImageSize = 'auto'
@ -50,7 +51,7 @@
if (listProvider !== undefined) listProvider.updateFocus(value) if (listProvider !== undefined) listProvider.updateFocus(value)
const popupInfo = showPopup( const popupInfo = showPopup(
FilePreviewPopup, FilePreviewPopup,
{ file: value.file, name: value.name, contentType: value.type }, { file: value.$lookup?.file ?? value.file, name: value.name, contentType: value.type },
value.type.startsWith('image/') ? 'centered' : 'float' value.type.startsWith('image/') ? 'centered' : 'float'
) )
dispatch('open', popupInfo.id) dispatch('open', popupInfo.id)

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import contact from '@hcengineering/contact' import contact from '@hcengineering/contact'
import { Account, Doc, Ref, generateId } from '@hcengineering/core' import { Account, Doc, Ref, generateId, type Blob } from '@hcengineering/core'
import { IntlString, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform' import { IntlString, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { KeyedAttribute, createQuery, getClient, uploadFile } from '@hcengineering/presentation' import { KeyedAttribute, createQuery, getClient, uploadFile } from '@hcengineering/presentation'
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources' import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
@ -132,7 +132,7 @@
progress = false progress = false
} }
async function createAttachment (file: File): Promise<{ file: string, type: string } | undefined> { async function createAttachment (file: File): Promise<{ file: Ref<Blob>, type: string } | undefined> {
try { try {
const uuid = await uploadFile(file) const uuid = await uploadFile(file)
const _id: Ref<Attachment> = generateId() const _id: Ref<Attachment> = generateId()

View File

@ -15,7 +15,7 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import { Attachment } from '@hcengineering/attachment' import { Attachment } from '@hcengineering/attachment'
import { Account, Class, Doc, generateId, Markup, Ref, Space, toIdMap } from '@hcengineering/core' import { Account, Class, Doc, generateId, Markup, Ref, Space, toIdMap, type Blob } from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform' import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { import {
createQuery, createQuery,
@ -137,7 +137,7 @@
} }
} }
async function createAttachment (file: File): Promise<{ file: string, type: string } | undefined> { async function createAttachment (file: File): Promise<{ file: Ref<Blob>, type: string } | undefined> {
if (space === undefined || objectId === undefined || _class === undefined) return if (space === undefined || objectId === undefined || _class === undefined) return
try { try {
const uuid = await uploadFile(file) const uuid = await uploadFile(file)

View File

@ -13,12 +13,13 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { getFileUrl } from '@hcengineering/presentation' import { getBlobHref } from '@hcengineering/presentation'
import type { Attachment } from '@hcengineering/attachment' import type { Attachment } from '@hcengineering/attachment'
import AttachmentPresenter from './AttachmentPresenter.svelte' import AttachmentPresenter from './AttachmentPresenter.svelte'
import type { WithLookup } from '@hcengineering/core'
export let value: Attachment export let value: WithLookup<Attachment>
export let preload = true export let preload = true
const maxSizeRem = 20 const maxSizeRem = 20
@ -57,7 +58,7 @@
</script> </script>
<video controls width={dimensions.width} height={dimensions.height} preload={preload ? 'auto' : 'none'}> <video controls width={dimensions.width} height={dimensions.height} preload={preload ? 'auto' : 'none'}>
<source src={getFileUrl(value.file, 'full', value.name)} /> <source src={getBlobHref(value.$lookup?.file, value.file, value.name)} />
<track kind="captions" label={value.name} /> <track kind="captions" label={value.name} />
<div class="container"> <div class="container">
<AttachmentPresenter {value} /> <AttachmentPresenter {value} />

View File

@ -14,13 +14,13 @@
--> -->
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import { Doc, getCurrentAccount } from '@hcengineering/core' import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { getFileUrl, getClient } from '@hcengineering/presentation' import { getBlobHref, getClient } from '@hcengineering/presentation'
import { Icon, IconMoreV, showPopup, Menu } from '@hcengineering/ui' import { Icon, IconMoreV, Menu, showPopup } from '@hcengineering/ui'
import FileDownload from './icons/FileDownload.svelte'
import { AttachmentGalleryPresenter } from '..' import { AttachmentGalleryPresenter } from '..'
import FileDownload from './icons/FileDownload.svelte'
export let attachments: Attachment[] export let attachments: WithLookup<Attachment>[]
let selectedFileNumber: number | undefined let selectedFileNumber: number | undefined
const myAccId = getCurrentAccount()._id const myAccId = getCurrentAccount()._id
const client = getClient() const client = getClient()
@ -57,7 +57,10 @@
<AttachmentGalleryPresenter value={attachment}> <AttachmentGalleryPresenter value={attachment}>
<svelte:fragment slot="rowMenu"> <svelte:fragment slot="rowMenu">
<div class="eAttachmentCellActions" class:fixed={i === selectedFileNumber}> <div class="eAttachmentCellActions" class:fixed={i === selectedFileNumber}>
<a href={getFileUrl(attachment.file, 'full', attachment.name)} download={attachment.name}> <a
href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)}
download={attachment.name}
>
<Icon icon={FileDownload} size={'small'} /> <Icon icon={FileDownload} size={'small'} />
</a> </a>
<div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}> <div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}>

View File

@ -14,13 +14,13 @@
--> -->
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import { Doc, getCurrentAccount } from '@hcengineering/core' import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { getFileUrl, getClient } from '@hcengineering/presentation' import { getFileUrl, getClient, getBlobHref } from '@hcengineering/presentation'
import { Icon, IconMoreV, showPopup, Menu } from '@hcengineering/ui' import { Icon, IconMoreV, showPopup, Menu } from '@hcengineering/ui'
import FileDownload from './icons/FileDownload.svelte' import FileDownload from './icons/FileDownload.svelte'
import { AttachmentPresenter } from '..' import { AttachmentPresenter } from '..'
export let attachments: Attachment[] export let attachments: WithLookup<Attachment>[]
let selectedFileNumber: number | undefined let selectedFileNumber: number | undefined
const myAccId = getCurrentAccount()._id const myAccId = getCurrentAccount()._id
const client = getClient() const client = getClient()
@ -56,7 +56,7 @@
<AttachmentPresenter value={attachment} /> <AttachmentPresenter value={attachment} />
</div> </div>
<div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}> <div class="eAttachmentRowActions" class:fixed={i === selectedFileNumber}>
<a href={getFileUrl(attachment.file, 'full', attachment.name)} download={attachment.name}> <a href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)} download={attachment.name}>
<Icon icon={FileDownload} size={'small'} /> <Icon icon={FileDownload} size={'small'} />
</a> </a>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->

View File

@ -14,19 +14,20 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Attachment } from '@hcengineering/attachment' import type { Attachment } from '@hcengineering/attachment'
import { getFileUrl } from '@hcengineering/presentation' import { getBlobHref, getFileUrl } from '@hcengineering/presentation'
import { CircleButton, Progress } from '@hcengineering/ui' import { CircleButton, Progress } from '@hcengineering/ui'
import Play from './icons/Play.svelte' import Play from './icons/Play.svelte'
import Pause from './icons/Pause.svelte' import Pause from './icons/Pause.svelte'
import type { WithLookup } from '@hcengineering/core'
export let value: Attachment export let value: WithLookup<Attachment>
export let fullSize = false export let fullSize = false
let time = 0 let time = 0
let duration = Number.POSITIVE_INFINITY let duration = Number.POSITIVE_INFINITY
let paused = true let paused = true
function buttonClick () { function buttonClick (): void {
paused = !paused paused = !paused
} }
@ -47,7 +48,7 @@
</div> </div>
</div> </div>
<audio bind:duration bind:currentTime={time} bind:paused> <audio bind:duration bind:currentTime={time} bind:paused>
<source src={getFileUrl(value.file, 'full', value.name)} type={value.type} /> <source src={getBlobHref(value.$lookup?.file, value.file, value.name)} type={value.type} />
</audio> </audio>
<style lang="scss"> <style lang="scss">

View File

@ -0,0 +1,112 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
// import { Doc } from '@hcengineering/core'
import type { Attachment } from '@hcengineering/attachment'
import type { WithLookup } from '@hcengineering/core'
import presentation, { ActionContext, IconDownload, getBlobHref } from '@hcengineering/presentation'
import { Button, Dialog } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import { getType } from '../utils'
import AudioPlayer from './AudioPlayer.svelte'
export let value: WithLookup<Attachment>
export let showIcon = true
export let fullSize = false
const dispatch = createEventDispatcher()
function iconLabel (name: string): string {
const parts = name.split('.')
const ext = parts[parts.length - 1]
return ext.substring(0, 4).toUpperCase()
}
onMount(() => {
if (fullSize) {
dispatch('fullsize')
}
})
let download: HTMLAnchorElement
$: type = getType(value.type)
$: src = getBlobHref(value.$lookup?.file, value.file, value.name)
</script>
<ActionContext context={{ mode: 'browser' }} />
<Dialog
isFullSize
on:fullsize
on:close={() => {
dispatch('close')
}}
>
<svelte:fragment slot="title">
<div class="antiTitle icon-wrapper">
{#if showIcon}
<div class="wrapped-icon">
<div class="flex-center icon">
{iconLabel(value.name)}
</div>
</div>
{/if}
<span class="wrapped-title">{value.name}</span>
</div>
</svelte:fragment>
<svelte:fragment slot="utils">
<a class="no-line" href={src} download={value.name} bind:this={download}>
<Button
icon={IconDownload}
kind={'ghost'}
on:click={() => {
download.click()
}}
showTooltip={{ label: presentation.string.Download }}
/>
</a>
</svelte:fragment>
{#if type === 'video'}
<video controls preload={'auto'}>
<source {src} />
<track kind="captions" label={value.name} />
</video>
{:else if type === 'audio'}
<AudioPlayer {value} fullSize={true} />
{:else}
<iframe class="pdfviewer-content" src={src + '#view=FitH&navpanes=0'} title="" />
{/if}
</Dialog>
<style lang="scss">
.icon {
position: relative;
flex-shrink: 0;
width: 2rem;
height: 2rem;
font-weight: 500;
font-size: 0.625rem;
color: var(--primary-button-color);
background-color: var(--primary-button-default);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0.5rem;
cursor: pointer;
}
video {
max-width: 100%;
max-height: 100%;
border-radius: 0.75rem;
}
</style>

View File

@ -15,10 +15,10 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Photo } from '@hcengineering/attachment' import { Photo } from '@hcengineering/attachment'
import { Class, Doc, Ref, Space } from '@hcengineering/core' import { Class, Doc, Ref, Space, type WithLookup } from '@hcengineering/core'
import { setPlatformStatus, unknownError } from '@hcengineering/platform' import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { FilePreviewPopup, createQuery, getClient, getFileUrl, uploadFile } from '@hcengineering/presentation' import { FilePreviewPopup, createQuery, getBlobHref, getClient, uploadFile } from '@hcengineering/presentation'
import { Button, IconAdd, Label, showPopup, Spinner } from '@hcengineering/ui' import { Button, IconAdd, Label, Spinner, showPopup } from '@hcengineering/ui'
import attachment from '../plugin' import attachment from '../plugin'
import UploadDuo from './icons/UploadDuo.svelte' import UploadDuo from './icons/UploadDuo.svelte'
@ -28,7 +28,7 @@
let inputFile: HTMLInputElement let inputFile: HTMLInputElement
let loading = 0 let loading = 0
let images: Photo[] = [] let images: WithLookup<Photo>[] = []
const client = getClient() const client = getClient()
const query = createQuery() const query = createQuery()
@ -42,12 +42,12 @@
} }
) )
async function create (file: File) { async function create (file: File): Promise<void> {
if (!file.type.startsWith('image/')) return if (!file.type.startsWith('image/')) return
loading++ loading++
try { try {
const uuid = await uploadFile(file) const uuid = await uploadFile(file)
client.addCollection(attachment.class.Photo, space, objectId, _class, 'attachments', { await client.addCollection(attachment.class.Photo, space, objectId, _class, 'attachments', {
name: file.name, name: file.name,
file: uuid, file: uuid,
type: file.type, type: file.type,
@ -55,40 +55,44 @@
lastModified: file.lastModified lastModified: file.lastModified
}) })
} catch (err: any) { } catch (err: any) {
setPlatformStatus(unknownError(err)) await setPlatformStatus(unknownError(err))
} finally { } finally {
loading-- loading--
} }
} }
function fileSelected () { function fileSelected (): void {
const list = inputFile.files const list = inputFile.files
if (list === null || list.length === 0) return if (list === null || list.length === 0) return
for (let index = 0; index < list.length; index++) { for (let index = 0; index < list.length; index++) {
const file = list.item(index) const file = list.item(index)
if (file !== null) create(file) if (file !== null) {
void create(file)
}
} }
inputFile.value = '' inputFile.value = ''
} }
function fileDrop (e: DragEvent) { function fileDrop (e: DragEvent): void {
const list = e.dataTransfer?.files const list = e.dataTransfer?.files
if (list === undefined || list.length === 0) return if (list === undefined || list.length === 0) return
for (let index = 0; index < list.length; index++) { for (let index = 0; index < list.length; index++) {
const file = list.item(index) const file = list.item(index)
if (file !== null) create(file) if (file !== null) {
void create(file)
}
} }
} }
let dragover = false let dragover = false
function click (ev: Event, item?: Photo): void { function click (ev: Event, item?: WithLookup<Photo>): void {
const el: HTMLElement = ev.currentTarget as HTMLElement const el: HTMLElement = ev.currentTarget as HTMLElement
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }) el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' })
if (item !== undefined) { if (item !== undefined) {
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ file: item.file, name: item.name, contentType: item.type }, { file: item.$lookup?.file ?? item.file, name: item.name, contentType: item.type },
item.type.startsWith('image/') ? 'centered' : 'float' item.type.startsWith('image/') ? 'centered' : 'float'
) )
} else { } else {
@ -145,7 +149,7 @@
click(ev, image) click(ev, image)
}} }}
> >
<img src={getFileUrl(image.file, 'full', image.name)} alt={image.name} /> <img src={getBlobHref(image.$lookup?.file, image.file, image.name)} alt={image.name} />
</div> </div>
{/each} {/each}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->

View File

@ -15,9 +15,9 @@
// //
import { type Attachment } from '@hcengineering/attachment' import { type Attachment } from '@hcengineering/attachment'
import { type Class, type Data, type Doc, type Ref, type Space, type TxOperations as Client } from '@hcengineering/core' import { type Class, type TxOperations as Client, type Data, type Doc, type Ref, type Space } from '@hcengineering/core'
import { getFileMetadata, uploadFile } from '@hcengineering/presentation'
import { setPlatformStatus, unknownError } from '@hcengineering/platform' import { setPlatformStatus, unknownError } from '@hcengineering/platform'
import { getFileMetadata, uploadFile } from '@hcengineering/presentation'
import attachment from './plugin' import attachment from './plugin'

View File

@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import type { AttachedDoc, Class, Ref } from '@hcengineering/core' import type { AttachedDoc, Blob, Class, Ref } from '@hcengineering/core'
import type { Asset, Plugin } from '@hcengineering/platform' import type { Asset, Plugin } from '@hcengineering/platform'
import { IntlString, plugin, Resource } from '@hcengineering/platform' import { IntlString, plugin, Resource } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference' import type { Preference } from '@hcengineering/preference'
@ -26,7 +26,7 @@ import { AnyComponent } from '@hcengineering/ui'
*/ */
export interface Attachment extends AttachedDoc { export interface Attachment extends AttachedDoc {
name: string name: string
file: string file: Ref<Blob>
size: number size: number
type: string type: string
lastModified: number lastModified: number
@ -78,7 +78,7 @@ export default plugin(attachmentId, {
SavedAttachments: '' as Ref<Class<SavedAttachments>> SavedAttachments: '' as Ref<Class<SavedAttachments>>
}, },
helper: { helper: {
UploadFile: '' as Resource<(file: File) => Promise<string>>, UploadFile: '' as Resource<(file: File) => Promise<Ref<Blob>>>,
DeleteFile: '' as Resource<(id: string) => Promise<void>> DeleteFile: '' as Resource<(id: string) => Promise<void>>
}, },
string: { string: {

View File

@ -1,5 +1,5 @@
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import contact, { Channel, combineName, Contact, Employee, PersonAccount } from '@hcengineering/contact' import contact, { AvatarType, Channel, combineName, Contact, Employee, PersonAccount } from '@hcengineering/contact'
import core, { import core, {
Account, Account,
AccountRole, AccountRole,
@ -21,7 +21,8 @@ import core, {
Timestamp, Timestamp,
TxOperations, TxOperations,
TxProcessor, TxProcessor,
WithLookup WithLookup,
type Blob as PlatformBlob
} from '@hcengineering/core' } from '@hcengineering/core'
import gmail, { Message } from '@hcengineering/gmail' import gmail, { Message } from '@hcengineering/gmail'
import recruit from '@hcengineering/recruit' import recruit from '@hcengineering/recruit'
@ -242,7 +243,7 @@ export async function syncDocument (
body: data body: data
}) })
if (resp.status === 200) { if (resp.status === 200) {
const uuid = await resp.text() const uuid = (await resp.text()) as Ref<PlatformBlob>
ed.file = uuid ed.file = uuid
ed._id = attachmentId as Ref<Attachment & BitrixSyncDoc> ed._id = attachmentId as Ref<Attachment & BitrixSyncDoc>
@ -812,7 +813,7 @@ async function downloadComments (
attachedToClass: c._class, attachedToClass: c._class,
bitrixId: `attach-${v.id}`, bitrixId: `attach-${v.id}`,
collection: 'attachments', collection: 'attachments',
file: '', file: '' as Ref<PlatformBlob>,
lastModified: Date.now(), lastModified: Date.now(),
modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System, modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System,
modifiedOn: new Date(it.CREATED ?? new Date().toString()).getTime(), modifiedOn: new Date(it.CREATED ?? new Date().toString()).getTime(),
@ -941,7 +942,8 @@ async function synchronizeUsers (
if (accountId === undefined) { if (accountId === undefined) {
const employeeId = await ops.client.createDoc(contact.class.Person, contact.space.Contacts, { const employeeId = await ops.client.createDoc(contact.class.Person, contact.space.Contacts, {
name: combineName(u.NAME, u.LAST_NAME), name: combineName(u.NAME, u.LAST_NAME),
avatar: u.PERSONAL_PHOTO, avatarType: AvatarType.EXTERNAL,
avatarProps: { url: u.PERSONAL_PHOTO },
city: u.PERSONAL_CITY city: u.PERSONAL_CITY
}) })
await ops.client.createMixin(employeeId, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, { await ops.client.createMixin(employeeId, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, {

View File

@ -14,7 +14,8 @@ import core, {
Space, Space,
TxOperations, TxOperations,
WithLookup, WithLookup,
generateId generateId,
type Blob as PlatformBlob
} from '@hcengineering/core' } from '@hcengineering/core'
import { Message } from '@hcengineering/gmail' import { Message } from '@hcengineering/gmail'
import recruit, { Applicant, Candidate, Vacancy } from '@hcengineering/recruit' import recruit, { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
@ -561,7 +562,7 @@ export async function convert (
const attachDoc: Attachment & BitrixSyncDoc = { const attachDoc: Attachment & BitrixSyncDoc = {
_id: generateId(), _id: generateId(),
bitrixId: `${blobRef.id}`, bitrixId: `${blobRef.id}`,
file: '', // Empty since not uploaded yet. file: '' as Ref<PlatformBlob>, // Empty since not uploaded yet.
name: blobRef.id, name: blobRef.id,
size: -1, size: -1,
type: 'application/octet-stream', type: 'application/octet-stream',

View File

@ -45,7 +45,7 @@
on:click={() => onClick(p)} on:click={() => onClick(p)}
> >
<div class="icon"> <div class="icon">
<Avatar size={'x-small'} avatar={p.avatar} name={p.name} /> <Avatar size={'x-small'} person={p} name={p.name} />
</div> </div>
</div> </div>
{/each} {/each}

View File

@ -55,7 +55,7 @@
{#if persons.length === 1} {#if persons.length === 1}
<Avatar <Avatar
avatar={persons[0].avatar} person={persons[0]}
size={avatarSize} size={avatarSize}
name={persons[0].name} name={persons[0].name}
{showStatus} {showStatus}
@ -66,7 +66,7 @@
{#if persons.length > 1 && size === 'medium'} {#if persons.length > 1 && size === 'medium'}
<div class="group"> <div class="group">
{#each persons.slice(0, visiblePersons - 1) as person} {#each persons.slice(0, visiblePersons - 1) as person}
<Avatar avatar={person.avatar} size="tiny" name={person.name} /> <Avatar {person} size="tiny" name={person.name} />
{/each} {/each}
{#if persons.length > visiblePersons} {#if persons.length > visiblePersons}
<div class="rect"> <div class="rect">

View File

@ -17,8 +17,8 @@
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentPresenter, FileDownload } from '@hcengineering/attachment-resources' import { AttachmentPresenter, FileDownload } from '@hcengineering/attachment-resources'
import { ChunterSpace } from '@hcengineering/chunter' import { ChunterSpace } from '@hcengineering/chunter'
import { Doc, SortingOrder, getCurrentAccount } from '@hcengineering/core' import { Doc, SortingOrder, getCurrentAccount, type WithLookup } from '@hcengineering/core'
import { createQuery, getClient, getFileUrl } from '@hcengineering/presentation' import { createQuery, getBlobHref, getClient } from '@hcengineering/presentation'
import { Icon, IconMoreV, Label, Menu, getCurrentResolvedLocation, navigate, showPopup } from '@hcengineering/ui' import { Icon, IconMoreV, Label, Menu, getCurrentResolvedLocation, navigate, showPopup } from '@hcengineering/ui'
export let channel: ChunterSpace | undefined export let channel: ChunterSpace | undefined
@ -26,7 +26,7 @@
const client = getClient() const client = getClient()
const query = createQuery() const query = createQuery()
let visibleAttachments: Attachment[] | undefined let visibleAttachments: WithLookup<Attachment>[] | undefined
let totalAttachments = 0 let totalAttachments = 0
const ATTACHEMNTS_LIMIT = 5 const ATTACHEMNTS_LIMIT = 5
let selectedRowNumber: number | undefined let selectedRowNumber: number | undefined
@ -83,7 +83,10 @@
<AttachmentPresenter value={attachment} /> <AttachmentPresenter value={attachment} />
</div> </div>
<div class="eAttachmentRowActions" class:fixed={i === selectedRowNumber}> <div class="eAttachmentRowActions" class:fixed={i === selectedRowNumber}>
<a href={getFileUrl(attachment.file, 'full', attachment.name)} download={attachment.name}> <a
href={getBlobHref(attachment.$lookup?.file, attachment.file, attachment.name)}
download={attachment.name}
>
<Icon icon={FileDownload} size={'small'} /> <Icon icon={FileDownload} size={'small'} />
</a> </a>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->

View File

@ -13,13 +13,13 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts" context="module"> <script lang="ts" context="module">
import contact, { AvatarProvider } from '@hcengineering/contact' import contact, { AvatarProvider, getAvatarColorForId, type AvatarInfo } from '@hcengineering/contact'
import { Client, Ref } from '@hcengineering/core' import { Ref, type Data, type WithLookup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient, sizeToWidth } from '@hcengineering/presentation'
const providers = new Map<string, AvatarProvider | null>() const providers = new Map<string, AvatarProvider | null>()
async function getProvider (client: Client, providerId: Ref<AvatarProvider>): Promise<AvatarProvider | undefined> { async function getProvider (providerId: Ref<AvatarProvider>): Promise<AvatarProvider | undefined> {
const p = providers.get(providerId) const p = providers.get(providerId)
if (p !== undefined) { if (p !== undefined) {
return p ?? undefined return p ?? undefined
@ -31,7 +31,8 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { AvatarType, getAvatarProviderId, getFirstName, getLastName } from '@hcengineering/contact' import { getAvatarProviderId, getFirstName, getLastName } from '@hcengineering/contact'
import { Account } from '@hcengineering/core'
import { Asset, getMetadata, getResource } from '@hcengineering/platform' import { Asset, getMetadata, getResource } from '@hcengineering/platform'
import { getBlobURL, reduceCalls } from '@hcengineering/presentation' import { getBlobURL, reduceCalls } from '@hcengineering/presentation'
import { import {
@ -44,11 +45,10 @@
themeStore themeStore
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { Account } from '@hcengineering/core'
import AvatarInstance from './AvatarInstance.svelte'
import { loadUsersStatus, statusByUserStore } from '../utils' import { loadUsersStatus, statusByUserStore } from '../utils'
import AvatarInstance from './AvatarInstance.svelte'
export let avatar: string | null | undefined = undefined export let person: Data<WithLookup<AvatarInfo>> | undefined = undefined
export let name: string | null | undefined = undefined export let name: string | null | undefined = undefined
export let direct: Blob | undefined = undefined export let direct: Blob | undefined = undefined
export let size: IconSize export let size: IconSize
@ -63,7 +63,9 @@
avatarInst.pulse() avatarInst.pulse()
} }
let url: string[] | undefined let url: string | undefined
let srcSet: string | undefined
let avatarProvider: AvatarProvider | undefined let avatarProvider: AvatarProvider | undefined
let color: ColorDefinition | undefined = undefined let color: ColorDefinition | undefined = undefined
let element: HTMLElement let element: HTMLElement
@ -86,26 +88,28 @@
const update = reduceCalls(async function ( const update = reduceCalls(async function (
size: IconSize, size: IconSize,
avatar?: string | null, avatar?: Data<WithLookup<AvatarInfo>>,
direct?: Blob, direct?: Blob,
name?: string | null name?: string | null
) { ) {
const width = sizeToWidth(size)
if (direct !== undefined) { if (direct !== undefined) {
const blobURL = await getBlobURL(direct) const blobURL = await getBlobURL(direct)
url = [blobURL] url = blobURL
avatarProvider = undefined avatarProvider = undefined
} else if (avatar) { } else if (avatar != null) {
const avatarProviderId = getAvatarProviderId(avatar) const avatarProviderId = getAvatarProviderId(avatar.avatarType)
avatarProvider = avatarProviderId && (await getProvider(getClient(), avatarProviderId)) avatarProvider = avatarProviderId !== undefined ? await getProvider(avatarProviderId) : undefined
if (!avatarProvider || avatarProvider.type === AvatarType.COLOR) { if (avatarProvider === undefined) {
url = undefined url = undefined
color = getPlatformAvatarColorByName(avatar.split('://')[1], $themeStore.dark) color = getPlatformAvatarColorByName(
} else if (avatarProvider?.type === AvatarType.IMAGE) { avatar.avatarProps?.color ?? getAvatarColorForId(displayName),
url = (await getResource(avatarProvider.getUrl))(avatar, size) $themeStore.dark
)
} else { } else {
const uri = avatar.split('://')[1] ;({ url, srcSet, color } = (await getResource(avatarProvider.getUrl))(avatar, displayName, width))
url = (await getResource(avatarProvider.getUrl))(uri, size) console.log(url, srcSet, color)
} }
} else if (name != null) { } else if (name != null) {
color = getPlatformAvatarColorForTextDef(name, $themeStore.dark) color = getPlatformAvatarColorForTextDef(name, $themeStore.dark)
@ -116,15 +120,13 @@
avatarProvider = undefined avatarProvider = undefined
} }
}) })
$: void update(size, avatar, direct, name) $: void update(size, person, direct, name)
$: srcset = url?.slice(1)?.join(', ')
onMount(() => { onMount(() => {
loadUsersStatus() loadUsersStatus()
}) })
$: userStatus = account ? $statusByUserStore.get(account) : undefined $: userStatus = account !== undefined ? $statusByUserStore.get(account) : undefined
</script> </script>
{#if showStatus && account} {#if showStatus && account}
@ -132,7 +134,7 @@
<AvatarInstance <AvatarInstance
bind:this={avatarInst} bind:this={avatarInst}
{url} {url}
{srcset} srcset={srcSet}
{displayName} {displayName}
{size} {size}
{icon} {icon}
@ -155,7 +157,7 @@
<AvatarInstance <AvatarInstance
bind:this={avatarInst} bind:this={avatarInst}
{url} {url}
{srcset} srcset={srcSet}
{displayName} {displayName}
{size} {size}
{icon} {icon}

View File

@ -17,7 +17,7 @@
import { AnySvelteComponent, ColorDefinition, Icon, IconSize, resizeObserver } from '@hcengineering/ui' import { AnySvelteComponent, ColorDefinition, Icon, IconSize, resizeObserver } from '@hcengineering/ui'
import AvatarIcon from './icons/Avatar.svelte' import AvatarIcon from './icons/Avatar.svelte'
export let url: string[] | undefined export let url: string | undefined
export let srcset: string | undefined export let srcset: string | undefined
export let displayName: string export let displayName: string
export let size: IconSize export let size: IconSize
@ -86,7 +86,7 @@
style:background-color={color && !url ? color.icon : 'var(--theme-button-default)'} style:background-color={color && !url ? color.icon : 'var(--theme-button-default)'}
> >
{#if url} {#if url}
<img class="hulyAvatarSize-{size} ava-image" src={url[0]} {srcset} alt={''} /> <img class="hulyAvatarSize-{size} ava-image" src={url} {srcset} alt={''} />
{:else if displayName && displayName !== ''} {:else if displayName && displayName !== ''}
<div <div
class="ava-text" class="ava-text"

View File

@ -0,0 +1,51 @@
<!--
// Copyright © 2024 Anticrm Platform Contributors.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import contact, { type Contact, type Employee } from '@hcengineering/contact'
import core, { Account, type Ref, type WithLookup } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { AnySvelteComponent, IconSize } from '@hcengineering/ui'
import { employeeByIdStore, personByIdStore } from '../utils'
import Avatar from './Avatar.svelte'
export let _id: Ref<Contact>
export let name: string | null | undefined = undefined
export let size: IconSize
export let icon: Asset | AnySvelteComponent | undefined = undefined
export let variant: 'circle' | 'roundedRect' | 'none' = 'roundedRect'
export let borderColor: number | undefined = undefined
export let standby: boolean = false
export let showStatus: boolean = true
export let account: Ref<Account> | undefined = undefined
$: empValue = $employeeByIdStore.get(_id as Ref<Employee>) ?? $personByIdStore.get(_id)
let _contact: WithLookup<Contact> | undefined
$: if (empValue === undefined) {
void getClient()
.findOne(contact.class.Contact, { _id }, { lookup: { avatar: core.class.Blob } })
.then((c) => {
_contact = c
})
} else {
_contact = $employeeByIdStore.get(_id as Ref<Employee>) ?? $personByIdStore.get(_id)
}
</script>
<Avatar person={_contact} {name} {size} {icon} {variant} {borderColor} {standby} {showStatus} {account} />

View File

@ -39,5 +39,5 @@
</script> </script>
{#if person} {#if person}
<Avatar bind:this={avatar} {size} avatar={person.avatar} name={person.name} borderColor={user.color} standby /> <Avatar bind:this={avatar} {size} {person} name={person.name} borderColor={user.color} standby />
{/if} {/if}

View File

@ -57,7 +57,7 @@
{/if} {/if}
{#each persons as person, i} {#each persons as person, i}
<div class="combine-avatar {size}" data-over={getDataOver(persons.length === i + 1, items)}> <div class="combine-avatar {size}" data-over={getDataOver(persons.length === i + 1, items)}>
<Avatar avatar={person.avatar} {size} name={person.name} showStatus={false} /> <Avatar {person} {size} name={person.name} showStatus={false} />
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Channel, combineName, Employee, PersonAccount } from '@hcengineering/contact' import { AvatarType, Channel, combineName, Employee, PersonAccount } from '@hcengineering/contact'
import core, { AccountRole, AttachedData, Data, generateId, Ref } from '@hcengineering/core' import core, { AccountRole, AttachedData, Data, generateId, Ref } from '@hcengineering/core'
import login from '@hcengineering/login' import login from '@hcengineering/login'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
@ -44,7 +44,8 @@
const person: Data<Employee> = { const person: Data<Employee> = {
name: '', name: '',
city: '', city: '',
active: true active: true,
avatarType: AvatarType.COLOR
} }
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -56,7 +57,10 @@
changeEmail() changeEmail()
const name = combineName(firstName, lastName) const name = combineName(firstName, lastName)
person.name = name person.name = name
person.avatar = await avatarEditor.createAvatar() const info = await avatarEditor.createAvatar()
person.avatar = info.avatar
person.avatarType = info.avatarType
person.avatarProps = info.avatarProps
await client.createDoc(contact.class.Person, contact.space.Contacts, person, id) await client.createDoc(contact.class.Person, contact.space.Contacts, person, id)
await client.createMixin(id, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, { await client.createMixin(id, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, {
@ -179,7 +183,7 @@
</div> </div>
<div class="ml-4"> <div class="ml-4">
<EditableAvatar <EditableAvatar
avatar={person.avatar} {person}
name={combineName(firstName, lastName)} name={combineName(firstName, lastName)}
{email} {email}
size={'large'} size={'large'}

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Channel, combineName, Person, PersonAccount } from '@hcengineering/contact' import { AvatarType, Channel, combineName, Person, PersonAccount } from '@hcengineering/contact'
import core, { AccountRole, AttachedData, Data, generateId, Ref } from '@hcengineering/core' import core, { AccountRole, AttachedData, Data, generateId, Ref } from '@hcengineering/core'
import login from '@hcengineering/login' import login from '@hcengineering/login'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
@ -48,7 +48,8 @@
const name = combineName(firstName, lastName) const name = combineName(firstName, lastName)
const person: Data<Person> = { const person: Data<Person> = {
name, name,
city: '' city: '',
avatarType: AvatarType.COLOR
} }
await client.createDoc(contact.class.Person, contact.space.Contacts, person, id) await client.createDoc(contact.class.Person, contact.space.Contacts, person, id)

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Channel, combineName, findPerson, Person } from '@hcengineering/contact' import { AvatarType, Channel, combineName, findPerson, Person } from '@hcengineering/contact'
import { AttachedData, Data, generateId } from '@hcengineering/core' import { AttachedData, Data, generateId } from '@hcengineering/core'
import { Card, getClient } from '@hcengineering/presentation' import { Card, getClient } from '@hcengineering/presentation'
import { createFocusManager, EditBox, FocusHandler, IconInfo, Label } from '@hcengineering/ui' import { createFocusManager, EditBox, FocusHandler, IconInfo, Label } from '@hcengineering/ui'
@ -42,10 +42,14 @@
async function createPerson () { async function createPerson () {
const person: Data<Person> = { const person: Data<Person> = {
name: combineName(firstName, lastName), name: combineName(firstName, lastName),
city: object.city city: object.city,
avatarType: AvatarType.COLOR
} }
person.avatar = await avatarEditor.createAvatar() const info = await avatarEditor.createAvatar()
person.avatar = info.avatar
person.avatarType = info.avatarType
person.avatarProps = info.avatarProps
const personId = await client.createDoc(contact.class.Person, contact.space.Contacts, person, id) const personId = await client.createDoc(contact.class.Person, contact.space.Contacts, person, id)
@ -115,12 +119,7 @@
</div> </div>
</div> </div>
<div class="ml-4"> <div class="ml-4">
<EditableAvatar <EditableAvatar person={object} name={combineName(firstName, lastName)} size={'large'} bind:this={avatarEditor} />
avatar={object.avatar}
name={combineName(firstName, lastName)}
size={'large'}
bind:this={avatarEditor}
/>
</div> </div>
</div> </div>
<svelte:fragment slot="pool"> <svelte:fragment slot="pool">

View File

@ -92,9 +92,7 @@
await avatarEditor.removeAvatar(object.avatar) await avatarEditor.removeAvatar(object.avatar)
} }
const avatar = await avatarEditor.createAvatar() const avatar = await avatarEditor.createAvatar()
await client.update(object, { await client.diffUpdate(object, avatar)
avatar
})
} }
const manager = createFocusManager() const manager = createFocusManager()
@ -108,7 +106,7 @@
{#key object} {#key object}
{#if editable} {#if editable}
<EditableAvatar <EditableAvatar
avatar={object.avatar} person={object}
{email} {email}
size={'x-large'} size={'x-large'}
name={object.name} name={object.name}
@ -116,7 +114,7 @@
on:done={onAvatarDone} on:done={onAvatarDone}
/> />
{:else} {:else}
<Avatar avatar={object.avatar} size={'x-large'} name={object.name} /> <Avatar person={object} size={'x-large'} name={object.name} />
{/if} {/if}
{/key} {/key}
</div> </div>

View File

@ -70,9 +70,7 @@
await avatarEditor.removeAvatar(object.avatar) await avatarEditor.removeAvatar(object.avatar)
} }
const avatar = await avatarEditor.createAvatar() const avatar = await avatarEditor.createAvatar()
await client.update(object, { await client.diffUpdate(object, avatar)
avatar
})
} }
const manager = createFocusManager() const manager = createFocusManager()
@ -86,7 +84,7 @@
{#key object} {#key object}
<EditableAvatar <EditableAvatar
disabled={readonly} disabled={readonly}
avatar={object.avatar} person={object}
size={'x-large'} size={'x-large'}
name={object.name} name={object.name}
bind:this={avatarEditor} bind:this={avatarEditor}

View File

@ -14,22 +14,16 @@
--> -->
<script lang="ts"> <script lang="ts">
import attachment from '@hcengineering/attachment' import attachment from '@hcengineering/attachment'
import { AvatarType } from '@hcengineering/contact' import { AvatarType, type AvatarInfo } from '@hcengineering/contact'
import { Asset, getResource } from '@hcengineering/platform' import { Asset, getResource } from '@hcengineering/platform'
import { uploadFile } from '@hcengineering/presentation' import { AnySvelteComponent, IconSize, showPopup } from '@hcengineering/ui'
import {
AnySvelteComponent,
IconSize,
getPlatformAvatarColorForTextDef,
showPopup,
themeStore
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import type { Data, Blob as PlatformBlob, Ref, WithLookup } from '@hcengineering/core'
import AvatarComponent from './Avatar.svelte' import AvatarComponent from './Avatar.svelte'
import SelectAvatarPopup from './SelectAvatarPopup.svelte' import SelectAvatarPopup from './SelectAvatarPopup.svelte'
export let avatar: string | null | undefined export let person: Data<WithLookup<AvatarInfo>> | undefined
export let name: string | null | undefined = undefined export let name: string | null | undefined = undefined
export let email: string | undefined = undefined export let email: string | undefined = undefined
export let size: IconSize export let size: IconSize
@ -39,30 +33,24 @@
export let imageOnly: boolean = false export let imageOnly: boolean = false
export let lessCrop: boolean = false export let lessCrop: boolean = false
$: [schema, uri] = avatar?.split('://') || [] $: selectedAvatarType = person?.avatarType ?? AvatarType.COLOR
$: selectedAvatar = person?.avatar
$: selectedAvatarProps = person?.avatarProps
let selectedAvatarType: AvatarType | undefined export async function createAvatar (): Promise<Data<AvatarInfo>> {
let selectedAvatar: string | null | undefined const result: Data<AvatarInfo> = {
$: selectedAvatarType = avatar?.includes('://') avatarType: selectedAvatarType,
? (schema as AvatarType) avatarProps: selectedAvatarProps,
: avatar === undefined avatar: selectedAvatar
? AvatarType.COLOR }
: AvatarType.IMAGE
$: selectedAvatar = selectedAvatarType === AvatarType.IMAGE ? avatar : uri
$: if (selectedAvatar === undefined && selectedAvatarType === AvatarType.COLOR) {
selectedAvatar = getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name
}
export async function createAvatar (): Promise<string | undefined> {
if (selectedAvatarType === AvatarType.IMAGE && direct !== undefined) { if (selectedAvatarType === AvatarType.IMAGE && direct !== undefined) {
const uploadFile = await getResource(attachment.helper.UploadFile) const uploadFile = await getResource(attachment.helper.UploadFile)
const file = new File([direct], 'avatar', { type: direct.type }) const file = new File([direct], 'avatar', { type: direct.type })
return await uploadFile(file) result.avatar = await uploadFile(file)
}
if (selectedAvatarType != null && selectedAvatar) {
return `${selectedAvatarType}://${selectedAvatar}`
} }
return result
} }
export async function removeAvatar (avatar: string) { export async function removeAvatar (avatar: string) {
@ -72,11 +60,16 @@
} }
} }
function handlePopupSubmit (submittedAvatarType?: AvatarType, submittedAvatar?: string, submittedDirect?: Blob) { function handlePopupSubmit (
submittedAvatarType: AvatarType,
submittedAvatar: Ref<PlatformBlob> | undefined | null,
submittedProps: Record<string, any> | undefined,
submittedDirect?: Blob
) {
selectedAvatarType = submittedAvatarType selectedAvatarType = submittedAvatarType
selectedAvatar = submittedAvatar selectedAvatar = submittedAvatar
selectedAvatarProps = submittedProps
direct = submittedDirect direct = submittedDirect
avatar = selectedAvatarType === AvatarType.IMAGE ? selectedAvatar : `${selectedAvatarType}://${selectedAvatar}`
dispatch('done') dispatch('done')
} }
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -84,12 +77,10 @@
async function showSelectionPopup (e: MouseEvent) { async function showSelectionPopup (e: MouseEvent) {
if (!disabled) { if (!disabled) {
showPopup(SelectAvatarPopup, { showPopup(SelectAvatarPopup, {
avatar: avatar: selectedAvatar,
selectedAvatarType === AvatarType.IMAGE selectedAvatarType,
? selectedAvatar selectedAvatarProps,
: selectedAvatarType === AvatarType.COLOR && avatar == null selectedAvatar,
? undefined
: `${selectedAvatarType}://${selectedAvatar}`,
email, email,
name, name,
file: direct, file: direct,
@ -109,11 +100,11 @@
{direct} {direct}
{size} {size}
{icon} {icon}
avatar={selectedAvatarType === AvatarType.IMAGE person={{
? selectedAvatar avatarType: selectedAvatarType,
: selectedAvatarType === AvatarType.COLOR && avatar == null avatarProps: selectedAvatarProps,
? undefined avatar: selectedAvatar
: `${selectedAvatarType}://${selectedAvatar}`} }}
{name} {name}
/> />
</div> </div>

View File

@ -60,7 +60,7 @@
> >
{#if employee} {#if employee}
<div class="flex-col-center pb-2"> <div class="flex-col-center pb-2">
<Avatar size={'x-large'} avatar={employee.avatar} name={employee.name} /> <Avatar size={'x-large'} person={employee} name={employee.name} />
</div> </div>
<div class="pb-2">{getName(client.getHierarchy(), employee)}</div> <div class="pb-2">{getName(client.getHierarchy(), employee)}</div>
<DocNavLink object={employee}> <DocNavLink object={employee}>

View File

@ -107,7 +107,7 @@
if (_update.avatar !== undefined || sourcePerson.avatar === targetPerson.avatar) { if (_update.avatar !== undefined || sourcePerson.avatar === targetPerson.avatar) {
// We replace avatar, we need to update source with target // We replace avatar, we need to update source with target
await client.update(sourcePerson, { await client.update(sourcePerson, {
avatar: sourcePerson.avatar === targetPerson.avatar ? '' : targetPerson.avatar avatar: sourcePerson.avatar === targetPerson.avatar ? null : targetPerson.avatar
}) })
} }
await client.update(targetPerson, _update) await client.update(targetPerson, _update)
@ -360,7 +360,7 @@
selected={update.avatar !== undefined} selected={update.avatar !== undefined}
> >
<svelte:fragment slot="item" let:item> <svelte:fragment slot="item" let:item>
<Avatar avatar={item.avatar} size={'x-large'} icon={contact.icon.Person} name={item.name} /> <Avatar person={item} size={'x-large'} icon={contact.icon.Person} name={item.name} />
</svelte:fragment> </svelte:fragment>
</MergeComparer> </MergeComparer>
<MergeComparer <MergeComparer

View File

@ -41,7 +41,7 @@
<div class="antiContactCard"> <div class="antiContactCard">
<div class="label uppercase"><Label label={contact.string.Organization} /></div> <div class="label uppercase"><Label label={contact.string.Organization} /></div>
<div class="flex-center logo"> <div class="flex-center logo">
<Avatar avatar={organization.avatar} size={'large'} icon={contact.icon.Company} /> <Avatar person={organization} size={'large'} icon={contact.icon.Company} />
</div> </div>
{#if organization} {#if organization}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->

View File

@ -42,7 +42,7 @@
<div class="antiContactCard"> <div class="antiContactCard">
<div class="label uppercase"><Label label={contact.string.Person} /></div> <div class="label uppercase"><Label label={contact.string.Person} /></div>
<div class="flex-center logo"> <div class="flex-center logo">
<Avatar avatar={object.avatar} size={'large'} icon={contact.icon.Company} name={object.name} /> <Avatar person={object} size={'large'} icon={contact.icon.Company} name={object.name} />
</div> </div>
{#if object} {#if object}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->

View File

@ -63,7 +63,7 @@
class:mr-2={shouldShowName && !enlargedText} class:mr-2={shouldShowName && !enlargedText}
class:mr-3={shouldShowName && enlargedText} class:mr-3={shouldShowName && enlargedText}
> >
<Avatar size={avatarSize} avatar={value.avatar} name={value.name} {showStatus} account={account?._id} /> <Avatar size={avatarSize} person={value} name={value.name} {showStatus} account={account?._id} />
</span> </span>
{/if} {/if}
{#if shouldShowName} {#if shouldShowName}

View File

@ -22,5 +22,5 @@
</script> </script>
{#if value} {#if value}
<Avatar avatar={value.avatar} {size} name={value.name} /> <Avatar person={value} {size} name={value.name} />
{/if} {/if}

View File

@ -15,62 +15,56 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { AvatarType, buildGravatarId, checkHasGravatar } from '@hcengineering/contact' import { AvatarType, buildGravatarId, checkHasGravatar, type AvatarInfo } from '@hcengineering/contact'
import type { Ref } from '@hcengineering/core'
import { Blob as PlatformBlob } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform' import { Asset } from '@hcengineering/platform'
import presentation, { Card, getFileUrl } from '@hcengineering/presentation'
import { import {
AnySvelteComponent, AnySvelteComponent,
ColorDefinition,
Label, Label,
showPopup,
TabList, TabList,
eventToHTMLElement, eventToHTMLElement,
getPlatformAvatarColorForTextDef,
getPlatformAvatarColorByName, getPlatformAvatarColorByName,
getPlatformAvatarColorForTextDef,
getPlatformAvatarColors, getPlatformAvatarColors,
ColorDefinition, showPopup,
themeStore themeStore
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { ColorsPopup } from '@hcengineering/view-resources' import { ColorsPopup } from '@hcengineering/view-resources'
import presentation, { Card, getFileUrl } from '@hcengineering/presentation'
import contact from '../plugin' import contact from '../plugin'
import { getAvatarTypeDropdownItems } from '../utils' import { getAvatarTypeDropdownItems } from '../utils'
import AvatarComponent from './Avatar.svelte' import AvatarComponent from './Avatar.svelte'
import EditAvatarPopup from './EditAvatarPopup.svelte' import EditAvatarPopup from './EditAvatarPopup.svelte'
export let avatar: string | null | undefined = undefined export let selectedAvatarType: AvatarType
export let selectedAvatar: AvatarInfo['avatar']
export let selectedAvatarProps: AvatarInfo['avatarProps']
const initialSelectedAvatarType = selectedAvatarType
const initialSelectedAvatar = selectedAvatar
const initialSelectedAvatarProps = { ...selectedAvatarProps }
export let name: string | null | undefined = undefined export let name: string | null | undefined = undefined
export let email: string | undefined export let email: string | undefined
export let file: Blob | undefined export let file: Blob | undefined
export let icon: Asset | AnySvelteComponent | undefined = undefined export let icon: Asset | AnySvelteComponent | undefined = undefined
export let imageOnly: boolean = false export let imageOnly: boolean = false
export let lessCrop: boolean = false export let lessCrop: boolean = false
export let onSubmit: (avatarType?: AvatarType, avatar?: string, file?: Blob) => void export let onSubmit: (
submittedAvatarType: AvatarType,
submittedAvatar: Ref<PlatformBlob> | undefined | null,
submittedProps: Record<string, any> | undefined,
submittedDirect?: Blob
) => void
const [schema, uri] = avatar?.split('://') || []
const colors = getPlatformAvatarColors($themeStore.dark) const colors = getPlatformAvatarColors($themeStore.dark)
let color: ColorDefinition | undefined = let color: ColorDefinition | undefined =
(schema as AvatarType) === AvatarType.COLOR ? getPlatformAvatarColorByName(uri, $themeStore.dark) : undefined (selectedAvatarType as AvatarType) === AvatarType.COLOR
? getPlatformAvatarColorByName(selectedAvatarProps?.color ?? '', $themeStore.dark)
: undefined
const initialSelectedType = (() => {
if (file) {
return AvatarType.IMAGE
}
if (!avatar) {
return AvatarType.COLOR
}
return avatar.includes('://') ? (schema as AvatarType) : AvatarType.IMAGE
})()
const initialSelectedAvatar = (() => {
if (!avatar) {
return getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name
}
return avatar.includes('://') ? uri : avatar
})()
let selectedAvatarType: AvatarType = initialSelectedType
let selectedAvatar: string = initialSelectedAvatar
let selectedFile: Blob | undefined = file let selectedFile: Blob | undefined = file
let hasGravatar = false let hasGravatar = false
@ -82,38 +76,43 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
function submit () { function submit () {
onSubmit(selectedAvatarType, selectedAvatar, selectedAvatarType === AvatarType.IMAGE ? selectedFile : undefined) onSubmit(
selectedAvatarType,
selectedAvatar,
selectedAvatarProps,
selectedAvatarType === AvatarType.IMAGE ? selectedFile : undefined
)
} }
let inputRef: HTMLInputElement let inputRef: HTMLInputElement
const targetMimes = ['image/png', 'image/jpg', 'image/jpeg'] const targetMimes = ['image/png', 'image/jpg', 'image/jpeg']
function handleDropdownSelection (e: any) { function handleDropdownSelection (e: any) {
if (selectedAvatarType === AvatarType.GRAVATAR && email) { if (selectedAvatarType === AvatarType.GRAVATAR && email) {
selectedAvatar = buildGravatarId(email) selectedAvatarProps = { url: buildGravatarId(email) }
} else if (selectedAvatarType === AvatarType.IMAGE) { } else if (selectedAvatarType === AvatarType.IMAGE) {
if (selectedFile) { if (selectedFile) {
return return
} }
if (file) { if (file) {
selectedFile = file selectedFile = file
} else if (avatar && !avatar.includes('://')) {
selectedAvatar = avatar
} else { } else {
selectedAvatar = '' selectedAvatar = undefined
inputRef.click() inputRef.click()
} }
} else { } else {
selectedAvatar = color ? color.name : getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name selectedAvatarProps = {
color: color ? color.name : getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name
}
} }
} }
async function handleImageAvatarClick () { async function handleImageAvatarClick (): Promise<void> {
let editableFile: Blob let editableFile: Blob
if (selectedFile !== undefined) { if (selectedFile !== undefined) {
editableFile = selectedFile editableFile = selectedFile
} else if (selectedAvatar && !(imageOnly && selectedAvatar === initialSelectedAvatar)) { } else if (selectedAvatar && !(imageOnly && selectedAvatar === initialSelectedAvatar)) {
const url = getFileUrl(selectedAvatar, 'full') const url = getFileUrl(selectedAvatar)
editableFile = await (await fetch(url)).blob() editableFile = await (await fetch(url)).blob()
} else { } else {
inputRef.click() inputRef.click()
@ -122,18 +121,21 @@
if (editableFile.size > 0) showCropper(editableFile) if (editableFile.size > 0) showCropper(editableFile)
} }
function showCropper (editableFile: Blob) { function showCropper (editableFile: Blob): void {
showPopup(EditAvatarPopup, { file: editableFile, lessCrop }, undefined, (blob) => { showPopup(EditAvatarPopup, { file: editableFile, lessCrop }, undefined, (blob) => {
if (blob === undefined) { if (blob === undefined) {
if (!selectedFile && (!avatar || avatar.includes('://'))) { if (!selectedFile && !initialSelectedAvatar) {
selectedAvatarType = AvatarType.COLOR selectedAvatarType = AvatarType.COLOR
selectedAvatar = getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name selectedAvatarProps = { color: getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name }
} }
return return
} }
if (blob === null) { if (blob === null) {
selectedAvatarType = AvatarType.COLOR selectedAvatarType = AvatarType.COLOR
selectedAvatar = imageOnly ? '' : getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name selectedAvatar = undefined
selectedAvatarProps = {
color: imageOnly ? '' : getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name
}
selectedFile = undefined selectedFile = undefined
} else { } else {
selectedFile = blob selectedFile = blob
@ -142,7 +144,7 @@
}) })
} }
function onSelectFile (e: any) { function onSelectFile (e: any): void {
const targetFile = e.target?.files[0] as File | undefined const targetFile = e.target?.files[0] as File | undefined
if (targetFile === undefined || !targetMimes.includes(targetFile.type)) { if (targetFile === undefined || !targetMimes.includes(targetFile.type)) {
@ -153,13 +155,14 @@
document.body.onfocus = null document.body.onfocus = null
} }
function handleFileSelectionCancel () { function handleFileSelectionCancel (): void {
document.body.onfocus = null document.body.onfocus = null
if (!inputRef.value.length) { if (!inputRef.value.length) {
if (!selectedFile) { if (!selectedFile) {
selectedAvatarType = AvatarType.COLOR selectedAvatarType = AvatarType.COLOR
selectedAvatar = getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name selectedAvatar = undefined
selectedAvatarProps = { color: getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name }
} }
} }
} }
@ -170,14 +173,14 @@
{ {
colors, colors,
columns: 6, columns: 6,
selected: getPlatformAvatarColorByName(selectedAvatar, $themeStore.dark), selected: getPlatformAvatarColorByName(selectedAvatarProps?.color ?? '', $themeStore.dark),
key: 'icon' key: 'icon'
}, },
eventToHTMLElement(event), eventToHTMLElement(event),
(col) => { (col) => {
if (col != null) { if (col != null) {
color = colors[col] color = colors[col]
selectedAvatar = color.name selectedAvatarProps = { color: color.name }
} }
} }
) )
@ -189,10 +192,10 @@
okLabel={presentation.string.Save} okLabel={presentation.string.Save}
width={'x-small'} width={'x-small'}
accentHeader accentHeader
canSave={selectedAvatarType !== initialSelectedType || canSave={selectedAvatarType !== initialSelectedAvatarType ||
selectedAvatar !== initialSelectedAvatar || selectedAvatar !== initialSelectedAvatar ||
selectedFile !== file || JSON.stringify(initialSelectedAvatarProps) !== JSON.stringify(selectedAvatarProps) ||
!avatar} selectedFile !== file}
okAction={submit} okAction={submit}
on:close={() => { on:close={() => {
dispatch('close') dispatch('close')
@ -214,11 +217,11 @@
}} }}
> >
<AvatarComponent <AvatarComponent
avatar={selectedAvatarType === AvatarType.IMAGE person={{
? selectedAvatar === '' avatarType: selectedAvatarType,
? `${AvatarType.COLOR}://${color?.color}` avatar: selectedAvatar,
: selectedAvatar avatarProps: selectedAvatarProps
: `${selectedAvatarType}://${selectedAvatar}`} }}
direct={selectedAvatarType === AvatarType.IMAGE ? selectedFile : undefined} direct={selectedAvatarType === AvatarType.IMAGE ? selectedFile : undefined}
size={'2x-large'} size={'2x-large'}
{icon} {icon}

View File

@ -37,7 +37,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-row-center" on:click> <div class="flex-row-center" on:click>
<Avatar <Avatar
avatar={person.avatar} {person}
size={avatarSize} size={avatarSize}
name={person.name} name={person.name}
on:accent-color on:accent-color

View File

@ -38,7 +38,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-row-center" on:click> <div class="flex-row-center" on:click>
<Avatar avatar={value.avatar} {size} {icon} name={value.name} on:accent-color {showStatus} account={account?._id} /> <Avatar person={value} {size} {icon} name={value.name} on:accent-color {showStatus} account={account?._id} />
<div class="flex-col min-w-0 {size === 'tiny' || size === 'inline' ? 'ml-1' : 'ml-2'}" class:max-w-20={short}> <div class="flex-col min-w-0 {size === 'tiny' || size === 'inline' ? 'ml-1' : 'ml-2'}" class:max-w-20={short}>
{#if subtitle}<div class="content-dark-color text-sm">{subtitle}</div>{/if} {#if subtitle}<div class="content-dark-color text-sm">{subtitle}</div>{/if}
<div class="label text-left">{getName(client.getHierarchy(), value)}</div> <div class="label text-left">{getName(client.getHierarchy(), value)}</div>

View File

@ -14,33 +14,47 @@
// limitations under the License. // limitations under the License.
// //
import { type Channel, type Contact, getGravatarUrl, getName, type Person } from '@hcengineering/contact' import {
getGravatarUrl,
getName,
type AvatarInfo,
type Channel,
type Contact,
type Person
} from '@hcengineering/contact'
import { import {
type Class, type Class,
type Client, type Client,
type Data,
type DocumentQuery, type DocumentQuery,
type Ref, type Ref,
type RelatedDocument, type RelatedDocument,
type WithLookup type WithLookup
} from '@hcengineering/core' } from '@hcengineering/core'
import login from '@hcengineering/login' import login from '@hcengineering/login'
import { type IntlString, type Resources, getResource } from '@hcengineering/platform' import { getResource, type IntlString, type Resources } from '@hcengineering/platform'
import { MessageBox, type ObjectSearchResult, getClient, getFileUrl } from '@hcengineering/presentation' import { MessageBox, getBlobHref, getBlobSrcSet, getClient, type ObjectSearchResult } from '@hcengineering/presentation'
import { import {
getPlatformAvatarColorByName,
getPlatformAvatarColorForTextDef,
getPlatformColorDef,
hexColorToNumber,
parseURL,
showPopup,
themeStore,
type AnyComponent, type AnyComponent,
type AnySvelteComponent, type AnySvelteComponent,
type IconSize, type ColorDefinition,
type TooltipAlignment, type TooltipAlignment
getIconSize2x,
parseURL,
showPopup
} from '@hcengineering/ui' } from '@hcengineering/ui'
import AccountArrayEditor from './components/AccountArrayEditor.svelte' import AccountArrayEditor from './components/AccountArrayEditor.svelte'
import AccountBox from './components/AccountBox.svelte' import AccountBox from './components/AccountBox.svelte'
import AssigneeBox from './components/AssigneeBox.svelte' import AssigneeBox from './components/AssigneeBox.svelte'
import AssigneePopup from './components/AssigneePopup.svelte' import AssigneePopup from './components/AssigneePopup.svelte'
import Avatar from './components/Avatar.svelte' import Avatar from './components/Avatar.svelte'
import AvatarRef from './components/AvatarRef.svelte'
import ChannelFilter from './components/ChannelFilter.svelte' import ChannelFilter from './components/ChannelFilter.svelte'
import ChannelIcon from './components/ChannelIcon.svelte'
import ChannelPanel from './components/ChannelPanel.svelte' import ChannelPanel from './components/ChannelPanel.svelte'
import ChannelPresenter from './components/ChannelPresenter.svelte' import ChannelPresenter from './components/ChannelPresenter.svelte'
import Channels from './components/Channels.svelte' import Channels from './components/Channels.svelte'
@ -48,6 +62,7 @@ import ChannelsDropdown from './components/ChannelsDropdown.svelte'
import ChannelsEditor from './components/ChannelsEditor.svelte' import ChannelsEditor from './components/ChannelsEditor.svelte'
import ChannelsPresenter from './components/ChannelsPresenter.svelte' import ChannelsPresenter from './components/ChannelsPresenter.svelte'
import ChannelsView from './components/ChannelsView.svelte' import ChannelsView from './components/ChannelsView.svelte'
import CollaborationUserAvatar from './components/CollaborationUserAvatar.svelte'
import CombineAvatars from './components/CombineAvatars.svelte' import CombineAvatars from './components/CombineAvatars.svelte'
import ContactArrayEditor from './components/ContactArrayEditor.svelte' import ContactArrayEditor from './components/ContactArrayEditor.svelte'
import ContactPresenter from './components/ContactPresenter.svelte' import ContactPresenter from './components/ContactPresenter.svelte'
@ -55,18 +70,16 @@ import ContactRefPresenter from './components/ContactRefPresenter.svelte'
import Contacts from './components/Contacts.svelte' import Contacts from './components/Contacts.svelte'
import ContactsTabs from './components/ContactsTabs.svelte' import ContactsTabs from './components/ContactsTabs.svelte'
import CreateEmployee from './components/CreateEmployee.svelte' import CreateEmployee from './components/CreateEmployee.svelte'
import CreateGuest from './components/CreateGuest.svelte'
import CreateOrganization from './components/CreateOrganization.svelte' import CreateOrganization from './components/CreateOrganization.svelte'
import CreatePerson from './components/CreatePerson.svelte' import CreatePerson from './components/CreatePerson.svelte'
import CollaborationUserAvatar from './components/CollaborationUserAvatar.svelte'
import DeleteConfirmationPopup from './components/DeleteConfirmationPopup.svelte' import DeleteConfirmationPopup from './components/DeleteConfirmationPopup.svelte'
import EditEmployee from './components/EditEmployee.svelte' import EditEmployee from './components/EditEmployee.svelte'
import EditMember from './components/EditMember.svelte' import EditMember from './components/EditMember.svelte'
import EditOrganization from './components/EditOrganization.svelte' import EditOrganization from './components/EditOrganization.svelte'
import EditOrganizationPanel from './components/EditOrganizationPanel.svelte'
import EditPerson from './components/EditPerson.svelte' import EditPerson from './components/EditPerson.svelte'
import EditableAvatar from './components/EditableAvatar.svelte' import EditableAvatar from './components/EditableAvatar.svelte'
import PersonAccountFilterValuePresenter from './components/PersonAccountFilterValuePresenter.svelte'
import PersonAccountPresenter from './components/PersonAccountPresenter.svelte'
import PersonAccountRefPresenter from './components/PersonAccountRefPresenter.svelte'
import EmployeeArrayEditor from './components/EmployeeArrayEditor.svelte' import EmployeeArrayEditor from './components/EmployeeArrayEditor.svelte'
import EmployeeBox from './components/EmployeeBox.svelte' import EmployeeBox from './components/EmployeeBox.svelte'
import EmployeeBrowser from './components/EmployeeBrowser.svelte' import EmployeeBrowser from './components/EmployeeBrowser.svelte'
@ -82,33 +95,34 @@ import MembersPresenter from './components/MembersPresenter.svelte'
import MergePersons from './components/MergePersons.svelte' import MergePersons from './components/MergePersons.svelte'
import OrganizationEditor from './components/OrganizationEditor.svelte' import OrganizationEditor from './components/OrganizationEditor.svelte'
import OrganizationPresenter from './components/OrganizationPresenter.svelte' import OrganizationPresenter from './components/OrganizationPresenter.svelte'
import PersonAccountFilterValuePresenter from './components/PersonAccountFilterValuePresenter.svelte'
import PersonAccountPresenter from './components/PersonAccountPresenter.svelte'
import PersonAccountRefPresenter from './components/PersonAccountRefPresenter.svelte'
import PersonEditor from './components/PersonEditor.svelte' import PersonEditor from './components/PersonEditor.svelte'
import PersonIcon from './components/PersonIcon.svelte'
import PersonPresenter from './components/PersonPresenter.svelte' import PersonPresenter from './components/PersonPresenter.svelte'
import PersonRefPresenter from './components/PersonRefPresenter.svelte' import PersonRefPresenter from './components/PersonRefPresenter.svelte'
import SelectAvatars from './components/SelectAvatars.svelte' import SelectAvatars from './components/SelectAvatars.svelte'
import SelectUsersPopup from './components/SelectUsersPopup.svelte'
import SocialEditor from './components/SocialEditor.svelte' import SocialEditor from './components/SocialEditor.svelte'
import SpaceMembers from './components/SpaceMembers.svelte' import SpaceMembers from './components/SpaceMembers.svelte'
import SpaceMembersEditor from './components/SpaceMembersEditor.svelte'
import SystemAvatar from './components/SystemAvatar.svelte'
import UserBox from './components/UserBox.svelte' import UserBox from './components/UserBox.svelte'
import UserBoxItems from './components/UserBoxItems.svelte' import UserBoxItems from './components/UserBoxItems.svelte'
import UserBoxList from './components/UserBoxList.svelte' import UserBoxList from './components/UserBoxList.svelte'
import UserDetails from './components/UserDetails.svelte'
import UserInfo from './components/UserInfo.svelte' import UserInfo from './components/UserInfo.svelte'
import UsersList from './components/UsersList.svelte'
import UsersPopup from './components/UsersPopup.svelte' import UsersPopup from './components/UsersPopup.svelte'
import ActivityChannelPresenter from './components/activity/ActivityChannelPresenter.svelte' import ActivityChannelPresenter from './components/activity/ActivityChannelPresenter.svelte'
import NameChangedActivityMessage from './components/activity/NameChangedActivityMessage.svelte'
import TxNameChange from './components/activity/TxNameChange.svelte'
import IconAddMember from './components/icons/AddMember.svelte'
import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte' import ExpandRightDouble from './components/icons/ExpandRightDouble.svelte'
import IconMembers from './components/icons/Members.svelte' import IconMembers from './components/icons/Members.svelte'
import TxNameChange from './components/activity/TxNameChange.svelte'
import NameChangedActivityMessage from './components/activity/NameChangedActivityMessage.svelte'
import SystemAvatar from './components/SystemAvatar.svelte'
import PersonIcon from './components/PersonIcon.svelte'
import UsersList from './components/UsersList.svelte'
import SelectUsersPopup from './components/SelectUsersPopup.svelte'
import IconAddMember from './components/icons/AddMember.svelte'
import UserDetails from './components/UserDetails.svelte'
import EditOrganizationPanel from './components/EditOrganizationPanel.svelte'
import ChannelIcon from './components/ChannelIcon.svelte'
import CreateGuest from './components/CreateGuest.svelte'
import SpaceMembersEditor from './components/SpaceMembersEditor.svelte'
import { get } from 'svelte/store'
import contact from './plugin' import contact from './plugin'
import { import {
channelIdentifierProvider, channelIdentifierProvider,
@ -133,53 +147,54 @@ import {
export * from './utils' export * from './utils'
export { employeeByIdStore, employeesStore } from './utils' export { employeeByIdStore, employeesStore } from './utils'
export { export {
Channels,
ChannelsEditor,
ContactRefPresenter,
ContactPresenter,
ChannelsView,
ChannelsDropdown,
EmployeePresenter,
PersonPresenter,
OrganizationPresenter,
EmployeeBrowser,
MemberPresenter,
EmployeeArrayEditor,
EmployeeEditor,
PersonAccountRefPresenter,
PersonAccountPresenter,
MembersPresenter,
EditPerson,
EmployeeRefPresenter,
AccountArrayEditor, AccountArrayEditor,
AccountBox, AccountBox,
CreateOrganization,
ExpandRightDouble,
EditableAvatar,
UserBox,
AssigneeBox, AssigneeBox,
AssigneePopup, AssigneePopup,
Avatar, Avatar,
UsersPopup, AvatarRef,
EmployeeBox, Channels,
UserBoxList, ChannelsDropdown,
Members, ChannelsEditor,
SpaceMembers, ChannelsView,
CombineAvatars, CombineAvatars,
UserInfo, ContactPresenter,
IconMembers, ContactRefPresenter,
SelectAvatars, CreateGuest,
UserBoxItems, CreateOrganization,
MembersBox,
PersonRefPresenter,
SystemAvatar,
PersonIcon,
UsersList,
SelectUsersPopup,
IconAddMember,
UserDetails,
DeleteConfirmationPopup, DeleteConfirmationPopup,
CreateGuest EditPerson,
EditableAvatar,
EmployeeArrayEditor,
EmployeeBox,
EmployeeBrowser,
EmployeeEditor,
EmployeePresenter,
EmployeeRefPresenter,
ExpandRightDouble,
IconAddMember,
IconMembers,
MemberPresenter,
Members,
MembersBox,
MembersPresenter,
OrganizationPresenter,
PersonAccountPresenter,
PersonAccountRefPresenter,
PersonIcon,
PersonPresenter,
PersonRefPresenter,
SelectAvatars,
SelectUsersPopup,
SpaceMembers,
SystemAvatar,
UserBox,
UserBoxItems,
UserBoxList,
UserDetails,
UserInfo,
UsersList,
UsersPopup
} }
const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({ const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({
@ -287,6 +302,18 @@ export interface PersonLabelTooltip {
props?: any props?: any
} }
function getPersonColor (person: Data<WithLookup<AvatarInfo>>, name: string): ColorDefinition {
const dark = get(themeStore).dark
if (person.avatarProps?.color !== undefined) {
if (person.avatarProps?.color?.startsWith('#')) {
return getPlatformColorDef(hexColorToNumber(person.avatarProps?.color), dark)
}
return getPlatformAvatarColorByName(person.avatarProps?.color, dark)
}
return getPlatformAvatarColorForTextDef(name, dark)
}
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
actionImpl: { actionImpl: {
KickEmployee: kickEmployee, KickEmployee: kickEmployee,
@ -330,6 +357,7 @@ export default async (): Promise<Resources> => ({
ChannelFilter, ChannelFilter,
MergePersons, MergePersons,
Avatar, Avatar,
AvatarRef,
UserBoxList, UserBoxList,
ChannelPresenter, ChannelPresenter,
ChannelPanel, ChannelPanel,
@ -362,19 +390,31 @@ export default async (): Promise<Resources> => ({
) => await queryContact(contact.class.Organization, client, query, filter) ) => await queryContact(contact.class.Organization, client, query, filter)
}, },
function: { function: {
GetFileUrl: (file: string, size: IconSize, fileName?: string) => { GetFileUrl: (person: Data<WithLookup<AvatarInfo>>, name: string, width: number) => {
return [ if (person.avatar == null) {
getFileUrl(file, size, fileName), return {
getFileUrl(file, size, fileName) + ' 1x', color: getPersonColor(person, name)
getFileUrl(file, getIconSize2x(size), fileName) + ' 2x' }
] }
return {
url: getBlobHref(person.$lookup?.avatar, person.avatar),
srcset: getBlobSrcSet(person.$lookup?.avatar, person.avatar, width),
color: getPersonColor(person, name)
}
}, },
GetGravatarUrl: (file: string, size: IconSize, fileName?: string) => [ GetGravatarUrl: (person: Data<WithLookup<AvatarInfo>>, name: string, width: number) => ({
getGravatarUrl(file, size), url: person.avatarProps?.url !== undefined ? getGravatarUrl(person.avatarProps?.url, width) : undefined,
getGravatarUrl(file, size) + ' 1x', srcset:
getGravatarUrl(file, getIconSize2x(size)) + ' 2x' person.avatarProps?.url !== undefined
], ? `${getGravatarUrl(person.avatarProps?.url, width)} 1x, ${getGravatarUrl(person.avatarProps?.url, width * 2)} 2x`
GetColorUrl: (uri: string) => [uri], : undefined,
color: getPersonColor(person, name)
}),
GetColorUrl: (person: Data<WithLookup<AvatarInfo>>, name: string) => ({ color: getPersonColor(person, name) }),
GetExternalUrl: (person: Data<WithLookup<AvatarInfo>>, name: string) => ({
color: getPersonColor(person, name),
url: person.avatarProps?.url
}),
EmployeeSort: employeeSort, EmployeeSort: employeeSort,
FilterChannelInResult: filterChannelInResult, FilterChannelInResult: filterChannelInResult,
FilterChannelNinResult: filterChannelNinResult, FilterChannelNinResult: filterChannelNinResult,

View File

@ -31,6 +31,7 @@ import {
import core, { import core, {
getCurrentAccount, getCurrentAccount,
toIdMap, toIdMap,
type Account,
type Class, type Class,
type Client, type Client,
type Doc, type Doc,
@ -39,8 +40,8 @@ import core, {
type Ref, type Ref,
type Timestamp, type Timestamp,
type TxOperations, type TxOperations,
type Account, type UserStatus,
type UserStatus type WithLookup
} from '@hcengineering/core' } from '@hcengineering/core'
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification' import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
import { getEmbeddedLabel, getResource, translate } from '@hcengineering/platform' import { getEmbeddedLabel, getResource, translate } from '@hcengineering/platform'
@ -287,14 +288,14 @@ async function generateLocation (loc: Location, id: Ref<Contact>): Promise<Resol
} }
} }
export const employeeByIdStore = writable<IdMap<Employee>>(new Map()) export const employeeByIdStore = writable<IdMap<WithLookup<Employee>>>(new Map())
export const employeesStore = writable<Employee[]>([]) export const employeesStore = writable<Array<WithLookup<Employee>>>([])
export const personAccountByIdStore = writable<IdMap<PersonAccount>>(new Map()) export const personAccountByIdStore = writable<IdMap<PersonAccount>>(new Map())
export const channelProviders = writable<ChannelProvider[]>([]) export const channelProviders = writable<ChannelProvider[]>([])
export const personAccountPersonByIdStore = writable<IdMap<Person>>(new Map()) export const personAccountPersonByIdStore = writable<IdMap<WithLookup<Person>>>(new Map())
export const statusByUserStore = writable<Map<Ref<Account>, UserStatus>>(new Map()) export const statusByUserStore = writable<Map<Ref<Account>, UserStatus>>(new Map())
@ -311,10 +312,19 @@ function fillStores (): void {
const accountPersonQuery = createQuery(true) const accountPersonQuery = createQuery(true)
const query = createQuery(true) const query = createQuery(true)
query.query(contact.mixin.Employee, {}, (res) => { query.query(
employeesStore.set(res) contact.mixin.Employee,
employeeByIdStore.set(toIdMap(res)) {},
}) (res) => {
employeesStore.set(res)
employeeByIdStore.set(toIdMap(res))
},
{
lookup: {
avatar: core.class.Blob
}
}
)
const accountQ = createQuery(true) const accountQ = createQuery(true)
accountQ.query(contact.class.PersonAccount, {}, (res) => { accountQ.query(contact.class.PersonAccount, {}, (res) => {
@ -322,11 +332,16 @@ function fillStores (): void {
const persons = res.map((it) => it.person) const persons = res.map((it) => it.person)
accountPersonQuery.query( accountPersonQuery.query<Person>(
contact.class.Person, contact.class.Person,
{ _id: { $in: persons }, [contact.mixin.Employee]: { $exists: false } }, { _id: { $in: persons }, [contact.mixin.Employee]: { $exists: false } },
(res) => { (res) => {
personAccountPersonByIdStore.set(toIdMap(res)) personAccountPersonByIdStore.set(toIdMap(res))
},
{
lookup: {
avatar: core.class.Blob
}
} }
) )
}) })

View File

@ -14,11 +14,23 @@
// limitations under the License. // limitations under the License.
// //
import { Account, AttachedDoc, Class, Doc, Ref, Space, Timestamp, UXObject } from '@hcengineering/core' import {
Account,
AttachedDoc,
Class,
Doc,
Ref,
Space,
Timestamp,
UXObject,
type Blob,
type Data,
type WithLookup
} from '@hcengineering/core'
import type { Asset, Metadata, Plugin, Resource } from '@hcengineering/platform' import type { Asset, Metadata, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform' import { IntlString, plugin } from '@hcengineering/platform'
import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates' import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates'
import type { AnyComponent, IconSize, ResolvedLocation } from '@hcengineering/ui' import type { AnyComponent, ColorDefinition, ResolvedLocation } from '@hcengineering/ui'
import { Action, FilterMode, Viewlet } from '@hcengineering/view' import { Action, FilterMode, Viewlet } from '@hcengineering/view'
/** /**
@ -65,13 +77,19 @@ export interface ChannelItem extends AttachedDoc {
export enum AvatarType { export enum AvatarType {
COLOR = 'color', COLOR = 'color',
IMAGE = 'image', IMAGE = 'image',
GRAVATAR = 'gravatar' GRAVATAR = 'gravatar',
EXTERNAL = 'external'
} }
/** /**
* @public * @public
*/ */
export type GetAvatarUrl = (uri: string, size: IconSize) => string[] export type GetAvatarUrl = (
uri: Data<WithLookup<AvatarInfo>>,
name: string,
width?: number
) => { url?: string, srcSet?: string, color: ColorDefinition }
/** /**
* @public * @public
@ -81,12 +99,19 @@ export interface AvatarProvider extends Doc {
getUrl: Resource<GetAvatarUrl> getUrl: Resource<GetAvatarUrl>
} }
export interface AvatarInfo extends Doc {
avatarType: AvatarType
avatar?: Ref<Blob> | null
avatarProps?: {
color?: string
url?: string
}
}
/** /**
* @public * @public
*/ */
export interface Contact extends Doc { export interface Contact extends Doc, AvatarInfo {
name: string name: string
avatar?: string | null
attachments?: number attachments?: number
comments?: number comments?: number
channels?: number channels?: number
@ -180,6 +205,7 @@ export const contactPlugin = plugin(contactId, {
ChannelsPresenter: '' as AnyComponent, ChannelsPresenter: '' as AnyComponent,
MembersPresenter: '' as AnyComponent, MembersPresenter: '' as AnyComponent,
Avatar: '' as AnyComponent, Avatar: '' as AnyComponent,
AvatarRef: '' as AnyComponent,
UserBoxList: '' as AnyComponent, UserBoxList: '' as AnyComponent,
ChannelPresenter: '' as AnyComponent, ChannelPresenter: '' as AnyComponent,
SpaceMembers: '' as AnyComponent, SpaceMembers: '' as AnyComponent,
@ -212,7 +238,8 @@ export const contactPlugin = plugin(contactId, {
function: { function: {
GetColorUrl: '' as Resource<GetAvatarUrl>, GetColorUrl: '' as Resource<GetAvatarUrl>,
GetFileUrl: '' as Resource<GetAvatarUrl>, GetFileUrl: '' as Resource<GetAvatarUrl>,
GetGravatarUrl: '' as Resource<GetAvatarUrl> GetGravatarUrl: '' as Resource<GetAvatarUrl>,
GetExternalUrl: '' as Resource<GetAvatarUrl>
}, },
icon: { icon: {
ContactApplication: '' as Asset, ContactApplication: '' as Asset,

View File

@ -13,12 +13,12 @@
// limitations under the License. // limitations under the License.
// //
import { AttachedData, Class, Client, Doc, FindResult, Ref, Hierarchy } from '@hcengineering/core' import { AttachedData, Class, Client, Doc, FindResult, Hierarchy, Ref } from '@hcengineering/core'
import { IconSize, ColorDefinition } from '@hcengineering/ui'
import { MD5 } from 'crypto-js'
import { AvatarProvider, AvatarType, Channel, Contact, contactPlugin, Person } from '.'
import { AVATAR_COLORS, GravatarPlaceholderType } from './types'
import { getMetadata } from '@hcengineering/platform' import { getMetadata } from '@hcengineering/platform'
import { ColorDefinition } from '@hcengineering/ui'
import { MD5 } from 'crypto-js'
import { AvatarProvider, AvatarType, Channel, Contact, Person, contactPlugin } from '.'
import { AVATAR_COLORS, GravatarPlaceholderType } from './types'
/** /**
* @public * @public
@ -58,16 +58,8 @@ export function buildGravatarId (email: string): string {
/** /**
* @public * @public
*/ */
export function getAvatarProviderId (avatar?: string | null): Ref<AvatarProvider> | undefined { export function getAvatarProviderId (kind: AvatarType): Ref<AvatarProvider> | undefined {
if (avatar === null || avatar === undefined || avatar === '') { switch (kind) {
return
}
if (!avatar.includes('://')) {
return contactPlugin.avatarProvider.Image
}
const [schema] = avatar.split('://')
switch (schema) {
case AvatarType.GRAVATAR: case AvatarType.GRAVATAR:
return contactPlugin.avatarProvider.Gravatar return contactPlugin.avatarProvider.Gravatar
case AvatarType.COLOR: case AvatarType.COLOR:
@ -81,31 +73,9 @@ export function getAvatarProviderId (avatar?: string | null): Ref<AvatarProvider
*/ */
export function getGravatarUrl ( export function getGravatarUrl (
gravatarId: string, gravatarId: string,
size: IconSize = 'full', width: number = 64,
placeholder: GravatarPlaceholderType = 'identicon' placeholder: GravatarPlaceholderType = 'identicon'
): string { ): string {
let width = 64
switch (size) {
case 'inline':
case 'tiny':
case 'x-small':
case 'small':
case 'medium':
width = 128
break
case 'large':
width = 256
break
case 'x-large':
width = 512
break
case '2x-large':
width = 1024
break
case 'full':
width = 2048
break
}
return `https://gravatar.com/avatar/${gravatarId}?s=${width}&d=${placeholder}` return `https://gravatar.com/avatar/${gravatarId}?s=${width}&d=${placeholder}`
} }
@ -114,7 +84,7 @@ export function getGravatarUrl (
*/ */
export async function checkHasGravatar (gravatarId: string, fetch?: typeof window.fetch): Promise<boolean> { export async function checkHasGravatar (gravatarId: string, fetch?: typeof window.fetch): Promise<boolean> {
try { try {
return (await (fetch ?? window.fetch)(getGravatarUrl(gravatarId, 'full', '404'))).ok return (await (fetch ?? window.fetch)(getGravatarUrl(gravatarId, 2048, '404'))).ok
} catch { } catch {
return false return false
} }

View File

@ -16,7 +16,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment' import attachment, { Attachment } from '@hcengineering/attachment'
import core, { Doc, Ref, WithLookup, generateId } from '@hcengineering/core' import core, { Doc, Ref, WithLookup, generateId, type Blob } from '@hcengineering/core'
import { Document } from '@hcengineering/document' import { Document } from '@hcengineering/document'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel' import { Panel } from '@hcengineering/panel'
@ -102,7 +102,7 @@
isStarred = res.length !== 0 isStarred = res.length !== 0
}) })
async function createEmbedding (file: File): Promise<{ file: string, type: string } | undefined> { async function createEmbedding (file: File): Promise<{ file: Ref<Blob>, type: string } | undefined> {
if (doc === undefined) { if (doc === undefined) {
return undefined return undefined
} }

View File

@ -49,7 +49,7 @@
showPopup( showPopup(
FilePreviewPopup, FilePreviewPopup,
{ {
file: blob._id, file: value.$lookup?.file ?? value.file,
contentType: blob.contentType, contentType: blob.contentType,
name: value.name, name: value.name,
metadata: value.metadata metadata: value.metadata

View File

@ -13,17 +13,17 @@
// limitations under the License. // limitations under the License.
// //
import { type Doc, type Ref } from '@hcengineering/core' import { type Doc, type Ref, type WithLookup } from '@hcengineering/core'
import drive, { type Drive, type File, type Folder } from '@hcengineering/drive' import drive, { type Drive, type File, type Folder } from '@hcengineering/drive'
import { type Resources } from '@hcengineering/platform' import { type Resources } from '@hcengineering/platform'
import { getFileUrl } from '@hcengineering/presentation' import { getBlobHref } from '@hcengineering/presentation'
import { type Location, showPopup } from '@hcengineering/ui' import { showPopup, type Location } from '@hcengineering/ui'
import CreateDrive from './components/CreateDrive.svelte' import CreateDrive from './components/CreateDrive.svelte'
import DrivePanel from './components/DrivePanel.svelte' import DrivePanel from './components/DrivePanel.svelte'
import DrivePresenter from './components/DrivePresenter.svelte'
import DriveSpaceHeader from './components/DriveSpaceHeader.svelte' import DriveSpaceHeader from './components/DriveSpaceHeader.svelte'
import DriveSpacePresenter from './components/DriveSpacePresenter.svelte' import DriveSpacePresenter from './components/DriveSpacePresenter.svelte'
import DrivePresenter from './components/DrivePresenter.svelte'
import EditFolder from './components/EditFolder.svelte' import EditFolder from './components/EditFolder.svelte'
import FilePresenter from './components/FilePresenter.svelte' import FilePresenter from './components/FilePresenter.svelte'
import FileSizePresenter from './components/FileSizePresenter.svelte' import FileSizePresenter from './components/FileSizePresenter.svelte'
@ -47,10 +47,10 @@ async function EditDrive (drive: Drive): Promise<void> {
showPopup(CreateDrive, { drive }) showPopup(CreateDrive, { drive })
} }
async function DownloadFile (doc: File | File[]): Promise<void> { async function DownloadFile (doc: WithLookup<File> | Array<WithLookup<File>>): Promise<void> {
const files = Array.isArray(doc) ? doc : [doc] const files = Array.isArray(doc) ? doc : [doc]
for (const file of files) { for (const file of files) {
const href = getFileUrl(file.file, 'full', file.name) const href = getBlobHref(file.$lookup?.file, file.file, file.name)
const link = document.createElement('a') const link = document.createElement('a')
link.style.display = 'none' link.style.display = 'none'
link.target = '_blank' link.target = '_blank'

View File

@ -10,9 +10,15 @@ import core, {
} from '@hcengineering/core' } from '@hcengineering/core'
import login, { loginId } from '@hcengineering/login' import login, { loginId } from '@hcengineering/login'
import { getMetadata, getResource, setMetadata } from '@hcengineering/platform' import { getMetadata, getResource, setMetadata } from '@hcengineering/platform'
import presentation, { closeClient, refreshClient, setClient } from '@hcengineering/presentation' import presentation, { closeClient, refreshClient, setClient, setPresentationCookie } from '@hcengineering/presentation'
import { fetchMetadataLocalStorage, getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui' import {
import { writable } from 'svelte/store' fetchMetadataLocalStorage,
getCurrentLocation,
navigate,
setMetadataLocalStorage,
workspaceId
} from '@hcengineering/ui'
import { writable, get } from 'svelte/store'
export const versionError = writable<string | undefined>(undefined) export const versionError = writable<string | undefined>(undefined)
@ -31,8 +37,8 @@ export async function connect (title: string): Promise<Client | undefined> {
return return
} }
setMetadata(presentation.metadata.Token, token) setMetadata(presentation.metadata.Token, token)
document.cookie =
encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent(token) + '; path=/' setPresentationCookie(token, get(workspaceId))
const getEndpoint = await getResource(login.function.GetEndpoint) const getEndpoint = await getResource(login.function.GetEndpoint)
const endpoint = await getEndpoint() const endpoint = await getEndpoint()
@ -183,8 +189,7 @@ function clearMetadata (ws: string): void {
} }
setMetadata(presentation.metadata.Token, null) setMetadata(presentation.metadata.Token, null)
setMetadataLocalStorage(login.metadata.LastToken, null) setMetadataLocalStorage(login.metadata.LastToken, null)
document.cookie = setPresentationCookie('', get(workspaceId))
encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent('') + '; path=/'
setMetadataLocalStorage(login.metadata.LoginEndpoint, null) setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
setMetadataLocalStorage(login.metadata.LoginEmail, null) setMetadataLocalStorage(login.metadata.LoginEmail, null)
void closeClient() void closeClient()

View File

@ -123,7 +123,7 @@
<div class="mr-2"> <div class="mr-2">
<Button icon={IconAdd} kind={'list'} on:click={createChild} /> <Button icon={IconAdd} kind={'list'} on:click={createChild} />
</div> </div>
<Avatar size={'medium'} avatar={value.avatar} icon={hr.icon.Department} name={value.name} /> <Avatar size={'medium'} person={value} icon={hr.icon.Department} name={value.name} />
<div class="flex-row ml-2 mr-4"> <div class="flex-row ml-2 mr-4">
<div class="fs-title"> <div class="fs-title">
{value.name} {value.name}

View File

@ -37,9 +37,7 @@
await avatarEditor.removeAvatar(object.avatar) await avatarEditor.removeAvatar(object.avatar)
} }
const avatar = await avatarEditor.createAvatar() const avatar = await avatarEditor.createAvatar()
await client.updateDoc(object._class, object.space, object._id, { await client.diffUpdate(object, avatar)
avatar
})
} }
async function nameChange (): Promise<void> { async function nameChange (): Promise<void> {
@ -73,7 +71,7 @@
<div class="mr-8"> <div class="mr-8">
{#key object} {#key object}
<EditableAvatar <EditableAvatar
avatar={object.avatar} person={object}
size={'x-large'} size={'x-large'}
icon={hr.icon.Department} icon={hr.icon.Department}
bind:this={avatarEditor} bind:this={avatarEditor}

View File

@ -34,7 +34,7 @@
<DocNavLink object={value}> <DocNavLink object={value}>
<div class="flex-row-center"> <div class="flex-row-center">
<div class="member-icon mr-2"> <div class="member-icon mr-2">
<Avatar size={'medium'} avatar={value.avatar} name={value.name} /> <Avatar size={'medium'} person={value} name={value.name} />
</div> </div>
<div class="flex-col"> <div class="flex-col">
<div class="member-title fs-title"> <div class="member-title fs-title">

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Channel, combineName, Contact, findContacts } from '@hcengineering/contact' import { AvatarType, Channel, combineName, Contact, findContacts } from '@hcengineering/contact'
import { ChannelsDropdown, EditableAvatar, PersonPresenter } from '@hcengineering/contact-resources' import { ChannelsDropdown, EditableAvatar, PersonPresenter } from '@hcengineering/contact-resources'
import contact from '@hcengineering/contact-resources/src/plugin' import contact from '@hcengineering/contact-resources/src/plugin'
import { AttachedData, Class, Data, Doc, generateId, MixinData, Ref, WithLookup } from '@hcengineering/core' import { AttachedData, Class, Data, Doc, generateId, MixinData, Ref, WithLookup } from '@hcengineering/core'
@ -60,10 +60,14 @@
async function createCustomer () { async function createCustomer () {
const candidate: Data<Contact> = { const candidate: Data<Contact> = {
name: formatName(targetClass._id, firstName, lastName, object.name), name: formatName(targetClass._id, firstName, lastName, object.name),
city: object.city city: object.city,
avatarType: AvatarType.COLOR
} }
if (avatar !== undefined) { if (avatar !== undefined) {
candidate.avatar = await avatarEditor.createAvatar() const info = await avatarEditor.createAvatar()
candidate.avatar = info.avatar
candidate.avatarType = info.avatarType
candidate.avatarProps = info.avatarProps
} }
const candidateData: MixinData<Contact, Customer> = { const candidateData: MixinData<Contact, Customer> = {
description: object.description description: object.description
@ -188,7 +192,7 @@
</div> </div>
<div class="ml-4 flex"> <div class="ml-4 flex">
<EditableAvatar <EditableAvatar
avatar={object.avatar} person={object}
size={'large'} size={'large'}
name={object.name} name={object.name}
bind:this={avatarEditor} bind:this={avatarEditor}

View File

@ -181,7 +181,7 @@
<div class="flex-between header bottom-divider"> <div class="flex-between header bottom-divider">
<div class="flex-row-center"> <div class="flex-row-center">
{#if employee} {#if employee}
<Avatar size={'smaller'} avatar={employee.avatar} name={employee.name} /> <Avatar size={'smaller'} person={employee} name={employee.name} />
<span class="font-medium mx-2">{getName(client.getHierarchy(), employee)}</span> <span class="font-medium mx-2">{getName(client.getHierarchy(), employee)}</span>
{/if} {/if}
{#if newTxes > 0} {#if newTxes > 0}

View File

@ -22,7 +22,7 @@
<svelte:fragment slot="content"> <svelte:fragment slot="content">
<div class="flex-row-center flex-wrap gap-2"> <div class="flex-row-center flex-wrap gap-2">
{#if sender} {#if sender}
<Avatar avatar={sender.avatar} name={sender.name} size={'small'} /> <Avatar person={sender} name={sender.name} size={'small'} />
{/if} {/if}
<span class="overflow-label"> <span class="overflow-label">
{value.body} {value.body}

View File

@ -89,7 +89,7 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="inbox-activity__content shrink flex-grow clear-mins" class:read={newTxes === 0}> <div class="inbox-activity__content shrink flex-grow clear-mins" class:read={newTxes === 0}>
<div class="flex-row-center gap-2"> <div class="flex-row-center gap-2">
<Avatar avatar={employee?.avatar} size={'small'} name={employee?.name} /> <Avatar person={employee} size={'small'} name={employee?.name} />
{#if employee} {#if employee}
<span class="font-medium">{getName(client.getHierarchy(), employee)}</span> <span class="font-medium">{getName(client.getHierarchy(), employee)}</span>
{:else} {:else}

View File

@ -45,7 +45,7 @@
<div class="antiContactCard"> <div class="antiContactCard">
<div class="label uppercase"><Label label={recruit.string.Talent} /></div> <div class="label uppercase"><Label label={recruit.string.Talent} /></div>
<Avatar avatar={candidate?.avatar} size={'large'} name={candidate?.name} /> <Avatar person={candidate} size={'large'} name={candidate?.name} />
{#if candidate} {#if candidate}
<DocNavLink object={candidate} {disabled}> <DocNavLink object={candidate} {disabled}>
<div class="name lines-limit-2"> <div class="name lines-limit-2">

View File

@ -15,7 +15,14 @@
<script lang="ts"> <script lang="ts">
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import attachment from '@hcengineering/attachment' import attachment from '@hcengineering/attachment'
import contact, { Channel, ChannelProvider, combineName, findContacts, Person } from '@hcengineering/contact' import contact, {
AvatarType,
Channel,
ChannelProvider,
combineName,
findContacts,
Person
} from '@hcengineering/contact'
import { ChannelsDropdown, EditableAvatar, PersonPresenter } from '@hcengineering/contact-resources' import { ChannelsDropdown, EditableAvatar, PersonPresenter } from '@hcengineering/contact-resources'
import { import {
Account, Account,
@ -182,11 +189,13 @@
const candidate: Data<Person> = { const candidate: Data<Person> = {
name: combineName(object.firstName ?? '', object.lastName ?? ''), name: combineName(object.firstName ?? '', object.lastName ?? ''),
city: object.city, city: object.city,
channels: 0 channels: 0,
} avatarType: AvatarType.COLOR
if (avatar !== undefined) {
candidate.avatar = await avatarEditor.createAvatar()
} }
const info = await avatarEditor.createAvatar()
candidate.avatar = info.avatar
candidate.avatarType = info.avatarType
candidate.avatarProps = info.avatarProps
const candidateData: MixinData<Person, Candidate> = { const candidateData: MixinData<Person, Candidate> = {
title: object.title, title: object.title,
onsite: object.onsite, onsite: object.onsite,
@ -619,7 +628,9 @@
disabled={loading} disabled={loading}
bind:this={avatarEditor} bind:this={avatarEditor}
bind:direct={object.avatar} bind:direct={object.avatar}
avatar={undefined} person={{
avatarType: AvatarType.COLOR
}}
size={'large'} size={'large'}
name={combineName(object?.firstName?.trim() ?? '', object?.lastName?.trim() ?? '')} name={combineName(object?.firstName?.trim() ?? '', object?.lastName?.trim() ?? '')}
/> />

View File

@ -67,7 +67,7 @@
{/if} {/if}
<div class="flex-between mb-1"> <div class="flex-between mb-1">
<div class="flex-row-center"> <div class="flex-row-center">
<Avatar avatar={object.$lookup?.attachedTo?.avatar} size={'medium'} name={object.$lookup?.attachedTo?.name} /> <Avatar person={object.$lookup?.attachedTo} size={'medium'} name={object.$lookup?.attachedTo?.name} />
<div class="flex-grow flex-col min-w-0 ml-2"> <div class="flex-grow flex-col min-w-0 ml-2">
<div class="fs-title over-underline lines-limit-2"> <div class="fs-title over-underline lines-limit-2">
{object.$lookup?.attachedTo ? getName(client.getHierarchy(), object.$lookup.attachedTo) : ''} {object.$lookup?.attachedTo ? getName(client.getHierarchy(), object.$lookup.attachedTo) : ''}

View File

@ -45,7 +45,7 @@
on:click={() => onClick(p)} on:click={() => onClick(p)}
> >
<div class="icon"> <div class="icon">
<Avatar size={'x-small'} avatar={p.avatar} name={p.name} /> <Avatar size={'x-small'} person={p} name={p.name} />
</div> </div>
</div> </div>
{/each} {/each}

View File

@ -53,9 +53,7 @@
await avatarEditor.removeAvatar(employee.avatar) await avatarEditor.removeAvatar(employee.avatar)
} }
const avatar = await avatarEditor.createAvatar() const avatar = await avatarEditor.createAvatar()
await client.update(employee, { await client.diffUpdate(employee, avatar)
avatar
})
} }
const manager = createFocusManager() const manager = createFocusManager()
@ -97,7 +95,7 @@
<div class="flex flex-grow w-full"> <div class="flex flex-grow w-full">
<div class="mr-8"> <div class="mr-8">
<EditableAvatar <EditableAvatar
avatar={employee.avatar} person={employee}
email={account.email} email={account.email}
size={'x-large'} size={'x-large'}
name={employee.name} name={employee.name}

View File

@ -13,23 +13,14 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onDestroy } from 'svelte' import { AvatarType } from '@hcengineering/contact'
import contact, { Employee, PersonAccount, combineName, getFirstName, getLastName } from '@hcengineering/contact' import { EditableAvatar } from '@hcengineering/contact-resources'
import { ChannelsEditor, EditableAvatar, employeeByIdStore } from '@hcengineering/contact-resources'
import { AttributeEditor, getClient, MessageBox } from '@hcengineering/presentation'
import {
Button,
createFocusManager,
EditBox,
FocusHandler,
showPopup,
Header,
Breadcrumb,
Label
} from '@hcengineering/ui'
import setting from '../plugin'
import { WorkspaceSetting } from '@hcengineering/setting'
import { getEmbeddedLabel } from '@hcengineering/platform' import { getEmbeddedLabel } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { WorkspaceSetting } from '@hcengineering/setting'
import { FocusHandler, Label, createFocusManager } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import setting from '../plugin'
export let visibleNav: boolean = true export let visibleNav: boolean = true
@ -49,18 +40,19 @@
await client.createDoc( await client.createDoc(
setting.class.WorkspaceSetting, setting.class.WorkspaceSetting,
setting.space.Setting, setting.space.Setting,
{ icon: avatar }, { icon: avatar.avatar },
setting.ids.WorkspaceSetting setting.ids.WorkspaceSetting
) )
return return
} }
if (workspaceSettings.icon != null) { const avatar = await avatarEditor.createAvatar()
if (workspaceSettings.icon != null && workspaceSettings.icon !== avatar.avatar) {
// Different avatar
await avatarEditor.removeAvatar(workspaceSettings.icon) await avatarEditor.removeAvatar(workspaceSettings.icon)
} }
const avatar = await avatarEditor.createAvatar()
await client.update(workspaceSettings, { await client.update(workspaceSettings, {
icon: avatar icon: avatar.avatar
}) })
} }
@ -71,7 +63,10 @@
<div class="hulyComponent p-10 flex ac-body row"> <div class="hulyComponent p-10 flex ac-body row">
<EditableAvatar <EditableAvatar
avatar={workspaceSettings?.icon} person={{
avatarType: AvatarType.IMAGE,
avatar: workspaceSettings?.icon
}}
size={'x-large'} size={'x-large'}
bind:this={avatarEditor} bind:this={avatarEditor}
on:done={onAvatarDone} on:done={onAvatarDone}

Some files were not shown because too many files have changed in this diff Show More