mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-7011: Switch to Ref<Blob> (#5661)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
27a98e0641
commit
8c6a5f4e9d
@ -11,6 +11,9 @@ dependencies:
|
||||
'@aws-sdk/client-s3':
|
||||
specifier: ^3.575.0
|
||||
version: 3.577.0
|
||||
'@aws-sdk/s3-request-presigner':
|
||||
specifier: ^3.582.0
|
||||
version: 3.582.0
|
||||
'@elastic/elasticsearch':
|
||||
specifier: ^7.14.0
|
||||
version: 7.17.13
|
||||
@ -1857,6 +1860,21 @@ packages:
|
||||
tslib: 2.6.2
|
||||
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:
|
||||
resolution: {integrity: sha512-QS/dh3+NqZbXtY0j/DZ867ogP413pG5cFGqBy9OeOhDMsolcwLrQbi0S0c621dc1QNq+er9ffaMhZ/aPkyXXIg==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@ -1902,6 +1920,20 @@ packages:
|
||||
tslib: 2.6.2
|
||||
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:
|
||||
resolution: {integrity: sha512-mMykGRFBYmlDcMhdbhNM0z1JFUaYYZ8r9WV7Dd0T2PWELv2brSAjDAOBHdJLHObDMYRnM6H0/Y974qTl3icEcQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@ -1914,6 +1946,18 @@ packages:
|
||||
tslib: 2.6.2
|
||||
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):
|
||||
resolution: {integrity: sha512-0CkIZpcC3DNQJQ1hDjm2bdSy/Xjs7Ny5YvSsacasGOkNfk+FdkiQy6N67bZX3Zbc9KIx+Nz4bu3iDeNSNplnnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@ -1953,6 +1997,16 @@ packages:
|
||||
tslib: 2.6.2
|
||||
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:
|
||||
resolution: {integrity: sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@ -19662,7 +19716,7 @@ packages:
|
||||
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):
|
||||
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
|
||||
name: '@rush-temp/drive-resources'
|
||||
version: 0.0.0
|
||||
@ -20629,7 +20683,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
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
|
||||
name: '@rush-temp/minio'
|
||||
version: 0.0.0
|
||||
@ -22817,12 +22871,13 @@ packages:
|
||||
dev: false
|
||||
|
||||
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
|
||||
name: '@rush-temp/s3'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@aws-sdk/client-s3': 3.577.0
|
||||
'@aws-sdk/s3-request-presigner': 3.582.0
|
||||
'@types/jest': 29.5.12
|
||||
'@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)
|
||||
|
@ -17,42 +17,42 @@ import attachment from '@hcengineering/attachment'
|
||||
import chunter, { type ChatMessage } from '@hcengineering/chunter'
|
||||
import contact from '@hcengineering/contact'
|
||||
import core, {
|
||||
ClassifierKind,
|
||||
DOMAIN_STATUS,
|
||||
DOMAIN_TX,
|
||||
type MeasureContext,
|
||||
SortingOrder,
|
||||
TxOperations,
|
||||
TxProcessor,
|
||||
generateId,
|
||||
getObjectValue,
|
||||
toIdMap,
|
||||
type BackupClient,
|
||||
type Class,
|
||||
type Client as CoreClient,
|
||||
type Doc,
|
||||
type Domain,
|
||||
type MeasureContext,
|
||||
type Ref,
|
||||
type TxCreateDoc,
|
||||
type WorkspaceId,
|
||||
type StatusCategory,
|
||||
type TxMixin,
|
||||
type TxCUD,
|
||||
type TxUpdateDoc,
|
||||
DOMAIN_STATUS,
|
||||
type Status,
|
||||
toIdMap,
|
||||
type Class,
|
||||
ClassifierKind
|
||||
type StatusCategory,
|
||||
type TxCUD,
|
||||
type TxCreateDoc,
|
||||
type TxMixin,
|
||||
type TxUpdateDoc,
|
||||
type WorkspaceId
|
||||
} 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 recruit, { type Applicant, type Vacancy } from '@hcengineering/recruit'
|
||||
import recruitModel, { defaultApplicantStatuses } from '@hcengineering/model-recruit'
|
||||
import { type StorageAdapter } from '@hcengineering/server-core'
|
||||
import { connect } from '@hcengineering/server-tool'
|
||||
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 { deepEqual } from 'fast-equals'
|
||||
import { MongoClient } from 'mongodb'
|
||||
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
|
||||
import { DOMAIN_SPACE } from '@hcengineering/model-core'
|
||||
|
||||
export async function cleanWorkspace (
|
||||
ctx: MeasureContext,
|
||||
@ -77,7 +77,7 @@ export async function cleanWorkspace (
|
||||
const contacts = await ops.findAll(contact.class.Contact, {})
|
||||
|
||||
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)
|
||||
@ -177,7 +177,7 @@ export async function fixMinioBW (
|
||||
break
|
||||
}
|
||||
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])
|
||||
removed++
|
||||
if (removed % 100 === 0) {
|
||||
|
@ -15,18 +15,18 @@
|
||||
|
||||
import activity from '@hcengineering/activity'
|
||||
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 {
|
||||
type Builder,
|
||||
Index,
|
||||
Model,
|
||||
Prop,
|
||||
TypeAttachment,
|
||||
TypeBlob,
|
||||
TypeBoolean,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
TypeTimestamp,
|
||||
UX
|
||||
UX,
|
||||
type Builder
|
||||
} from '@hcengineering/model'
|
||||
import core, { TAttachedDoc } from '@hcengineering/model-core'
|
||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||
@ -46,8 +46,8 @@ export class TAttachment extends TAttachedDoc implements Attachment {
|
||||
@Index(IndexKind.FullText)
|
||||
name!: string
|
||||
|
||||
@Prop(TypeAttachment(), attachment.string.File)
|
||||
file!: string
|
||||
@Prop(TypeBlob(), attachment.string.File)
|
||||
file!: Ref<Blob>
|
||||
|
||||
@Prop(TypeString(), attachment.string.Size)
|
||||
size!: number
|
||||
|
@ -36,6 +36,7 @@ import {
|
||||
DOMAIN_MODEL,
|
||||
DateRangeMode,
|
||||
IndexKind,
|
||||
type Blob,
|
||||
type Class,
|
||||
type Domain,
|
||||
type Markup,
|
||||
@ -50,10 +51,11 @@ import {
|
||||
Model,
|
||||
Prop,
|
||||
ReadOnly,
|
||||
TypeAttachment,
|
||||
TypeBlob,
|
||||
TypeBoolean,
|
||||
TypeCollaborativeMarkup,
|
||||
TypeDate,
|
||||
TypeRecord,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
TypeTimestamp,
|
||||
@ -104,10 +106,23 @@ export class TContact extends TDoc implements Contact {
|
||||
@Index(IndexKind.FullText)
|
||||
name!: string
|
||||
|
||||
@Prop(TypeAttachment(), contact.string.Avatar)
|
||||
@Prop(TypeString(), contact.string.Avatar)
|
||||
@Index(IndexKind.FullText)
|
||||
@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)
|
||||
channels?: number
|
||||
@ -709,6 +724,16 @@ export function createModel (builder: Builder): void {
|
||||
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, {
|
||||
presenter: contact.component.PersonPresenter
|
||||
})
|
||||
|
@ -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 {
|
||||
createDefaultSpace,
|
||||
tryMigrate,
|
||||
tryUpgrade,
|
||||
type MigrateOperation,
|
||||
type MigrateUpdate,
|
||||
type MigrationClient,
|
||||
type MigrationDocumentQuery,
|
||||
type MigrationUpgradeClient,
|
||||
type ModelLogger
|
||||
} from '@hcengineering/model'
|
||||
@ -14,6 +16,7 @@ import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
|
||||
import core from '@hcengineering/model-core'
|
||||
import { DOMAIN_VIEW } from '@hcengineering/model-view'
|
||||
|
||||
import { AvatarType, type Contact } from '@hcengineering/contact'
|
||||
import contact, { DOMAIN_CONTACT, contactId } from './index'
|
||||
|
||||
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 = {
|
||||
async migrate (client: MigrationClient, logger: ModelLogger): Promise<void> {
|
||||
await tryMigrate(client, contactId, [
|
||||
@ -183,6 +235,12 @@ export const contactOperation: MigrateOperation = {
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
state: 'avatars',
|
||||
func: async (client) => {
|
||||
await migrateAvatars(client)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -213,8 +213,8 @@ export class TTypeString extends TType {}
|
||||
export class TTypeRecord extends TType {}
|
||||
|
||||
@UX(core.string.String)
|
||||
@Model(core.class.TypeAttachment, core.class.Type)
|
||||
export class TTypeAttachment extends TType {}
|
||||
@Model(core.class.TypeBlob, core.class.Type)
|
||||
export class TTypeBlob extends TType {}
|
||||
|
||||
@UX(core.string.Hyperlink)
|
||||
@Model(core.class.TypeHyperlink, core.class.Type)
|
||||
|
@ -60,7 +60,7 @@ import {
|
||||
TRefTo,
|
||||
TType,
|
||||
TTypeAny,
|
||||
TTypeAttachment,
|
||||
TTypeBlob,
|
||||
TTypeBoolean,
|
||||
TTypeCollaborativeDoc,
|
||||
TTypeCollaborativeDocVersion,
|
||||
@ -153,7 +153,7 @@ export function createModel (builder: Builder): void {
|
||||
TTypeString,
|
||||
TTypeRank,
|
||||
TTypeRecord,
|
||||
TTypeAttachment,
|
||||
TTypeBlob,
|
||||
TTypeHyperlink,
|
||||
TCollection,
|
||||
TVersion,
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Blob, type Class, type Doc, type Ref, DOMAIN_MODEL } from '@hcengineering/core'
|
||||
import { type Builder, Model, Prop, TypeRef, TypeString } from '@hcengineering/model'
|
||||
import { DOMAIN_MODEL, type Blob, type Class, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { Model, Prop, TypeRef, TypeString, type Builder } from '@hcengineering/model'
|
||||
import core, { TDoc } from '@hcengineering/model-core'
|
||||
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
|
||||
// Import types to prevent .svelte components to being exposed to type typescript.
|
||||
@ -27,12 +27,12 @@ import {
|
||||
type ComponentPointExtension,
|
||||
type CreateExtensionKind,
|
||||
type DocAttributeRule,
|
||||
type DocRules,
|
||||
type DocCreateExtension,
|
||||
type DocCreateFunction,
|
||||
type DocRules,
|
||||
type FilePreviewExtension,
|
||||
type ObjectSearchContext,
|
||||
type ObjectSearchCategory,
|
||||
type ObjectSearchContext,
|
||||
type ObjectSearchFactory
|
||||
} from '@hcengineering/presentation/src/types'
|
||||
import { type AnyComponent, type ComponentExtensionId } from '@hcengineering/ui/src/types'
|
||||
|
@ -45,8 +45,8 @@ export function createModel (builder: Builder): void {
|
||||
builder.mixin(contact.class.Contact, core.class.Class, serverCore.mixin.SearchPresenter, {
|
||||
searchConfig: {
|
||||
iconConfig: {
|
||||
component: contact.component.Avatar,
|
||||
props: ['avatar', 'name']
|
||||
component: contact.component.AvatarRef,
|
||||
props: ['_id']
|
||||
},
|
||||
title: { props: ['name'] }
|
||||
},
|
||||
|
@ -57,8 +57,8 @@ export function createModel (builder: Builder): void {
|
||||
builder.mixin(recruit.class.Applicant, core.class.Class, serverCore.mixin.SearchPresenter, {
|
||||
searchConfig: {
|
||||
iconConfig: {
|
||||
component: contact.component.Avatar,
|
||||
props: [{ avatar: ['attachedTo', 'avatar'] }, { name: ['attachedTo', 'name'] }]
|
||||
component: contact.component.AvatarRef,
|
||||
props: [{ _id: ['attachedTo'] }]
|
||||
},
|
||||
shortTitle: 'identifier',
|
||||
title: {
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
import activity from '@hcengineering/activity'
|
||||
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 core, { TClass, TConfiguration, TDoc } from '@hcengineering/model-core'
|
||||
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)
|
||||
export class TWorkspaceSetting extends TDoc implements WorkspaceSetting {
|
||||
icon?: string
|
||||
icon?: Ref<Blob>
|
||||
}
|
||||
|
||||
@Mixin(setting.mixin.SpaceTypeEditor, core.class.Class)
|
||||
|
@ -460,7 +460,7 @@ export function createModel (builder: Builder): void {
|
||||
view.component.StringEditor,
|
||||
view.component.StringEditorPopup
|
||||
)
|
||||
classPresenter(builder, core.class.TypeAttachment, view.component.StringPresenter)
|
||||
classPresenter(builder, core.class.TypeBlob, view.component.StringPresenter)
|
||||
classPresenter(
|
||||
builder,
|
||||
core.class.TypeHyperlink,
|
||||
|
@ -559,6 +559,22 @@ export interface Blob extends Doc {
|
||||
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
|
||||
*
|
||||
|
@ -111,7 +111,7 @@ export default plugin(coreId, {
|
||||
Account: '' as Ref<Class<Account>>,
|
||||
Type: '' as Ref<Class<Type<any>>>,
|
||||
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>>>,
|
||||
TypeHyperlink: '' as Ref<Class<Type<Hyperlink>>>,
|
||||
TypeNumber: '' as Ref<Class<Type<number>>>,
|
||||
|
@ -13,20 +13,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { LoadModelResponse } from '.'
|
||||
import type { Class, Doc, Domain, Ref, Timestamp } from './classes'
|
||||
import { Hierarchy } from './hierarchy'
|
||||
import type { Doc, Domain, Ref } from './classes'
|
||||
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'
|
||||
|
||||
/**
|
||||
@ -78,24 +66,3 @@ export interface LowLevelStorage {
|
||||
// Remove a list of documents.
|
||||
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>
|
||||
}
|
||||
|
@ -214,7 +214,7 @@ export function extractDocKey (key: string): {
|
||||
export function isFullTextAttribute (attr: AnyAttribute): boolean {
|
||||
return (
|
||||
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.TypeCollaborativeDoc
|
||||
)
|
||||
|
@ -387,8 +387,8 @@ export function TypeString (): Type<string> {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function TypeAttachment (): Type<string> {
|
||||
return { _class: core.class.TypeAttachment, label: core.string.String }
|
||||
export function TypeBlob (): Type<string> {
|
||||
return { _class: core.class.TypeBlob, label: core.string.String }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -13,20 +13,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
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 { getPreviewType, previewTypes } from '../file'
|
||||
import { BlobMetadata, FilePreviewExtension } from '../types'
|
||||
import { getFileUrl } from '../utils'
|
||||
import { getBlobHref, getFileUrl } from '../utils'
|
||||
|
||||
import ActionContext from './ActionContext.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 contentType: string
|
||||
export let metadata: BlobMetadata | undefined
|
||||
@ -57,9 +57,8 @@
|
||||
} else {
|
||||
previewType = undefined
|
||||
}
|
||||
|
||||
let download: HTMLAnchorElement
|
||||
$: src = file === undefined ? '' : getFileUrl(file, 'full', name)
|
||||
$: src = file === undefined ? '' : typeof file === 'string' ? getFileUrl(file, name) : getBlobHref(file, file._id)
|
||||
</script>
|
||||
|
||||
<ActionContext context={{ mode: 'browser' }} />
|
||||
|
@ -14,14 +14,15 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
// import { Doc } from '@hcengineering/core'
|
||||
import type { Blob, Ref } from '@hcengineering/core'
|
||||
import { Button, Dialog, Label, Spinner } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import presentation from '..'
|
||||
import { getFileUrl } from '../utils'
|
||||
import Download from './icons/Download.svelte'
|
||||
import { getBlobHref, getFileUrl } from '../utils'
|
||||
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 contentType: string | undefined
|
||||
// export let popupOptions: PopupOptions
|
||||
@ -44,7 +45,8 @@
|
||||
}
|
||||
})
|
||||
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/')
|
||||
|
||||
let frame: HTMLIFrameElement | undefined = undefined
|
||||
|
@ -13,14 +13,14 @@
|
||||
// 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 { type PopupAlignment } from '@hcengineering/ui'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
import { type BlobMetadata, type FilePreviewExtension } from './types'
|
||||
import { createQuery } from './utils'
|
||||
import plugin from './plugin'
|
||||
import type { BlobMetadata, FilePreviewExtension } from './types'
|
||||
import { createQuery } from './utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -22,6 +22,7 @@ import core, {
|
||||
type AnyAttribute,
|
||||
type ArrOf,
|
||||
type AttachedDoc,
|
||||
type BlobLookup,
|
||||
type Class,
|
||||
type Client,
|
||||
type Collection,
|
||||
@ -34,23 +35,25 @@ import core, {
|
||||
type MeasureDoneOperation,
|
||||
type Mixin,
|
||||
type Obj,
|
||||
type Blob as PlatformBlob,
|
||||
type Ref,
|
||||
type RefTo,
|
||||
type SearchOptions,
|
||||
type SearchQuery,
|
||||
type SearchResult,
|
||||
type Space,
|
||||
type Tx,
|
||||
type TxResult,
|
||||
type TypeAny,
|
||||
type WithLookup,
|
||||
type Space
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { getMetadata, getResource } from '@hcengineering/platform'
|
||||
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 { deepEqual } from 'fast-equals'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { get } from 'svelte/store'
|
||||
import { type KeyedAttribute } from '..'
|
||||
import { OptimizeQueryMiddleware, PresentationPipelineImpl, type PresentationPipeline } from './pipeline'
|
||||
import plugin from './plugin'
|
||||
@ -357,19 +360,117 @@ export function createQuery (dontDestroy?: boolean): LiveQuery {
|
||||
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
|
||||
*/
|
||||
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('://')) {
|
||||
return file
|
||||
}
|
||||
const uploadUrl = getMetadata(plugin.metadata.UploadURL)
|
||||
|
||||
if (filename !== undefined) {
|
||||
return `${uploadUrl as string}/${filename}?file=${file}&size=${size as string}`
|
||||
let result = ''
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
@ -13,10 +13,19 @@
|
||||
// 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'
|
||||
|
||||
export type ListBlobResult = Omit<Blob, 'contentType' | 'version'>
|
||||
|
||||
export interface UploadedObjectInfo {
|
||||
etag: string
|
||||
versionId: string | null
|
||||
@ -27,6 +36,11 @@ export interface BlobStorageIterator {
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface BlobLookupResult {
|
||||
lookups: BlobLookup[]
|
||||
updates?: Map<Ref<Blob>, DocumentUpdate<BlobLookup>>
|
||||
}
|
||||
|
||||
export interface StorageAdapter {
|
||||
initialize: (ctx: MeasureContext, workspaceId: WorkspaceId) => Promise<void>
|
||||
|
||||
@ -56,6 +70,9 @@ export interface StorageAdapter {
|
||||
offset: number,
|
||||
length?: number
|
||||
) => Promise<Readable>
|
||||
|
||||
// Lookup will extend Blob with lookup information.
|
||||
lookup: (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]) => Promise<BlobLookupResult>
|
||||
}
|
||||
|
||||
export interface StorageAdapterEx extends StorageAdapter {
|
||||
@ -133,6 +150,10 @@ export class DummyStorageAdapter implements StorageAdapter, StorageAdapterEx {
|
||||
): Promise<UploadedObjectInfo> {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
async lookup (ctx: MeasureContext, workspaceId: WorkspaceIdWithUrl, docs: Blob[]): Promise<BlobLookupResult> {
|
||||
return { lookups: [] }
|
||||
}
|
||||
}
|
||||
|
||||
export function createDummyStorageAdapter (): StorageAdapter {
|
||||
|
@ -15,23 +15,23 @@
|
||||
//
|
||||
-->
|
||||
<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 Class, type CollaborativeDoc, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { IntlString, getMetadata, translate } from '@hcengineering/platform'
|
||||
import { markupToJSON } from '@hcengineering/text'
|
||||
import presentation, { getFileUrl, getImageSize } from '@hcengineering/presentation'
|
||||
import view from '@hcengineering/view'
|
||||
import { markupToJSON } from '@hcengineering/text'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
IconSize,
|
||||
Loading,
|
||||
PopupAlignment,
|
||||
ThrottledCaller,
|
||||
getEventPositionElement,
|
||||
getPopupPositionElement,
|
||||
ThrottledCaller,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||
import Collaboration, { isChangeOrigin } from '@tiptap/extension-collaboration'
|
||||
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
||||
@ -39,8 +39,8 @@
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
|
||||
import { Doc as YDoc } from 'yjs'
|
||||
|
||||
import { deleteAttachment } from '../command/deleteAttachment'
|
||||
import { Completion } from '../Completion'
|
||||
import { deleteAttachment } from '../command/deleteAttachment'
|
||||
import { textEditorCommandHandler } from '../commands'
|
||||
import { EditorKit } from '../kits/editor-kit'
|
||||
import textEditorPlugin from '../plugin'
|
||||
@ -64,13 +64,13 @@
|
||||
import { noSelectionRender, renderCursor } from './editor/collaboration'
|
||||
import { defaultEditorAttributes } from './editor/editorProps'
|
||||
import { EmojiExtension } from './extension/emoji'
|
||||
import { ImageUploadExtension } from './extension/imageUploadExt'
|
||||
import { type FileAttachFunction } from './extension/types'
|
||||
import { FileUploadExtension } from './extension/fileUploadExt'
|
||||
import { LeftMenuExtension } from './extension/leftMenu'
|
||||
import { ImageUploadExtension } from './extension/imageUploadExt'
|
||||
import { InlineCommandsExtension } from './extension/inlineCommands'
|
||||
import { InlinePopupExtension } from './extension/inlinePopup'
|
||||
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
|
||||
import { LeftMenuExtension } from './extension/leftMenu'
|
||||
import { type FileAttachFunction } from './extension/types'
|
||||
import { completionConfig, inlineCommandsConfig } from './extensions'
|
||||
|
||||
export let collaborativeDoc: CollaborativeDoc
|
||||
@ -288,7 +288,7 @@
|
||||
optionalExtensions.push(
|
||||
ImageUploadExtension.configure({
|
||||
attachFile,
|
||||
uploadUrl: getMetadata(presentation.metadata.UploadURL)
|
||||
getFileUrl
|
||||
})
|
||||
)
|
||||
}
|
||||
@ -339,10 +339,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
const size = await getImageSize(
|
||||
file,
|
||||
getFileUrl(attached.file, 'full', getMetadata(presentation.metadata.UploadURL))
|
||||
)
|
||||
const size = await getImageSize(file, getFileUrl(attached.file))
|
||||
|
||||
editor.commands.insertContent(
|
||||
{
|
||||
|
@ -54,7 +54,7 @@
|
||||
function openOriginalImage (): void {
|
||||
const attributes = textEditor.getAttributes('image')
|
||||
const fileId = attributes['file-id'] ?? attributes.src
|
||||
const url = getFileUrl(fileId, 'full')
|
||||
const url = getFileUrl(fileId)
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 { EmptyMarkup } from '@hcengineering/text'
|
||||
import {
|
||||
@ -17,21 +17,21 @@
|
||||
registerFocus,
|
||||
resizeObserver
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { AnyExtension } from '@tiptap/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { Completion } from '../Completion'
|
||||
import textEditorPlugin from '../plugin'
|
||||
import StyledTextEditor from './StyledTextEditor.svelte'
|
||||
|
||||
import { completionConfig, inlineCommandsConfig } from './extensions'
|
||||
import { RefAction } from '../types'
|
||||
import { addTableHandler } from '../utils'
|
||||
import { EmojiExtension } from './extension/emoji'
|
||||
import { FocusExtension } from './extension/focus'
|
||||
import { ImageUploadExtension } from './extension/imageUploadExt'
|
||||
import { InlineCommandsExtension } from './extension/inlineCommands'
|
||||
import { type FileAttachFunction } from './extension/types'
|
||||
import { RefAction } from '../types'
|
||||
import { addTableHandler } from '../utils'
|
||||
import { completionConfig, inlineCommandsConfig } from './extensions'
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let content: Markup
|
||||
@ -177,7 +177,7 @@
|
||||
function configureExtensions (): AnyExtension[] {
|
||||
const imageUploadPlugin = ImageUploadExtension.configure({
|
||||
attachFile,
|
||||
uploadUrl: getMetadata(presentation.metadata.UploadURL)
|
||||
getFileUrl
|
||||
})
|
||||
|
||||
const completionPlugin = Completion.configure({
|
||||
@ -251,10 +251,7 @@
|
||||
return
|
||||
}
|
||||
|
||||
const size = await getImageSize(
|
||||
file,
|
||||
getFileUrl(attached.file, 'full', getMetadata(presentation.metadata.UploadURL))
|
||||
)
|
||||
const size = await getImageSize(file, getFileUrl(attached.file))
|
||||
|
||||
textEditor.editorHandler.insertContent(
|
||||
{
|
||||
|
@ -79,7 +79,7 @@ export const FileExtension = FileNode.extend<FileOptions>({
|
||||
const fileType = HTMLAttributes['data-file-type']
|
||||
let href: string = ''
|
||||
if (id != null) {
|
||||
href = getFileUrl(id, 'full', fileName)
|
||||
href = getFileUrl(id, fileName)
|
||||
}
|
||||
const linkAttributes = {
|
||||
class: 'file-name',
|
||||
|
@ -14,7 +14,7 @@
|
||||
//
|
||||
import { FilePreviewPopup } from '@hcengineering/presentation'
|
||||
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 { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
|
||||
@ -26,9 +26,7 @@ export type ImageAlignment = 'center' | 'left' | 'right'
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ImageOptions extends ImageNodeOptions {
|
||||
uploadUrl: string
|
||||
}
|
||||
export interface ImageOptions extends ImageNodeOptions {}
|
||||
|
||||
export interface ImageAlignmentOptions {
|
||||
align?: ImageAlignment
|
||||
@ -63,11 +61,6 @@ declare module '@tiptap/core' {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@ -76,7 +69,8 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
|
||||
return {
|
||||
inline: true,
|
||||
HTMLAttributes: {},
|
||||
uploadUrl: ''
|
||||
getFileUrl: () => '',
|
||||
getFileUrlSrcSet: () => ''
|
||||
}
|
||||
},
|
||||
|
||||
@ -105,33 +99,32 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
|
||||
this.options.HTMLAttributes,
|
||||
HTMLAttributes
|
||||
)
|
||||
|
||||
const uploadUrl = this.options.uploadUrl ?? ''
|
||||
const getFileUrl = this.options.getFileUrl
|
||||
const getFileUrlSrcSet = this.options.getFileUrlSrcSet
|
||||
|
||||
const id = imgAttributes['file-id']
|
||||
if (id != null) {
|
||||
imgAttributes.src = getFileUrl(id, 'full', uploadUrl)
|
||||
let width: IconSize | undefined
|
||||
imgAttributes.src = getFileUrl(id)
|
||||
let width: number | undefined
|
||||
// TODO: Use max width of component may be?
|
||||
switch (imgAttributes.width) {
|
||||
case '32px':
|
||||
width = 'small'
|
||||
width = 32
|
||||
break
|
||||
case '64px':
|
||||
width = 'medium'
|
||||
width = 64
|
||||
break
|
||||
case '128px':
|
||||
width = 128
|
||||
break
|
||||
case '256px':
|
||||
width = 'large'
|
||||
width = 256
|
||||
break
|
||||
case '512px':
|
||||
width = 'x-large'
|
||||
width = 512
|
||||
break
|
||||
}
|
||||
if (width !== undefined) {
|
||||
imgAttributes.src = getFileUrl(id, width, uploadUrl)
|
||||
imgAttributes.srcset =
|
||||
getFileUrl(id, width, uploadUrl) + ' 1x,' + getFileUrl(id, getIconSize2x(width), uploadUrl) + ' 2x'
|
||||
}
|
||||
imgAttributes.srcset = getFileUrlSrcSet(id, width)
|
||||
imgAttributes.class = 'text-editor-image'
|
||||
imgAttributes.contentEditable = false
|
||||
}
|
||||
|
@ -12,21 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { getImageSize } from '@hcengineering/presentation'
|
||||
import { Extension } from '@tiptap/core'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { type EditorView } from '@tiptap/pm/view'
|
||||
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
|
||||
import { getFileUrl } from './imageExt'
|
||||
import { type FileAttachFunction } from './types'
|
||||
import type { Blob, Ref } from '@hcengineering/core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ImageUploadOptions {
|
||||
attachFile?: FileAttachFunction
|
||||
uploadUrl: string
|
||||
getFileUrl: (fileId: Ref<Blob>) => string
|
||||
}
|
||||
|
||||
function getType (type: string): 'image' | 'other' {
|
||||
@ -43,13 +43,13 @@ function getType (type: string): 'image' | 'other' {
|
||||
export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
|
||||
addOptions () {
|
||||
return {
|
||||
uploadUrl: ''
|
||||
getFileUrl: () => ''
|
||||
}
|
||||
},
|
||||
|
||||
addProseMirrorPlugins () {
|
||||
const attachFile = this.options.attachFile
|
||||
const uploadUrl = this.options.uploadUrl
|
||||
const getFileUrl = this.options.getFileUrl
|
||||
|
||||
function handleDrop (
|
||||
view: EditorView,
|
||||
@ -61,9 +61,6 @@ export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
|
||||
for (const uri of uris) {
|
||||
if (uri !== '') {
|
||||
const url = new URL(uri)
|
||||
if (uploadUrl === undefined || !url.href.includes(uploadUrl)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const _file = (url.searchParams.get('file') ?? '').split('/').join('')
|
||||
|
||||
@ -77,7 +74,7 @@ export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
|
||||
if (type === 'image') {
|
||||
const node = view.state.schema.nodes.image.create({
|
||||
'file-id': _file,
|
||||
src: getFileUrl(_file, 'full', uploadUrl)
|
||||
src: getFileUrl(_file as Ref<Blob>)
|
||||
})
|
||||
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
|
||||
view.dispatch(transaction)
|
||||
@ -95,7 +92,7 @@ export const ImageUploadExtension = Extension.create<ImageUploadOptions>({
|
||||
const file = files.item(i)
|
||||
if (file != null && file.type.startsWith('image/')) {
|
||||
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,
|
||||
pos: { pos: number, inside: number } | null,
|
||||
attachFile: FileAttachFunction,
|
||||
uploadUrl: string
|
||||
getFileUrl: (fileId: Ref<Blob>) => string
|
||||
): Promise<void> {
|
||||
const attached = await attachFile(file)
|
||||
|
||||
@ -152,7 +149,7 @@ async function handleImageUpload (
|
||||
}
|
||||
|
||||
try {
|
||||
const url = getFileUrl(attached.file, 'full', uploadUrl)
|
||||
const url = getFileUrl(attached.file)
|
||||
const size = await getImageSize(file, url)
|
||||
const node = view.state.schema.nodes.image.create({
|
||||
'file-id': attached.file,
|
||||
|
@ -13,7 +13,9 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Blob, Ref } from '@hcengineering/core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type FileAttachFunction = (file: File) => Promise<{ file: string, type: string } | undefined>
|
||||
export type FileAttachFunction = (file: File) => Promise<{ file: Ref<Blob>, type: string } | undefined>
|
||||
|
@ -22,14 +22,13 @@ import TaskList from '@tiptap/extension-task-list'
|
||||
|
||||
import { DefaultKit, type DefaultKitOptions } from './default-kit'
|
||||
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { getFileUrl, getFileUrlSrcSet } from '@hcengineering/presentation'
|
||||
import { CodeBlockExtension, codeBlockOptions } from '@hcengineering/text'
|
||||
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 { 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]
|
||||
|
||||
@ -105,7 +104,8 @@ export const EditorKit = Extension.create<EditorKitOptions>({
|
||||
? [
|
||||
ImageExtension.configure({
|
||||
inline: true,
|
||||
uploadUrl: getMetadata(presentation.metadata.UploadURL) ?? '',
|
||||
getFileUrl,
|
||||
getFileUrlSrcSet,
|
||||
...this.options.image
|
||||
})
|
||||
]
|
||||
|
@ -18,6 +18,8 @@ import { collaborativeDocParse, concatLink } from '@hcengineering/core'
|
||||
import { ObservableV2 as Observable } from 'lib0/observable'
|
||||
import { type Doc as YDoc, applyUpdate } from 'yjs'
|
||||
import { type DocumentId, parseDocumentId } from '@hcengineering/collaborator-client'
|
||||
import { workspaceId } from '@hcengineering/ui'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
interface EVENTS {
|
||||
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
|
||||
|
||||
try {
|
||||
const res = await fetch(concatLink(frontUrl, `/files?file=${name}`))
|
||||
const res = await fetch(concatLink(frontUrl, `/files/${get(workspaceId)}?file=${name}`))
|
||||
|
||||
if (res.ok) {
|
||||
const blob = await res.blob()
|
||||
|
@ -14,6 +14,7 @@
|
||||
//
|
||||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
import { getDataAttribute } from './utils'
|
||||
import type { Ref, Blob } from '@hcengineering/core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -21,12 +22,8 @@ import { getDataAttribute } from './utils'
|
||||
export interface ImageOptions {
|
||||
inline: boolean
|
||||
HTMLAttributes: Record<string, any>
|
||||
uploadUrl?: 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}`
|
||||
getFileUrl: (fileId: Ref<Blob>, filename?: string) => string
|
||||
getFileUrlSrcSet: (fileId: Ref<Blob>, size?: number) => string
|
||||
}
|
||||
|
||||
/**
|
||||
@ -39,7 +36,8 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
return {
|
||||
inline: true,
|
||||
HTMLAttributes: {},
|
||||
uploadUrl: ''
|
||||
getFileUrl: () => '',
|
||||
getFileUrlSrcSet: () => ''
|
||||
}
|
||||
},
|
||||
|
||||
@ -107,8 +105,8 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
|
||||
const fileId = imgAttributes['file-id']
|
||||
if (fileId != null) {
|
||||
const uploadUrl = this.options.uploadUrl ?? ''
|
||||
imgAttributes.src = getFileUrl(uploadUrl, fileId)
|
||||
imgAttributes.src = this.options.getFileUrl(fileId)
|
||||
imgAttributes.srcset = this.options.getFileUrlSrcSet(fileId)
|
||||
}
|
||||
|
||||
return ['div', divAttributes, ['img', imgAttributes]]
|
||||
|
@ -23,7 +23,7 @@
|
||||
personByIdStore,
|
||||
SystemAvatar
|
||||
} 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 { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
||||
import activity, { ActivityMessage, ActivityMessagePreviewType } from '@hcengineering/activity'
|
||||
@ -47,7 +47,7 @@
|
||||
const limit = 300
|
||||
|
||||
let isActionsOpened = false
|
||||
let person: Person | undefined = undefined
|
||||
let person: WithLookup<Person> | undefined = undefined
|
||||
|
||||
let width: number
|
||||
|
||||
@ -59,7 +59,7 @@
|
||||
_id: Ref<Account> | undefined,
|
||||
accountById: Map<Ref<PersonAccount>, PersonAccount>,
|
||||
personById: Map<Ref<Person>, Person>
|
||||
): Person | undefined {
|
||||
): WithLookup<Person> | undefined {
|
||||
if (_id === undefined) {
|
||||
return undefined
|
||||
}
|
||||
@ -116,7 +116,7 @@
|
||||
{#if headerObject}
|
||||
<Icon icon={headerIcon ?? classIcon(client, headerObject._class) ?? activity.icon.Activity} size="small" />
|
||||
{:else if person}
|
||||
<Avatar size="card" avatar={person.avatar} name={person.name} />
|
||||
<Avatar size="card" {person} name={person.name} />
|
||||
{:else}
|
||||
<SystemAvatar size="card" />
|
||||
{/if}
|
||||
|
@ -96,7 +96,7 @@
|
||||
<div class="flex-row-center">
|
||||
<div class="avatars">
|
||||
{#each displayPersons as person}
|
||||
<Avatar size="x-small" avatar={person.avatar} name={person.name} />
|
||||
<Avatar size="x-small" {person} name={person.name} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
@ -80,7 +80,7 @@
|
||||
{#if value.icon}
|
||||
<SystemAvatar size="medium" icon={value.icon} iconProps={value.iconProps} />
|
||||
{:else if person}
|
||||
<Avatar size="medium" avatar={person.avatar} name={person.name} />
|
||||
<Avatar size="medium" {person} name={person.name} />
|
||||
{:else}
|
||||
<SystemAvatar size="medium" />
|
||||
{/if}
|
||||
|
@ -144,7 +144,7 @@
|
||||
{#if $$slots.icon}
|
||||
<slot name="icon" />
|
||||
{:else if person}
|
||||
<Avatar size="medium" avatar={person.avatar} name={person.name} />
|
||||
<Avatar size="medium" {person} name={person.name} />
|
||||
{:else}
|
||||
<SystemAvatar size="medium" />
|
||||
{/if}
|
||||
|
@ -14,21 +14,31 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Attachment } from '@hcengineering/attachment'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { getResource, getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import {
|
||||
FilePreviewPopup,
|
||||
getFileUrl,
|
||||
previewTypes,
|
||||
canPreviewFile,
|
||||
getPreviewAlignment
|
||||
getPreviewAlignment,
|
||||
getBlobHref
|
||||
} 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 attachmentPlugin from '../plugin'
|
||||
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 removable = false
|
||||
|
||||
@ -61,7 +71,7 @@
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: attachment.file,
|
||||
file: attachment.$lookup?.file ?? attachment.file,
|
||||
name: attachment.name,
|
||||
contentType: attachment.type ?? '',
|
||||
metadata: attachment.metadata
|
||||
@ -123,9 +133,10 @@
|
||||
<div class="flex">
|
||||
<a
|
||||
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}
|
||||
bind:this={download}
|
||||
use:tooltip={{ label: getEmbeddedLabel(attachment.name) }}
|
||||
on:click|stopPropagation
|
||||
>
|
||||
{#if canPreview}
|
||||
|
@ -14,12 +14,12 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 attachment from '../plugin'
|
||||
import AttachmentList from './AttachmentList.svelte'
|
||||
import { AttachmentImageSize } from '../types'
|
||||
import AttachmentList from './AttachmentList.svelte'
|
||||
|
||||
export let value: Doc & { attachments?: number }
|
||||
export let attachments: Attachment[] | undefined = undefined
|
||||
@ -30,7 +30,7 @@
|
||||
const savedAttachmentsQuery = createQuery()
|
||||
|
||||
let savedAttachmentsIds: Ref<Attachment>[] = []
|
||||
let resAttachments: Attachment[] = []
|
||||
let resAttachments: WithLookup<Attachment>[] = []
|
||||
|
||||
$: updateQuery(value, attachments)
|
||||
|
||||
@ -48,6 +48,11 @@
|
||||
},
|
||||
(res) => {
|
||||
resAttachments = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
|
@ -14,12 +14,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
import { showPopup, closeTooltip } from '@hcengineering/ui'
|
||||
import { FilePreviewPopup, getFileUrl } from '@hcengineering/presentation'
|
||||
import { getType } from '../utils'
|
||||
import type { WithLookup } from '@hcengineering/core'
|
||||
import { FilePreviewPopup, getBlobHref } from '@hcengineering/presentation'
|
||||
import { closeTooltip, showPopup } from '@hcengineering/ui'
|
||||
import filesize from 'filesize'
|
||||
import { getType } from '../utils'
|
||||
|
||||
export let value: Attachment
|
||||
export let value: WithLookup<Attachment>
|
||||
|
||||
const maxLength: number = 18
|
||||
const trimFilename = (fname: string): string =>
|
||||
@ -44,7 +45,7 @@
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: value.file,
|
||||
file: value.$lookup?.file ?? value.file,
|
||||
name: value.name,
|
||||
contentType: value.type,
|
||||
metadata: value.metadata
|
||||
@ -60,7 +61,7 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<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>
|
||||
{:else}
|
||||
<div class="cellMiscPreview">
|
||||
@ -71,7 +72,7 @@
|
||||
{extensionIconLabel(value.name)}
|
||||
</div>
|
||||
{: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>
|
||||
</a>
|
||||
{/if}
|
||||
@ -86,7 +87,7 @@
|
||||
{extensionIconLabel(value.name)}
|
||||
</div>
|
||||
{: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>
|
||||
</a>
|
||||
{/if}
|
||||
@ -99,7 +100,9 @@
|
||||
</div>
|
||||
{:else}
|
||||
<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>
|
||||
{/if}
|
||||
<div class="eCellInfoFilesize">{filesize(value.size)}</div>
|
||||
|
@ -13,13 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getIconSize2x, IconSize } from '@hcengineering/ui'
|
||||
import { getFileUrl } from '@hcengineering/presentation'
|
||||
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'
|
||||
|
||||
export let value: Attachment
|
||||
export let value: WithLookup<Attachment>
|
||||
export let size: AttachmentImageSize = 'auto'
|
||||
|
||||
interface Dimensions {
|
||||
@ -104,15 +105,13 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<img
|
||||
src={getFileUrl(value.file, urlSize)}
|
||||
src={getBlobHref(value.$lookup?.file, value.file, value.name)}
|
||||
style:object-fit={getObjectFit(dimensions)}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
srcset={`${getFileUrl(value.file, urlSize, value.name)} 1x, ${getFileUrl(
|
||||
value.file,
|
||||
getIconSize2x(urlSize),
|
||||
value.name
|
||||
)} 2x`}
|
||||
srcset={value.$lookup?.file !== undefined
|
||||
? getBlobSrcSet(value.$lookup?.file, value.file, sizeToWidth(urlSize))
|
||||
: getFileUrlSrcSet(value.file, sizeToWidth(urlSize))}
|
||||
alt={value.name}
|
||||
/>
|
||||
|
||||
|
@ -14,13 +14,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Ref, type WithLookup } from '@hcengineering/core'
|
||||
import { Scroller } from '@hcengineering/ui'
|
||||
|
||||
import AttachmentPreview from './AttachmentPreview.svelte'
|
||||
import { AttachmentImageSize } from '../types'
|
||||
|
||||
export let attachments: Attachment[] = []
|
||||
export let attachments: WithLookup<Attachment>[] = []
|
||||
export let savedAttachmentsIds: Ref<Attachment>[] = []
|
||||
export let imageSize: AttachmentImageSize | undefined = undefined
|
||||
export let videoPreload = true
|
||||
|
@ -14,12 +14,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Doc } from '@hcengineering/core'
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { createQuery, getClient, getFileMetadata, uploadFile } from '@hcengineering/presentation'
|
||||
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 attachment from '../plugin'
|
||||
|
||||
@ -30,7 +31,7 @@
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let docs: Attachment[] = []
|
||||
let docs: WithLookup<Attachment>[] = []
|
||||
|
||||
let progress = false
|
||||
|
||||
@ -42,6 +43,11 @@
|
||||
},
|
||||
(res) => {
|
||||
docs = res
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
file: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -14,24 +14,26 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import filesize from 'filesize'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
import core from '@hcengineering/core'
|
||||
import { showPopup, closeTooltip, Label, getIconSize2x, Loading } from '@hcengineering/ui'
|
||||
import core, { type WithLookup } from '@hcengineering/core'
|
||||
import presentation, {
|
||||
FilePreviewPopup,
|
||||
canPreviewFile,
|
||||
getFileUrl,
|
||||
getBlobHref,
|
||||
getBlobSrcSet,
|
||||
getPreviewAlignment,
|
||||
previewTypes
|
||||
previewTypes,
|
||||
sizeToWidth
|
||||
} from '@hcengineering/presentation'
|
||||
import { Label, closeTooltip, showPopup } from '@hcengineering/ui'
|
||||
import { permissionsStore } from '@hcengineering/view-resources'
|
||||
import filesize from 'filesize'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getType } from '../utils'
|
||||
|
||||
import AttachmentName from './AttachmentName.svelte'
|
||||
|
||||
export let value: Attachment | undefined
|
||||
export let value: WithLookup<Attachment> | undefined
|
||||
export let removable: boolean = false
|
||||
export let showPreview = false
|
||||
export let preview = false
|
||||
@ -82,7 +84,7 @@
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: value.file,
|
||||
file: value.$lookup?.file ?? value.file,
|
||||
name: value.name,
|
||||
contentType: value.type,
|
||||
metadata: value.metadata
|
||||
@ -100,24 +102,6 @@
|
||||
|
||||
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 {
|
||||
if (value === undefined) return
|
||||
event.dataTransfer?.setData('application/contentType', value.type)
|
||||
@ -132,25 +116,21 @@
|
||||
<a
|
||||
class="no-line"
|
||||
style:flex-shrink={0}
|
||||
href={getFileUrl(value.file, 'full', value.name)}
|
||||
href={getBlobHref(value.$lookup?.file, value.file, value.name)}
|
||||
download={value.name}
|
||||
on:click={clickHandler}
|
||||
on:mousedown={middleClickHandler}
|
||||
on:dragstart={dragStart}
|
||||
>
|
||||
{#if showPreview}
|
||||
<div
|
||||
{#if showPreview && isImage(value.type)}
|
||||
<img
|
||||
src={getBlobHref(value.$lookup?.file, value.file)}
|
||||
srcset={getBlobSrcSet(value.$lookup?.file, value.file, sizeToWidth('large'))}
|
||||
class="flex-center icon"
|
||||
class:svg={value.type === 'image/svg+xml'}
|
||||
class:image={isImage(value.type)}
|
||||
style={imgStyle}
|
||||
>
|
||||
{#if progress}
|
||||
<div class="flex p-3">
|
||||
<Loading />
|
||||
</div>
|
||||
{:else if !isImage(value.type)}{iconLabel(value.name)}{/if}
|
||||
</div>
|
||||
alt={value.name}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-center icon">
|
||||
{iconLabel(value.name)}
|
||||
@ -160,7 +140,7 @@
|
||||
<div class="flex-col info-container">
|
||||
<div class="name">
|
||||
<a
|
||||
href={getFileUrl(value.file, 'full', value.name)}
|
||||
href={getBlobHref(value.$lookup?.file, value.file, value.name)}
|
||||
download={value.name}
|
||||
on:click={clickHandler}
|
||||
on:mousedown={middleClickHandler}
|
||||
@ -174,7 +154,7 @@
|
||||
<span>•</span>
|
||||
<a
|
||||
class="no-line colorInherit"
|
||||
href={getFileUrl(value.file, 'full', value.name)}
|
||||
href={getBlobHref(value.$lookup?.file, value.file, value.name)}
|
||||
download={value.name}
|
||||
bind:this={download}
|
||||
>
|
||||
|
@ -27,8 +27,9 @@
|
||||
import { AttachmentImageSize } from '../types'
|
||||
import AttachmentImagePreview from './AttachmentImagePreview.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 listProvider: ListSelectionProvider | undefined = undefined
|
||||
export let imageSize: AttachmentImageSize = 'auto'
|
||||
@ -50,7 +51,7 @@
|
||||
if (listProvider !== undefined) listProvider.updateFocus(value)
|
||||
const popupInfo = showPopup(
|
||||
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'
|
||||
)
|
||||
dispatch('open', popupInfo.id)
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
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 { KeyedAttribute, createQuery, getClient, uploadFile } from '@hcengineering/presentation'
|
||||
import { getCollaborationUser, getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||
@ -132,7 +132,7 @@
|
||||
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 {
|
||||
const uuid = await uploadFile(file)
|
||||
const _id: Ref<Attachment> = generateId()
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
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 {
|
||||
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
|
||||
try {
|
||||
const uuid = await uploadFile(file)
|
||||
|
@ -13,12 +13,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getFileUrl } from '@hcengineering/presentation'
|
||||
import { getBlobHref } from '@hcengineering/presentation'
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
|
||||
import AttachmentPresenter from './AttachmentPresenter.svelte'
|
||||
import type { WithLookup } from '@hcengineering/core'
|
||||
|
||||
export let value: Attachment
|
||||
export let value: WithLookup<Attachment>
|
||||
export let preload = true
|
||||
|
||||
const maxSizeRem = 20
|
||||
@ -57,7 +58,7 @@
|
||||
</script>
|
||||
|
||||
<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} />
|
||||
<div class="container">
|
||||
<AttachmentPresenter {value} />
|
||||
|
@ -14,13 +14,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { Doc, getCurrentAccount } from '@hcengineering/core'
|
||||
import { getFileUrl, getClient } from '@hcengineering/presentation'
|
||||
import { Icon, IconMoreV, showPopup, Menu } from '@hcengineering/ui'
|
||||
import FileDownload from './icons/FileDownload.svelte'
|
||||
import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
|
||||
import { getBlobHref, getClient } from '@hcengineering/presentation'
|
||||
import { Icon, IconMoreV, Menu, showPopup } from '@hcengineering/ui'
|
||||
import { AttachmentGalleryPresenter } from '..'
|
||||
import FileDownload from './icons/FileDownload.svelte'
|
||||
|
||||
export let attachments: Attachment[]
|
||||
export let attachments: WithLookup<Attachment>[]
|
||||
let selectedFileNumber: number | undefined
|
||||
const myAccId = getCurrentAccount()._id
|
||||
const client = getClient()
|
||||
@ -57,7 +57,10 @@
|
||||
<AttachmentGalleryPresenter value={attachment}>
|
||||
<svelte:fragment slot="rowMenu">
|
||||
<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'} />
|
||||
</a>
|
||||
<div class="eAttachmentCellMenu" on:click={(event) => showFileMenu(event, attachment, i)}>
|
||||
|
@ -14,13 +14,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { Doc, getCurrentAccount } from '@hcengineering/core'
|
||||
import { getFileUrl, getClient } from '@hcengineering/presentation'
|
||||
import { Doc, getCurrentAccount, type WithLookup } from '@hcengineering/core'
|
||||
import { getFileUrl, getClient, getBlobHref } from '@hcengineering/presentation'
|
||||
import { Icon, IconMoreV, showPopup, Menu } from '@hcengineering/ui'
|
||||
import FileDownload from './icons/FileDownload.svelte'
|
||||
import { AttachmentPresenter } from '..'
|
||||
|
||||
export let attachments: Attachment[]
|
||||
export let attachments: WithLookup<Attachment>[]
|
||||
let selectedFileNumber: number | undefined
|
||||
const myAccId = getCurrentAccount()._id
|
||||
const client = getClient()
|
||||
@ -56,7 +56,7 @@
|
||||
<AttachmentPresenter value={attachment} />
|
||||
</div>
|
||||
<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'} />
|
||||
</a>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
@ -14,19 +14,20 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
import { getFileUrl } from '@hcengineering/presentation'
|
||||
import { getBlobHref, getFileUrl } from '@hcengineering/presentation'
|
||||
import { CircleButton, Progress } from '@hcengineering/ui'
|
||||
import Play from './icons/Play.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
|
||||
|
||||
let time = 0
|
||||
let duration = Number.POSITIVE_INFINITY
|
||||
let paused = true
|
||||
|
||||
function buttonClick () {
|
||||
function buttonClick (): void {
|
||||
paused = !paused
|
||||
}
|
||||
|
||||
@ -47,7 +48,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<style lang="scss">
|
||||
|
112
plugins/attachment-resources/src/components/MediaViewer.svelte
Normal file
112
plugins/attachment-resources/src/components/MediaViewer.svelte
Normal 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>
|
@ -15,10 +15,10 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 { FilePreviewPopup, createQuery, getClient, getFileUrl, uploadFile } from '@hcengineering/presentation'
|
||||
import { Button, IconAdd, Label, showPopup, Spinner } from '@hcengineering/ui'
|
||||
import { FilePreviewPopup, createQuery, getBlobHref, getClient, uploadFile } from '@hcengineering/presentation'
|
||||
import { Button, IconAdd, Label, Spinner, showPopup } from '@hcengineering/ui'
|
||||
import attachment from '../plugin'
|
||||
import UploadDuo from './icons/UploadDuo.svelte'
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
let inputFile: HTMLInputElement
|
||||
let loading = 0
|
||||
let images: Photo[] = []
|
||||
let images: WithLookup<Photo>[] = []
|
||||
|
||||
const client = getClient()
|
||||
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
|
||||
loading++
|
||||
try {
|
||||
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,
|
||||
file: uuid,
|
||||
type: file.type,
|
||||
@ -55,40 +55,44 @@
|
||||
lastModified: file.lastModified
|
||||
})
|
||||
} catch (err: any) {
|
||||
setPlatformStatus(unknownError(err))
|
||||
await setPlatformStatus(unknownError(err))
|
||||
} finally {
|
||||
loading--
|
||||
}
|
||||
}
|
||||
|
||||
function fileSelected () {
|
||||
function fileSelected (): void {
|
||||
const list = inputFile.files
|
||||
if (list === null || list.length === 0) return
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const file = list.item(index)
|
||||
if (file !== null) create(file)
|
||||
if (file !== null) {
|
||||
void create(file)
|
||||
}
|
||||
}
|
||||
inputFile.value = ''
|
||||
}
|
||||
|
||||
function fileDrop (e: DragEvent) {
|
||||
function fileDrop (e: DragEvent): void {
|
||||
const list = e.dataTransfer?.files
|
||||
if (list === undefined || list.length === 0) return
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const file = list.item(index)
|
||||
if (file !== null) create(file)
|
||||
if (file !== null) {
|
||||
void create(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' })
|
||||
if (item !== undefined) {
|
||||
showPopup(
|
||||
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'
|
||||
)
|
||||
} else {
|
||||
@ -145,7 +149,7 @@
|
||||
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>
|
||||
{/each}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
@ -15,9 +15,9 @@
|
||||
//
|
||||
|
||||
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 { getFileMetadata, uploadFile } from '@hcengineering/presentation'
|
||||
import { type Class, type TxOperations as Client, type Data, type Doc, type Ref, type Space } from '@hcengineering/core'
|
||||
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { getFileMetadata, uploadFile } from '@hcengineering/presentation'
|
||||
|
||||
import attachment from './plugin'
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
// 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 { IntlString, plugin, Resource } from '@hcengineering/platform'
|
||||
import type { Preference } from '@hcengineering/preference'
|
||||
@ -26,7 +26,7 @@ import { AnyComponent } from '@hcengineering/ui'
|
||||
*/
|
||||
export interface Attachment extends AttachedDoc {
|
||||
name: string
|
||||
file: string
|
||||
file: Ref<Blob>
|
||||
size: number
|
||||
type: string
|
||||
lastModified: number
|
||||
@ -78,7 +78,7 @@ export default plugin(attachmentId, {
|
||||
SavedAttachments: '' as Ref<Class<SavedAttachments>>
|
||||
},
|
||||
helper: {
|
||||
UploadFile: '' as Resource<(file: File) => Promise<string>>,
|
||||
UploadFile: '' as Resource<(file: File) => Promise<Ref<Blob>>>,
|
||||
DeleteFile: '' as Resource<(id: string) => Promise<void>>
|
||||
},
|
||||
string: {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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, {
|
||||
Account,
|
||||
AccountRole,
|
||||
@ -21,7 +21,8 @@ import core, {
|
||||
Timestamp,
|
||||
TxOperations,
|
||||
TxProcessor,
|
||||
WithLookup
|
||||
WithLookup,
|
||||
type Blob as PlatformBlob
|
||||
} from '@hcengineering/core'
|
||||
import gmail, { Message } from '@hcengineering/gmail'
|
||||
import recruit from '@hcengineering/recruit'
|
||||
@ -242,7 +243,7 @@ export async function syncDocument (
|
||||
body: data
|
||||
})
|
||||
if (resp.status === 200) {
|
||||
const uuid = await resp.text()
|
||||
const uuid = (await resp.text()) as Ref<PlatformBlob>
|
||||
|
||||
ed.file = uuid
|
||||
ed._id = attachmentId as Ref<Attachment & BitrixSyncDoc>
|
||||
@ -812,7 +813,7 @@ async function downloadComments (
|
||||
attachedToClass: c._class,
|
||||
bitrixId: `attach-${v.id}`,
|
||||
collection: 'attachments',
|
||||
file: '',
|
||||
file: '' as Ref<PlatformBlob>,
|
||||
lastModified: Date.now(),
|
||||
modifiedBy: userList.get(it.AUTHOR_ID) ?? core.account.System,
|
||||
modifiedOn: new Date(it.CREATED ?? new Date().toString()).getTime(),
|
||||
@ -941,7 +942,8 @@ async function synchronizeUsers (
|
||||
if (accountId === undefined) {
|
||||
const employeeId = await ops.client.createDoc(contact.class.Person, contact.space.Contacts, {
|
||||
name: combineName(u.NAME, u.LAST_NAME),
|
||||
avatar: u.PERSONAL_PHOTO,
|
||||
avatarType: AvatarType.EXTERNAL,
|
||||
avatarProps: { url: u.PERSONAL_PHOTO },
|
||||
city: u.PERSONAL_CITY
|
||||
})
|
||||
await ops.client.createMixin(employeeId, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, {
|
||||
|
@ -14,7 +14,8 @@ import core, {
|
||||
Space,
|
||||
TxOperations,
|
||||
WithLookup,
|
||||
generateId
|
||||
generateId,
|
||||
type Blob as PlatformBlob
|
||||
} from '@hcengineering/core'
|
||||
import { Message } from '@hcengineering/gmail'
|
||||
import recruit, { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
|
||||
@ -561,7 +562,7 @@ export async function convert (
|
||||
const attachDoc: Attachment & BitrixSyncDoc = {
|
||||
_id: generateId(),
|
||||
bitrixId: `${blobRef.id}`,
|
||||
file: '', // Empty since not uploaded yet.
|
||||
file: '' as Ref<PlatformBlob>, // Empty since not uploaded yet.
|
||||
name: blobRef.id,
|
||||
size: -1,
|
||||
type: 'application/octet-stream',
|
||||
|
@ -45,7 +45,7 @@
|
||||
on:click={() => onClick(p)}
|
||||
>
|
||||
<div class="icon">
|
||||
<Avatar size={'x-small'} avatar={p.avatar} name={p.name} />
|
||||
<Avatar size={'x-small'} person={p} name={p.name} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -55,7 +55,7 @@
|
||||
|
||||
{#if persons.length === 1}
|
||||
<Avatar
|
||||
avatar={persons[0].avatar}
|
||||
person={persons[0]}
|
||||
size={avatarSize}
|
||||
name={persons[0].name}
|
||||
{showStatus}
|
||||
@ -66,7 +66,7 @@
|
||||
{#if persons.length > 1 && size === 'medium'}
|
||||
<div class="group">
|
||||
{#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}
|
||||
{#if persons.length > visiblePersons}
|
||||
<div class="rect">
|
||||
|
@ -17,8 +17,8 @@
|
||||
import attachment, { Attachment } from '@hcengineering/attachment'
|
||||
import { AttachmentPresenter, FileDownload } from '@hcengineering/attachment-resources'
|
||||
import { ChunterSpace } from '@hcengineering/chunter'
|
||||
import { Doc, SortingOrder, getCurrentAccount } from '@hcengineering/core'
|
||||
import { createQuery, getClient, getFileUrl } from '@hcengineering/presentation'
|
||||
import { Doc, SortingOrder, getCurrentAccount, type WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getBlobHref, getClient } from '@hcengineering/presentation'
|
||||
import { Icon, IconMoreV, Label, Menu, getCurrentResolvedLocation, navigate, showPopup } from '@hcengineering/ui'
|
||||
|
||||
export let channel: ChunterSpace | undefined
|
||||
@ -26,7 +26,7 @@
|
||||
const client = getClient()
|
||||
|
||||
const query = createQuery()
|
||||
let visibleAttachments: Attachment[] | undefined
|
||||
let visibleAttachments: WithLookup<Attachment>[] | undefined
|
||||
let totalAttachments = 0
|
||||
const ATTACHEMNTS_LIMIT = 5
|
||||
let selectedRowNumber: number | undefined
|
||||
@ -83,7 +83,10 @@
|
||||
<AttachmentPresenter value={attachment} />
|
||||
</div>
|
||||
<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'} />
|
||||
</a>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
@ -13,13 +13,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts" context="module">
|
||||
import contact, { AvatarProvider } from '@hcengineering/contact'
|
||||
import { Client, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import contact, { AvatarProvider, getAvatarColorForId, type AvatarInfo } from '@hcengineering/contact'
|
||||
import { Ref, type Data, type WithLookup } from '@hcengineering/core'
|
||||
import { getClient, sizeToWidth } from '@hcengineering/presentation'
|
||||
|
||||
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)
|
||||
if (p !== undefined) {
|
||||
return p ?? undefined
|
||||
@ -31,7 +31,8 @@
|
||||
</script>
|
||||
|
||||
<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 { getBlobURL, reduceCalls } from '@hcengineering/presentation'
|
||||
import {
|
||||
@ -44,11 +45,10 @@
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { onMount } from 'svelte'
|
||||
import { Account } from '@hcengineering/core'
|
||||
import AvatarInstance from './AvatarInstance.svelte'
|
||||
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 direct: Blob | undefined = undefined
|
||||
export let size: IconSize
|
||||
@ -63,7 +63,9 @@
|
||||
avatarInst.pulse()
|
||||
}
|
||||
|
||||
let url: string[] | undefined
|
||||
let url: string | undefined
|
||||
let srcSet: string | undefined
|
||||
|
||||
let avatarProvider: AvatarProvider | undefined
|
||||
let color: ColorDefinition | undefined = undefined
|
||||
let element: HTMLElement
|
||||
@ -86,26 +88,28 @@
|
||||
|
||||
const update = reduceCalls(async function (
|
||||
size: IconSize,
|
||||
avatar?: string | null,
|
||||
avatar?: Data<WithLookup<AvatarInfo>>,
|
||||
direct?: Blob,
|
||||
name?: string | null
|
||||
) {
|
||||
const width = sizeToWidth(size)
|
||||
if (direct !== undefined) {
|
||||
const blobURL = await getBlobURL(direct)
|
||||
url = [blobURL]
|
||||
url = blobURL
|
||||
avatarProvider = undefined
|
||||
} else if (avatar) {
|
||||
const avatarProviderId = getAvatarProviderId(avatar)
|
||||
avatarProvider = avatarProviderId && (await getProvider(getClient(), avatarProviderId))
|
||||
} else if (avatar != null) {
|
||||
const avatarProviderId = getAvatarProviderId(avatar.avatarType)
|
||||
avatarProvider = avatarProviderId !== undefined ? await getProvider(avatarProviderId) : undefined
|
||||
|
||||
if (!avatarProvider || avatarProvider.type === AvatarType.COLOR) {
|
||||
if (avatarProvider === undefined) {
|
||||
url = undefined
|
||||
color = getPlatformAvatarColorByName(avatar.split('://')[1], $themeStore.dark)
|
||||
} else if (avatarProvider?.type === AvatarType.IMAGE) {
|
||||
url = (await getResource(avatarProvider.getUrl))(avatar, size)
|
||||
color = getPlatformAvatarColorByName(
|
||||
avatar.avatarProps?.color ?? getAvatarColorForId(displayName),
|
||||
$themeStore.dark
|
||||
)
|
||||
} else {
|
||||
const uri = avatar.split('://')[1]
|
||||
url = (await getResource(avatarProvider.getUrl))(uri, size)
|
||||
;({ url, srcSet, color } = (await getResource(avatarProvider.getUrl))(avatar, displayName, width))
|
||||
console.log(url, srcSet, color)
|
||||
}
|
||||
} else if (name != null) {
|
||||
color = getPlatformAvatarColorForTextDef(name, $themeStore.dark)
|
||||
@ -116,15 +120,13 @@
|
||||
avatarProvider = undefined
|
||||
}
|
||||
})
|
||||
$: void update(size, avatar, direct, name)
|
||||
|
||||
$: srcset = url?.slice(1)?.join(', ')
|
||||
$: void update(size, person, direct, name)
|
||||
|
||||
onMount(() => {
|
||||
loadUsersStatus()
|
||||
})
|
||||
|
||||
$: userStatus = account ? $statusByUserStore.get(account) : undefined
|
||||
$: userStatus = account !== undefined ? $statusByUserStore.get(account) : undefined
|
||||
</script>
|
||||
|
||||
{#if showStatus && account}
|
||||
@ -132,7 +134,7 @@
|
||||
<AvatarInstance
|
||||
bind:this={avatarInst}
|
||||
{url}
|
||||
{srcset}
|
||||
srcset={srcSet}
|
||||
{displayName}
|
||||
{size}
|
||||
{icon}
|
||||
@ -155,7 +157,7 @@
|
||||
<AvatarInstance
|
||||
bind:this={avatarInst}
|
||||
{url}
|
||||
{srcset}
|
||||
srcset={srcSet}
|
||||
{displayName}
|
||||
{size}
|
||||
{icon}
|
||||
|
@ -17,7 +17,7 @@
|
||||
import { AnySvelteComponent, ColorDefinition, Icon, IconSize, resizeObserver } from '@hcengineering/ui'
|
||||
import AvatarIcon from './icons/Avatar.svelte'
|
||||
|
||||
export let url: string[] | undefined
|
||||
export let url: string | undefined
|
||||
export let srcset: string | undefined
|
||||
export let displayName: string
|
||||
export let size: IconSize
|
||||
@ -86,7 +86,7 @@
|
||||
style:background-color={color && !url ? color.icon : 'var(--theme-button-default)'}
|
||||
>
|
||||
{#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 !== ''}
|
||||
<div
|
||||
class="ava-text"
|
||||
|
51
plugins/contact-resources/src/components/AvatarRef.svelte
Normal file
51
plugins/contact-resources/src/components/AvatarRef.svelte
Normal 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} />
|
@ -39,5 +39,5 @@
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
|
@ -57,7 +57,7 @@
|
||||
{/if}
|
||||
{#each persons as person, i}
|
||||
<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>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 login from '@hcengineering/login'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
@ -44,7 +44,8 @@
|
||||
const person: Data<Employee> = {
|
||||
name: '',
|
||||
city: '',
|
||||
active: true
|
||||
active: true,
|
||||
avatarType: AvatarType.COLOR
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -56,7 +57,10 @@
|
||||
changeEmail()
|
||||
const name = combineName(firstName, lastName)
|
||||
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.createMixin(id, contact.class.Person, contact.space.Contacts, contact.mixin.Employee, {
|
||||
@ -179,7 +183,7 @@
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<EditableAvatar
|
||||
avatar={person.avatar}
|
||||
{person}
|
||||
name={combineName(firstName, lastName)}
|
||||
{email}
|
||||
size={'large'}
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 login from '@hcengineering/login'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
@ -48,7 +48,8 @@
|
||||
const name = combineName(firstName, lastName)
|
||||
const person: Data<Person> = {
|
||||
name,
|
||||
city: ''
|
||||
city: '',
|
||||
avatarType: AvatarType.COLOR
|
||||
}
|
||||
|
||||
await client.createDoc(contact.class.Person, contact.space.Contacts, person, id)
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 { Card, getClient } from '@hcengineering/presentation'
|
||||
import { createFocusManager, EditBox, FocusHandler, IconInfo, Label } from '@hcengineering/ui'
|
||||
@ -42,10 +42,14 @@
|
||||
async function createPerson () {
|
||||
const person: Data<Person> = {
|
||||
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)
|
||||
|
||||
@ -115,12 +119,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<EditableAvatar
|
||||
avatar={object.avatar}
|
||||
name={combineName(firstName, lastName)}
|
||||
size={'large'}
|
||||
bind:this={avatarEditor}
|
||||
/>
|
||||
<EditableAvatar person={object} name={combineName(firstName, lastName)} size={'large'} bind:this={avatarEditor} />
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="pool">
|
||||
|
@ -92,9 +92,7 @@
|
||||
await avatarEditor.removeAvatar(object.avatar)
|
||||
}
|
||||
const avatar = await avatarEditor.createAvatar()
|
||||
await client.update(object, {
|
||||
avatar
|
||||
})
|
||||
await client.diffUpdate(object, avatar)
|
||||
}
|
||||
|
||||
const manager = createFocusManager()
|
||||
@ -108,7 +106,7 @@
|
||||
{#key object}
|
||||
{#if editable}
|
||||
<EditableAvatar
|
||||
avatar={object.avatar}
|
||||
person={object}
|
||||
{email}
|
||||
size={'x-large'}
|
||||
name={object.name}
|
||||
@ -116,7 +114,7 @@
|
||||
on:done={onAvatarDone}
|
||||
/>
|
||||
{:else}
|
||||
<Avatar avatar={object.avatar} size={'x-large'} name={object.name} />
|
||||
<Avatar person={object} size={'x-large'} name={object.name} />
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
|
@ -70,9 +70,7 @@
|
||||
await avatarEditor.removeAvatar(object.avatar)
|
||||
}
|
||||
const avatar = await avatarEditor.createAvatar()
|
||||
await client.update(object, {
|
||||
avatar
|
||||
})
|
||||
await client.diffUpdate(object, avatar)
|
||||
}
|
||||
|
||||
const manager = createFocusManager()
|
||||
@ -86,7 +84,7 @@
|
||||
{#key object}
|
||||
<EditableAvatar
|
||||
disabled={readonly}
|
||||
avatar={object.avatar}
|
||||
person={object}
|
||||
size={'x-large'}
|
||||
name={object.name}
|
||||
bind:this={avatarEditor}
|
||||
|
@ -14,22 +14,16 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment from '@hcengineering/attachment'
|
||||
import { AvatarType } from '@hcengineering/contact'
|
||||
import { AvatarType, type AvatarInfo } from '@hcengineering/contact'
|
||||
import { Asset, getResource } from '@hcengineering/platform'
|
||||
import { uploadFile } from '@hcengineering/presentation'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
IconSize,
|
||||
getPlatformAvatarColorForTextDef,
|
||||
showPopup,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { AnySvelteComponent, IconSize, showPopup } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import type { Data, Blob as PlatformBlob, Ref, WithLookup } from '@hcengineering/core'
|
||||
import AvatarComponent from './Avatar.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 email: string | undefined = undefined
|
||||
export let size: IconSize
|
||||
@ -39,30 +33,24 @@
|
||||
export let imageOnly: 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
|
||||
let selectedAvatar: string | null | undefined
|
||||
$: selectedAvatarType = avatar?.includes('://')
|
||||
? (schema as AvatarType)
|
||||
: avatar === undefined
|
||||
? 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<Data<AvatarInfo>> {
|
||||
const result: Data<AvatarInfo> = {
|
||||
avatarType: selectedAvatarType,
|
||||
avatarProps: selectedAvatarProps,
|
||||
avatar: selectedAvatar
|
||||
}
|
||||
|
||||
export async function createAvatar (): Promise<string | undefined> {
|
||||
if (selectedAvatarType === AvatarType.IMAGE && direct !== undefined) {
|
||||
const uploadFile = await getResource(attachment.helper.UploadFile)
|
||||
const file = new File([direct], 'avatar', { type: direct.type })
|
||||
|
||||
return await uploadFile(file)
|
||||
}
|
||||
if (selectedAvatarType != null && selectedAvatar) {
|
||||
return `${selectedAvatarType}://${selectedAvatar}`
|
||||
result.avatar = await uploadFile(file)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
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
|
||||
selectedAvatar = submittedAvatar
|
||||
selectedAvatarProps = submittedProps
|
||||
direct = submittedDirect
|
||||
avatar = selectedAvatarType === AvatarType.IMAGE ? selectedAvatar : `${selectedAvatarType}://${selectedAvatar}`
|
||||
dispatch('done')
|
||||
}
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -84,12 +77,10 @@
|
||||
async function showSelectionPopup (e: MouseEvent) {
|
||||
if (!disabled) {
|
||||
showPopup(SelectAvatarPopup, {
|
||||
avatar:
|
||||
selectedAvatarType === AvatarType.IMAGE
|
||||
? selectedAvatar
|
||||
: selectedAvatarType === AvatarType.COLOR && avatar == null
|
||||
? undefined
|
||||
: `${selectedAvatarType}://${selectedAvatar}`,
|
||||
avatar: selectedAvatar,
|
||||
selectedAvatarType,
|
||||
selectedAvatarProps,
|
||||
selectedAvatar,
|
||||
email,
|
||||
name,
|
||||
file: direct,
|
||||
@ -109,11 +100,11 @@
|
||||
{direct}
|
||||
{size}
|
||||
{icon}
|
||||
avatar={selectedAvatarType === AvatarType.IMAGE
|
||||
? selectedAvatar
|
||||
: selectedAvatarType === AvatarType.COLOR && avatar == null
|
||||
? undefined
|
||||
: `${selectedAvatarType}://${selectedAvatar}`}
|
||||
person={{
|
||||
avatarType: selectedAvatarType,
|
||||
avatarProps: selectedAvatarProps,
|
||||
avatar: selectedAvatar
|
||||
}}
|
||||
{name}
|
||||
/>
|
||||
</div>
|
||||
|
@ -60,7 +60,7 @@
|
||||
>
|
||||
{#if employee}
|
||||
<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 class="pb-2">{getName(client.getHierarchy(), employee)}</div>
|
||||
<DocNavLink object={employee}>
|
||||
|
@ -107,7 +107,7 @@
|
||||
if (_update.avatar !== undefined || sourcePerson.avatar === targetPerson.avatar) {
|
||||
// We replace avatar, we need to update source with target
|
||||
await client.update(sourcePerson, {
|
||||
avatar: sourcePerson.avatar === targetPerson.avatar ? '' : targetPerson.avatar
|
||||
avatar: sourcePerson.avatar === targetPerson.avatar ? null : targetPerson.avatar
|
||||
})
|
||||
}
|
||||
await client.update(targetPerson, _update)
|
||||
@ -360,7 +360,7 @@
|
||||
selected={update.avatar !== undefined}
|
||||
>
|
||||
<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>
|
||||
</MergeComparer>
|
||||
<MergeComparer
|
||||
|
@ -41,7 +41,7 @@
|
||||
<div class="antiContactCard">
|
||||
<div class="label uppercase"><Label label={contact.string.Organization} /></div>
|
||||
<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>
|
||||
{#if organization}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
@ -42,7 +42,7 @@
|
||||
<div class="antiContactCard">
|
||||
<div class="label uppercase"><Label label={contact.string.Person} /></div>
|
||||
<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>
|
||||
{#if object}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
|
@ -63,7 +63,7 @@
|
||||
class:mr-2={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>
|
||||
{/if}
|
||||
{#if shouldShowName}
|
||||
|
@ -22,5 +22,5 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<Avatar avatar={value.avatar} {size} name={value.name} />
|
||||
<Avatar person={value} {size} name={value.name} />
|
||||
{/if}
|
||||
|
@ -15,62 +15,56 @@
|
||||
<script lang="ts">
|
||||
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 presentation, { Card, getFileUrl } from '@hcengineering/presentation'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
ColorDefinition,
|
||||
Label,
|
||||
showPopup,
|
||||
TabList,
|
||||
eventToHTMLElement,
|
||||
getPlatformAvatarColorForTextDef,
|
||||
getPlatformAvatarColorByName,
|
||||
getPlatformAvatarColorForTextDef,
|
||||
getPlatformAvatarColors,
|
||||
ColorDefinition,
|
||||
showPopup,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { ColorsPopup } from '@hcengineering/view-resources'
|
||||
import presentation, { Card, getFileUrl } from '@hcengineering/presentation'
|
||||
import contact from '../plugin'
|
||||
import { getAvatarTypeDropdownItems } from '../utils'
|
||||
import AvatarComponent from './Avatar.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 email: string | undefined
|
||||
export let file: Blob | undefined
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let imageOnly: 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)
|
||||
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 hasGravatar = false
|
||||
@ -82,38 +76,43 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function submit () {
|
||||
onSubmit(selectedAvatarType, selectedAvatar, selectedAvatarType === AvatarType.IMAGE ? selectedFile : undefined)
|
||||
onSubmit(
|
||||
selectedAvatarType,
|
||||
selectedAvatar,
|
||||
selectedAvatarProps,
|
||||
selectedAvatarType === AvatarType.IMAGE ? selectedFile : undefined
|
||||
)
|
||||
}
|
||||
let inputRef: HTMLInputElement
|
||||
const targetMimes = ['image/png', 'image/jpg', 'image/jpeg']
|
||||
|
||||
function handleDropdownSelection (e: any) {
|
||||
if (selectedAvatarType === AvatarType.GRAVATAR && email) {
|
||||
selectedAvatar = buildGravatarId(email)
|
||||
selectedAvatarProps = { url: buildGravatarId(email) }
|
||||
} else if (selectedAvatarType === AvatarType.IMAGE) {
|
||||
if (selectedFile) {
|
||||
return
|
||||
}
|
||||
if (file) {
|
||||
selectedFile = file
|
||||
} else if (avatar && !avatar.includes('://')) {
|
||||
selectedAvatar = avatar
|
||||
} else {
|
||||
selectedAvatar = ''
|
||||
selectedAvatar = undefined
|
||||
inputRef.click()
|
||||
}
|
||||
} 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
|
||||
|
||||
if (selectedFile !== undefined) {
|
||||
editableFile = selectedFile
|
||||
} else if (selectedAvatar && !(imageOnly && selectedAvatar === initialSelectedAvatar)) {
|
||||
const url = getFileUrl(selectedAvatar, 'full')
|
||||
const url = getFileUrl(selectedAvatar)
|
||||
editableFile = await (await fetch(url)).blob()
|
||||
} else {
|
||||
inputRef.click()
|
||||
@ -122,18 +121,21 @@
|
||||
if (editableFile.size > 0) showCropper(editableFile)
|
||||
}
|
||||
|
||||
function showCropper (editableFile: Blob) {
|
||||
function showCropper (editableFile: Blob): void {
|
||||
showPopup(EditAvatarPopup, { file: editableFile, lessCrop }, undefined, (blob) => {
|
||||
if (blob === undefined) {
|
||||
if (!selectedFile && (!avatar || avatar.includes('://'))) {
|
||||
if (!selectedFile && !initialSelectedAvatar) {
|
||||
selectedAvatarType = AvatarType.COLOR
|
||||
selectedAvatar = getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name
|
||||
selectedAvatarProps = { color: getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name }
|
||||
}
|
||||
return
|
||||
}
|
||||
if (blob === null) {
|
||||
selectedAvatarType = AvatarType.COLOR
|
||||
selectedAvatar = imageOnly ? '' : getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name
|
||||
selectedAvatar = undefined
|
||||
selectedAvatarProps = {
|
||||
color: imageOnly ? '' : getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name
|
||||
}
|
||||
selectedFile = undefined
|
||||
} else {
|
||||
selectedFile = blob
|
||||
@ -142,7 +144,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
function onSelectFile (e: any) {
|
||||
function onSelectFile (e: any): void {
|
||||
const targetFile = e.target?.files[0] as File | undefined
|
||||
|
||||
if (targetFile === undefined || !targetMimes.includes(targetFile.type)) {
|
||||
@ -153,13 +155,14 @@
|
||||
document.body.onfocus = null
|
||||
}
|
||||
|
||||
function handleFileSelectionCancel () {
|
||||
function handleFileSelectionCancel (): void {
|
||||
document.body.onfocus = null
|
||||
|
||||
if (!inputRef.value.length) {
|
||||
if (!selectedFile) {
|
||||
selectedAvatarType = AvatarType.COLOR
|
||||
selectedAvatar = getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name
|
||||
selectedAvatar = undefined
|
||||
selectedAvatarProps = { color: getPlatformAvatarColorForTextDef(name ?? '', $themeStore.dark).name }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -170,14 +173,14 @@
|
||||
{
|
||||
colors,
|
||||
columns: 6,
|
||||
selected: getPlatformAvatarColorByName(selectedAvatar, $themeStore.dark),
|
||||
selected: getPlatformAvatarColorByName(selectedAvatarProps?.color ?? '', $themeStore.dark),
|
||||
key: 'icon'
|
||||
},
|
||||
eventToHTMLElement(event),
|
||||
(col) => {
|
||||
if (col != null) {
|
||||
color = colors[col]
|
||||
selectedAvatar = color.name
|
||||
selectedAvatarProps = { color: color.name }
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -189,10 +192,10 @@
|
||||
okLabel={presentation.string.Save}
|
||||
width={'x-small'}
|
||||
accentHeader
|
||||
canSave={selectedAvatarType !== initialSelectedType ||
|
||||
canSave={selectedAvatarType !== initialSelectedAvatarType ||
|
||||
selectedAvatar !== initialSelectedAvatar ||
|
||||
selectedFile !== file ||
|
||||
!avatar}
|
||||
JSON.stringify(initialSelectedAvatarProps) !== JSON.stringify(selectedAvatarProps) ||
|
||||
selectedFile !== file}
|
||||
okAction={submit}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
@ -214,11 +217,11 @@
|
||||
}}
|
||||
>
|
||||
<AvatarComponent
|
||||
avatar={selectedAvatarType === AvatarType.IMAGE
|
||||
? selectedAvatar === ''
|
||||
? `${AvatarType.COLOR}://${color?.color}`
|
||||
: selectedAvatar
|
||||
: `${selectedAvatarType}://${selectedAvatar}`}
|
||||
person={{
|
||||
avatarType: selectedAvatarType,
|
||||
avatar: selectedAvatar,
|
||||
avatarProps: selectedAvatarProps
|
||||
}}
|
||||
direct={selectedAvatarType === AvatarType.IMAGE ? selectedFile : undefined}
|
||||
size={'2x-large'}
|
||||
{icon}
|
||||
|
@ -37,7 +37,7 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex-row-center" on:click>
|
||||
<Avatar
|
||||
avatar={person.avatar}
|
||||
{person}
|
||||
size={avatarSize}
|
||||
name={person.name}
|
||||
on:accent-color
|
||||
|
@ -38,7 +38,7 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<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}>
|
||||
{#if subtitle}<div class="content-dark-color text-sm">{subtitle}</div>{/if}
|
||||
<div class="label text-left">{getName(client.getHierarchy(), value)}</div>
|
||||
|
@ -14,33 +14,47 @@
|
||||
// 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 {
|
||||
type Class,
|
||||
type Client,
|
||||
type Data,
|
||||
type DocumentQuery,
|
||||
type Ref,
|
||||
type RelatedDocument,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
import { type IntlString, type Resources, getResource } from '@hcengineering/platform'
|
||||
import { MessageBox, type ObjectSearchResult, getClient, getFileUrl } from '@hcengineering/presentation'
|
||||
import { getResource, type IntlString, type Resources } from '@hcengineering/platform'
|
||||
import { MessageBox, getBlobHref, getBlobSrcSet, getClient, type ObjectSearchResult } from '@hcengineering/presentation'
|
||||
import {
|
||||
getPlatformAvatarColorByName,
|
||||
getPlatformAvatarColorForTextDef,
|
||||
getPlatformColorDef,
|
||||
hexColorToNumber,
|
||||
parseURL,
|
||||
showPopup,
|
||||
themeStore,
|
||||
type AnyComponent,
|
||||
type AnySvelteComponent,
|
||||
type IconSize,
|
||||
type TooltipAlignment,
|
||||
getIconSize2x,
|
||||
parseURL,
|
||||
showPopup
|
||||
type ColorDefinition,
|
||||
type TooltipAlignment
|
||||
} from '@hcengineering/ui'
|
||||
import AccountArrayEditor from './components/AccountArrayEditor.svelte'
|
||||
import AccountBox from './components/AccountBox.svelte'
|
||||
import AssigneeBox from './components/AssigneeBox.svelte'
|
||||
import AssigneePopup from './components/AssigneePopup.svelte'
|
||||
import Avatar from './components/Avatar.svelte'
|
||||
import AvatarRef from './components/AvatarRef.svelte'
|
||||
import ChannelFilter from './components/ChannelFilter.svelte'
|
||||
import ChannelIcon from './components/ChannelIcon.svelte'
|
||||
import ChannelPanel from './components/ChannelPanel.svelte'
|
||||
import ChannelPresenter from './components/ChannelPresenter.svelte'
|
||||
import Channels from './components/Channels.svelte'
|
||||
@ -48,6 +62,7 @@ import ChannelsDropdown from './components/ChannelsDropdown.svelte'
|
||||
import ChannelsEditor from './components/ChannelsEditor.svelte'
|
||||
import ChannelsPresenter from './components/ChannelsPresenter.svelte'
|
||||
import ChannelsView from './components/ChannelsView.svelte'
|
||||
import CollaborationUserAvatar from './components/CollaborationUserAvatar.svelte'
|
||||
import CombineAvatars from './components/CombineAvatars.svelte'
|
||||
import ContactArrayEditor from './components/ContactArrayEditor.svelte'
|
||||
import ContactPresenter from './components/ContactPresenter.svelte'
|
||||
@ -55,18 +70,16 @@ import ContactRefPresenter from './components/ContactRefPresenter.svelte'
|
||||
import Contacts from './components/Contacts.svelte'
|
||||
import ContactsTabs from './components/ContactsTabs.svelte'
|
||||
import CreateEmployee from './components/CreateEmployee.svelte'
|
||||
import CreateGuest from './components/CreateGuest.svelte'
|
||||
import CreateOrganization from './components/CreateOrganization.svelte'
|
||||
import CreatePerson from './components/CreatePerson.svelte'
|
||||
import CollaborationUserAvatar from './components/CollaborationUserAvatar.svelte'
|
||||
import DeleteConfirmationPopup from './components/DeleteConfirmationPopup.svelte'
|
||||
import EditEmployee from './components/EditEmployee.svelte'
|
||||
import EditMember from './components/EditMember.svelte'
|
||||
import EditOrganization from './components/EditOrganization.svelte'
|
||||
import EditOrganizationPanel from './components/EditOrganizationPanel.svelte'
|
||||
import EditPerson from './components/EditPerson.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 EmployeeBox from './components/EmployeeBox.svelte'
|
||||
import EmployeeBrowser from './components/EmployeeBrowser.svelte'
|
||||
@ -82,33 +95,34 @@ import MembersPresenter from './components/MembersPresenter.svelte'
|
||||
import MergePersons from './components/MergePersons.svelte'
|
||||
import OrganizationEditor from './components/OrganizationEditor.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 PersonIcon from './components/PersonIcon.svelte'
|
||||
import PersonPresenter from './components/PersonPresenter.svelte'
|
||||
import PersonRefPresenter from './components/PersonRefPresenter.svelte'
|
||||
import SelectAvatars from './components/SelectAvatars.svelte'
|
||||
import SelectUsersPopup from './components/SelectUsersPopup.svelte'
|
||||
import SocialEditor from './components/SocialEditor.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 UserBoxItems from './components/UserBoxItems.svelte'
|
||||
import UserBoxList from './components/UserBoxList.svelte'
|
||||
import UserDetails from './components/UserDetails.svelte'
|
||||
import UserInfo from './components/UserInfo.svelte'
|
||||
import UsersList from './components/UsersList.svelte'
|
||||
import UsersPopup from './components/UsersPopup.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 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 {
|
||||
channelIdentifierProvider,
|
||||
@ -133,53 +147,54 @@ import {
|
||||
export * from './utils'
|
||||
export { employeeByIdStore, employeesStore } from './utils'
|
||||
export {
|
||||
Channels,
|
||||
ChannelsEditor,
|
||||
ContactRefPresenter,
|
||||
ContactPresenter,
|
||||
ChannelsView,
|
||||
ChannelsDropdown,
|
||||
EmployeePresenter,
|
||||
PersonPresenter,
|
||||
OrganizationPresenter,
|
||||
EmployeeBrowser,
|
||||
MemberPresenter,
|
||||
EmployeeArrayEditor,
|
||||
EmployeeEditor,
|
||||
PersonAccountRefPresenter,
|
||||
PersonAccountPresenter,
|
||||
MembersPresenter,
|
||||
EditPerson,
|
||||
EmployeeRefPresenter,
|
||||
AccountArrayEditor,
|
||||
AccountBox,
|
||||
CreateOrganization,
|
||||
ExpandRightDouble,
|
||||
EditableAvatar,
|
||||
UserBox,
|
||||
AssigneeBox,
|
||||
AssigneePopup,
|
||||
Avatar,
|
||||
UsersPopup,
|
||||
EmployeeBox,
|
||||
UserBoxList,
|
||||
Members,
|
||||
SpaceMembers,
|
||||
AvatarRef,
|
||||
Channels,
|
||||
ChannelsDropdown,
|
||||
ChannelsEditor,
|
||||
ChannelsView,
|
||||
CombineAvatars,
|
||||
UserInfo,
|
||||
IconMembers,
|
||||
SelectAvatars,
|
||||
UserBoxItems,
|
||||
MembersBox,
|
||||
PersonRefPresenter,
|
||||
SystemAvatar,
|
||||
PersonIcon,
|
||||
UsersList,
|
||||
SelectUsersPopup,
|
||||
IconAddMember,
|
||||
UserDetails,
|
||||
ContactPresenter,
|
||||
ContactRefPresenter,
|
||||
CreateGuest,
|
||||
CreateOrganization,
|
||||
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 => ({
|
||||
@ -287,6 +302,18 @@ export interface PersonLabelTooltip {
|
||||
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> => ({
|
||||
actionImpl: {
|
||||
KickEmployee: kickEmployee,
|
||||
@ -330,6 +357,7 @@ export default async (): Promise<Resources> => ({
|
||||
ChannelFilter,
|
||||
MergePersons,
|
||||
Avatar,
|
||||
AvatarRef,
|
||||
UserBoxList,
|
||||
ChannelPresenter,
|
||||
ChannelPanel,
|
||||
@ -362,19 +390,31 @@ export default async (): Promise<Resources> => ({
|
||||
) => await queryContact(contact.class.Organization, client, query, filter)
|
||||
},
|
||||
function: {
|
||||
GetFileUrl: (file: string, size: IconSize, fileName?: string) => {
|
||||
return [
|
||||
getFileUrl(file, size, fileName),
|
||||
getFileUrl(file, size, fileName) + ' 1x',
|
||||
getFileUrl(file, getIconSize2x(size), fileName) + ' 2x'
|
||||
]
|
||||
GetFileUrl: (person: Data<WithLookup<AvatarInfo>>, name: string, width: number) => {
|
||||
if (person.avatar == null) {
|
||||
return {
|
||||
color: getPersonColor(person, name)
|
||||
}
|
||||
}
|
||||
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(file, size),
|
||||
getGravatarUrl(file, size) + ' 1x',
|
||||
getGravatarUrl(file, getIconSize2x(size)) + ' 2x'
|
||||
],
|
||||
GetColorUrl: (uri: string) => [uri],
|
||||
GetGravatarUrl: (person: Data<WithLookup<AvatarInfo>>, name: string, width: number) => ({
|
||||
url: person.avatarProps?.url !== undefined ? getGravatarUrl(person.avatarProps?.url, width) : undefined,
|
||||
srcset:
|
||||
person.avatarProps?.url !== undefined
|
||||
? `${getGravatarUrl(person.avatarProps?.url, width)} 1x, ${getGravatarUrl(person.avatarProps?.url, width * 2)} 2x`
|
||||
: 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,
|
||||
FilterChannelInResult: filterChannelInResult,
|
||||
FilterChannelNinResult: filterChannelNinResult,
|
||||
|
@ -31,6 +31,7 @@ import {
|
||||
import core, {
|
||||
getCurrentAccount,
|
||||
toIdMap,
|
||||
type Account,
|
||||
type Class,
|
||||
type Client,
|
||||
type Doc,
|
||||
@ -39,8 +40,8 @@ import core, {
|
||||
type Ref,
|
||||
type Timestamp,
|
||||
type TxOperations,
|
||||
type Account,
|
||||
type UserStatus
|
||||
type UserStatus,
|
||||
type WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
|
||||
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 employeesStore = writable<Employee[]>([])
|
||||
export const employeeByIdStore = writable<IdMap<WithLookup<Employee>>>(new Map())
|
||||
export const employeesStore = writable<Array<WithLookup<Employee>>>([])
|
||||
|
||||
export const personAccountByIdStore = writable<IdMap<PersonAccount>>(new Map())
|
||||
|
||||
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())
|
||||
|
||||
@ -311,10 +312,19 @@ function fillStores (): void {
|
||||
const accountPersonQuery = createQuery(true)
|
||||
|
||||
const query = createQuery(true)
|
||||
query.query(contact.mixin.Employee, {}, (res) => {
|
||||
employeesStore.set(res)
|
||||
employeeByIdStore.set(toIdMap(res))
|
||||
})
|
||||
query.query(
|
||||
contact.mixin.Employee,
|
||||
{},
|
||||
(res) => {
|
||||
employeesStore.set(res)
|
||||
employeeByIdStore.set(toIdMap(res))
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
avatar: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const accountQ = createQuery(true)
|
||||
accountQ.query(contact.class.PersonAccount, {}, (res) => {
|
||||
@ -322,11 +332,16 @@ function fillStores (): void {
|
||||
|
||||
const persons = res.map((it) => it.person)
|
||||
|
||||
accountPersonQuery.query(
|
||||
accountPersonQuery.query<Person>(
|
||||
contact.class.Person,
|
||||
{ _id: { $in: persons }, [contact.mixin.Employee]: { $exists: false } },
|
||||
(res) => {
|
||||
personAccountPersonByIdStore.set(toIdMap(res))
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
avatar: core.class.Blob
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -14,11 +14,23 @@
|
||||
// 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 { IntlString, plugin } from '@hcengineering/platform'
|
||||
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'
|
||||
|
||||
/**
|
||||
@ -65,13 +77,19 @@ export interface ChannelItem extends AttachedDoc {
|
||||
export enum AvatarType {
|
||||
COLOR = 'color',
|
||||
IMAGE = 'image',
|
||||
GRAVATAR = 'gravatar'
|
||||
GRAVATAR = 'gravatar',
|
||||
|
||||
EXTERNAL = 'external'
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
@ -81,12 +99,19 @@ export interface AvatarProvider extends Doc {
|
||||
getUrl: Resource<GetAvatarUrl>
|
||||
}
|
||||
|
||||
export interface AvatarInfo extends Doc {
|
||||
avatarType: AvatarType
|
||||
avatar?: Ref<Blob> | null
|
||||
avatarProps?: {
|
||||
color?: string
|
||||
url?: string
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Contact extends Doc {
|
||||
export interface Contact extends Doc, AvatarInfo {
|
||||
name: string
|
||||
avatar?: string | null
|
||||
attachments?: number
|
||||
comments?: number
|
||||
channels?: number
|
||||
@ -180,6 +205,7 @@ export const contactPlugin = plugin(contactId, {
|
||||
ChannelsPresenter: '' as AnyComponent,
|
||||
MembersPresenter: '' as AnyComponent,
|
||||
Avatar: '' as AnyComponent,
|
||||
AvatarRef: '' as AnyComponent,
|
||||
UserBoxList: '' as AnyComponent,
|
||||
ChannelPresenter: '' as AnyComponent,
|
||||
SpaceMembers: '' as AnyComponent,
|
||||
@ -212,7 +238,8 @@ export const contactPlugin = plugin(contactId, {
|
||||
function: {
|
||||
GetColorUrl: '' as Resource<GetAvatarUrl>,
|
||||
GetFileUrl: '' as Resource<GetAvatarUrl>,
|
||||
GetGravatarUrl: '' as Resource<GetAvatarUrl>
|
||||
GetGravatarUrl: '' as Resource<GetAvatarUrl>,
|
||||
GetExternalUrl: '' as Resource<GetAvatarUrl>
|
||||
},
|
||||
icon: {
|
||||
ContactApplication: '' as Asset,
|
||||
|
@ -13,12 +13,12 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { AttachedData, Class, Client, Doc, FindResult, Ref, Hierarchy } 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 { AttachedData, Class, Client, Doc, FindResult, Hierarchy, Ref } from '@hcengineering/core'
|
||||
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
|
||||
@ -58,16 +58,8 @@ export function buildGravatarId (email: string): string {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getAvatarProviderId (avatar?: string | null): Ref<AvatarProvider> | undefined {
|
||||
if (avatar === null || avatar === undefined || avatar === '') {
|
||||
return
|
||||
}
|
||||
if (!avatar.includes('://')) {
|
||||
return contactPlugin.avatarProvider.Image
|
||||
}
|
||||
const [schema] = avatar.split('://')
|
||||
|
||||
switch (schema) {
|
||||
export function getAvatarProviderId (kind: AvatarType): Ref<AvatarProvider> | undefined {
|
||||
switch (kind) {
|
||||
case AvatarType.GRAVATAR:
|
||||
return contactPlugin.avatarProvider.Gravatar
|
||||
case AvatarType.COLOR:
|
||||
@ -81,31 +73,9 @@ export function getAvatarProviderId (avatar?: string | null): Ref<AvatarProvider
|
||||
*/
|
||||
export function getGravatarUrl (
|
||||
gravatarId: string,
|
||||
size: IconSize = 'full',
|
||||
width: number = 64,
|
||||
placeholder: GravatarPlaceholderType = 'identicon'
|
||||
): 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}`
|
||||
}
|
||||
|
||||
@ -114,7 +84,7 @@ export function getGravatarUrl (
|
||||
*/
|
||||
export async function checkHasGravatar (gravatarId: string, fetch?: typeof window.fetch): Promise<boolean> {
|
||||
try {
|
||||
return (await (fetch ?? window.fetch)(getGravatarUrl(gravatarId, 'full', '404'))).ok
|
||||
return (await (fetch ?? window.fetch)(getGravatarUrl(gravatarId, 2048, '404'))).ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 notification from '@hcengineering/notification'
|
||||
import { Panel } from '@hcengineering/panel'
|
||||
@ -102,7 +102,7 @@
|
||||
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) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -49,7 +49,7 @@
|
||||
showPopup(
|
||||
FilePreviewPopup,
|
||||
{
|
||||
file: blob._id,
|
||||
file: value.$lookup?.file ?? value.file,
|
||||
contentType: blob.contentType,
|
||||
name: value.name,
|
||||
metadata: value.metadata
|
||||
|
@ -13,17 +13,17 @@
|
||||
// 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 { type Resources } from '@hcengineering/platform'
|
||||
import { getFileUrl } from '@hcengineering/presentation'
|
||||
import { type Location, showPopup } from '@hcengineering/ui'
|
||||
import { getBlobHref } from '@hcengineering/presentation'
|
||||
import { showPopup, type Location } from '@hcengineering/ui'
|
||||
|
||||
import CreateDrive from './components/CreateDrive.svelte'
|
||||
import DrivePanel from './components/DrivePanel.svelte'
|
||||
import DrivePresenter from './components/DrivePresenter.svelte'
|
||||
import DriveSpaceHeader from './components/DriveSpaceHeader.svelte'
|
||||
import DriveSpacePresenter from './components/DriveSpacePresenter.svelte'
|
||||
import DrivePresenter from './components/DrivePresenter.svelte'
|
||||
import EditFolder from './components/EditFolder.svelte'
|
||||
import FilePresenter from './components/FilePresenter.svelte'
|
||||
import FileSizePresenter from './components/FileSizePresenter.svelte'
|
||||
@ -47,10 +47,10 @@ async function EditDrive (drive: Drive): Promise<void> {
|
||||
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]
|
||||
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')
|
||||
link.style.display = 'none'
|
||||
link.target = '_blank'
|
||||
|
@ -10,9 +10,15 @@ import core, {
|
||||
} from '@hcengineering/core'
|
||||
import login, { loginId } from '@hcengineering/login'
|
||||
import { getMetadata, getResource, setMetadata } from '@hcengineering/platform'
|
||||
import presentation, { closeClient, refreshClient, setClient } from '@hcengineering/presentation'
|
||||
import { fetchMetadataLocalStorage, getCurrentLocation, navigate, setMetadataLocalStorage } from '@hcengineering/ui'
|
||||
import { writable } from 'svelte/store'
|
||||
import presentation, { closeClient, refreshClient, setClient, setPresentationCookie } from '@hcengineering/presentation'
|
||||
import {
|
||||
fetchMetadataLocalStorage,
|
||||
getCurrentLocation,
|
||||
navigate,
|
||||
setMetadataLocalStorage,
|
||||
workspaceId
|
||||
} from '@hcengineering/ui'
|
||||
import { writable, get } from 'svelte/store'
|
||||
|
||||
export const versionError = writable<string | undefined>(undefined)
|
||||
|
||||
@ -31,8 +37,8 @@ export async function connect (title: string): Promise<Client | undefined> {
|
||||
return
|
||||
}
|
||||
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 endpoint = await getEndpoint()
|
||||
@ -183,8 +189,7 @@ function clearMetadata (ws: string): void {
|
||||
}
|
||||
setMetadata(presentation.metadata.Token, null)
|
||||
setMetadataLocalStorage(login.metadata.LastToken, null)
|
||||
document.cookie =
|
||||
encodeURIComponent(presentation.metadata.Token.replaceAll(':', '-')) + '=' + encodeURIComponent('') + '; path=/'
|
||||
setPresentationCookie('', get(workspaceId))
|
||||
setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
|
||||
setMetadataLocalStorage(login.metadata.LoginEmail, null)
|
||||
void closeClient()
|
||||
|
@ -123,7 +123,7 @@
|
||||
<div class="mr-2">
|
||||
<Button icon={IconAdd} kind={'list'} on:click={createChild} />
|
||||
</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="fs-title">
|
||||
{value.name}
|
||||
|
@ -37,9 +37,7 @@
|
||||
await avatarEditor.removeAvatar(object.avatar)
|
||||
}
|
||||
const avatar = await avatarEditor.createAvatar()
|
||||
await client.updateDoc(object._class, object.space, object._id, {
|
||||
avatar
|
||||
})
|
||||
await client.diffUpdate(object, avatar)
|
||||
}
|
||||
|
||||
async function nameChange (): Promise<void> {
|
||||
@ -73,7 +71,7 @@
|
||||
<div class="mr-8">
|
||||
{#key object}
|
||||
<EditableAvatar
|
||||
avatar={object.avatar}
|
||||
person={object}
|
||||
size={'x-large'}
|
||||
icon={hr.icon.Department}
|
||||
bind:this={avatarEditor}
|
||||
|
@ -34,7 +34,7 @@
|
||||
<DocNavLink object={value}>
|
||||
<div class="flex-row-center">
|
||||
<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 class="flex-col">
|
||||
<div class="member-title fs-title">
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 contact from '@hcengineering/contact-resources/src/plugin'
|
||||
import { AttachedData, Class, Data, Doc, generateId, MixinData, Ref, WithLookup } from '@hcengineering/core'
|
||||
@ -60,10 +60,14 @@
|
||||
async function createCustomer () {
|
||||
const candidate: Data<Contact> = {
|
||||
name: formatName(targetClass._id, firstName, lastName, object.name),
|
||||
city: object.city
|
||||
city: object.city,
|
||||
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<Contact, Customer> = {
|
||||
description: object.description
|
||||
@ -188,7 +192,7 @@
|
||||
</div>
|
||||
<div class="ml-4 flex">
|
||||
<EditableAvatar
|
||||
avatar={object.avatar}
|
||||
person={object}
|
||||
size={'large'}
|
||||
name={object.name}
|
||||
bind:this={avatarEditor}
|
||||
|
@ -181,7 +181,7 @@
|
||||
<div class="flex-between header bottom-divider">
|
||||
<div class="flex-row-center">
|
||||
{#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>
|
||||
{/if}
|
||||
{#if newTxes > 0}
|
||||
|
@ -22,7 +22,7 @@
|
||||
<svelte:fragment slot="content">
|
||||
<div class="flex-row-center flex-wrap gap-2">
|
||||
{#if sender}
|
||||
<Avatar avatar={sender.avatar} name={sender.name} size={'small'} />
|
||||
<Avatar person={sender} name={sender.name} size={'small'} />
|
||||
{/if}
|
||||
<span class="overflow-label">
|
||||
{value.body}
|
||||
|
@ -89,7 +89,7 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="inbox-activity__content shrink flex-grow clear-mins" class:read={newTxes === 0}>
|
||||
<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}
|
||||
<span class="font-medium">{getName(client.getHierarchy(), employee)}</span>
|
||||
{:else}
|
||||
|
@ -45,7 +45,7 @@
|
||||
|
||||
<div class="antiContactCard">
|
||||
<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}
|
||||
<DocNavLink object={candidate} {disabled}>
|
||||
<div class="name lines-limit-2">
|
||||
|
@ -15,7 +15,14 @@
|
||||
<script lang="ts">
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
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 {
|
||||
Account,
|
||||
@ -182,11 +189,13 @@
|
||||
const candidate: Data<Person> = {
|
||||
name: combineName(object.firstName ?? '', object.lastName ?? ''),
|
||||
city: object.city,
|
||||
channels: 0
|
||||
}
|
||||
if (avatar !== undefined) {
|
||||
candidate.avatar = await avatarEditor.createAvatar()
|
||||
channels: 0,
|
||||
avatarType: AvatarType.COLOR
|
||||
}
|
||||
const info = await avatarEditor.createAvatar()
|
||||
candidate.avatar = info.avatar
|
||||
candidate.avatarType = info.avatarType
|
||||
candidate.avatarProps = info.avatarProps
|
||||
const candidateData: MixinData<Person, Candidate> = {
|
||||
title: object.title,
|
||||
onsite: object.onsite,
|
||||
@ -619,7 +628,9 @@
|
||||
disabled={loading}
|
||||
bind:this={avatarEditor}
|
||||
bind:direct={object.avatar}
|
||||
avatar={undefined}
|
||||
person={{
|
||||
avatarType: AvatarType.COLOR
|
||||
}}
|
||||
size={'large'}
|
||||
name={combineName(object?.firstName?.trim() ?? '', object?.lastName?.trim() ?? '')}
|
||||
/>
|
||||
|
@ -67,7 +67,7 @@
|
||||
{/if}
|
||||
<div class="flex-between mb-1">
|
||||
<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="fs-title over-underline lines-limit-2">
|
||||
{object.$lookup?.attachedTo ? getName(client.getHierarchy(), object.$lookup.attachedTo) : ''}
|
||||
|
@ -45,7 +45,7 @@
|
||||
on:click={() => onClick(p)}
|
||||
>
|
||||
<div class="icon">
|
||||
<Avatar size={'x-small'} avatar={p.avatar} name={p.name} />
|
||||
<Avatar size={'x-small'} person={p} name={p.name} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -53,9 +53,7 @@
|
||||
await avatarEditor.removeAvatar(employee.avatar)
|
||||
}
|
||||
const avatar = await avatarEditor.createAvatar()
|
||||
await client.update(employee, {
|
||||
avatar
|
||||
})
|
||||
await client.diffUpdate(employee, avatar)
|
||||
}
|
||||
|
||||
const manager = createFocusManager()
|
||||
@ -97,7 +95,7 @@
|
||||
<div class="flex flex-grow w-full">
|
||||
<div class="mr-8">
|
||||
<EditableAvatar
|
||||
avatar={employee.avatar}
|
||||
person={employee}
|
||||
email={account.email}
|
||||
size={'x-large'}
|
||||
name={employee.name}
|
||||
|
@ -13,23 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import contact, { Employee, PersonAccount, combineName, getFirstName, getLastName } from '@hcengineering/contact'
|
||||
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 { AvatarType } from '@hcengineering/contact'
|
||||
import { EditableAvatar } from '@hcengineering/contact-resources'
|
||||
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
|
||||
|
||||
@ -49,18 +40,19 @@
|
||||
await client.createDoc(
|
||||
setting.class.WorkspaceSetting,
|
||||
setting.space.Setting,
|
||||
{ icon: avatar },
|
||||
{ icon: avatar.avatar },
|
||||
setting.ids.WorkspaceSetting
|
||||
)
|
||||
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)
|
||||
}
|
||||
const avatar = await avatarEditor.createAvatar()
|
||||
await client.update(workspaceSettings, {
|
||||
icon: avatar
|
||||
icon: avatar.avatar
|
||||
})
|
||||
}
|
||||
|
||||
@ -71,7 +63,10 @@
|
||||
|
||||
<div class="hulyComponent p-10 flex ac-body row">
|
||||
<EditableAvatar
|
||||
avatar={workspaceSettings?.icon}
|
||||
person={{
|
||||
avatarType: AvatarType.IMAGE,
|
||||
avatar: workspaceSettings?.icon
|
||||
}}
|
||||
size={'x-large'}
|
||||
bind:this={avatarEditor}
|
||||
on:done={onAvatarDone}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user