diff --git a/dev/tool/package.json b/dev/tool/package.json index 630be47238..2a140c1344 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -13,7 +13,7 @@ "docker:build": "docker build -t hardcoreeng/tool .", "docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/tool staging", "docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/tool", - "run-local": "cross-env SERVER_SECRET=secret MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost MONGO_URL=mongodb://localhost:27017 TRANSACTOR_URL=ws:/localhost:3333 TELEGRAM_DATABASE=telegram-service ELASTIC_URL=http://localhost:9200 REKONI_URL=http://localhost:4004 node --inspect -r ts-node/register ./src/__start.ts", + "run-local": "cross-env SERVER_SECRET=secret MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost MONGO_URL=mongodb://localhost:27017 TRANSACTOR_URL=ws:/localhost:3333 TELEGRAM_DATABASE=telegram-service ELASTIC_URL=http://localhost:9200 REKONI_URL=http://localhost:4004 node -r ts-node/register ./src/__start.ts", "run": "cross-env ts-node ./src/__start.ts", "upgrade": "rushx run-local upgrade", "lint": "eslint src", diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index 453b8e8d9f..c0bf70ef74 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -590,6 +590,25 @@ export function createModel (builder: Builder): void { }, contact.action.KickEmployee ) + createAction( + builder, + { + action: contact.actionImpl.KickEmployee, + label: contact.string.DeleteEmployee, + query: { + active: false + }, + category: contact.category.Contact, + target: contact.class.Employee, + input: 'focus', + context: { + mode: ['context'], + group: 'other' + }, + secured: true + }, + contact.action.KickEmployee + ) createAction( builder, @@ -602,6 +621,9 @@ export function createModel (builder: Builder): void { _object: 'value' } }, + query: { + active: false + }, label: contact.string.MergeEmployee, category: contact.category.Contact, target: contact.class.Employee, diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index edaeab5cdb..906d171f5c 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -591,7 +591,7 @@ export function createModel (builder: Builder): void { } const applicantViewOptions: ViewOptionsModel = { - groupBy: ['state', 'doneState', 'assignee'], + groupBy: ['state', 'doneState', 'assignee', 'space'], orderBy: [ ['state', SortingOrder.Ascending], ['doneState', SortingOrder.Ascending], diff --git a/models/server-contact/src/index.ts b/models/server-contact/src/index.ts index 062a147f53..dd12e80aea 100644 --- a/models/server-contact/src/index.ts +++ b/models/server-contact/src/index.ts @@ -43,7 +43,8 @@ export function createModel (builder: Builder): void { trigger: serverContact.trigger.OnContactDelete }) - builder.createDoc(serverCore.class.Trigger, core.space.Model, { - trigger: serverContact.trigger.OnEmployeeUpdate + builder.createDoc(serverCore.class.AsyncTrigger, core.space.Model, { + trigger: serverContact.trigger.OnEmployeeUpdate, + classes: [contact.class.Employee] }) } diff --git a/models/task/src/migration.ts b/models/task/src/migration.ts index 26c0be62e2..d75afd0658 100644 --- a/models/task/src/migration.ts +++ b/models/task/src/migration.ts @@ -211,7 +211,9 @@ export const taskOperation: MigrateOperation = { const stateTemplateClasses = client.hierarchy.getDescendants(task.class.StateTemplate) const doneStateTemplatesClasses = client.hierarchy.getDescendants(task.class.DoneStateTemplate) - await client.move(DOMAIN_STATE, { _class: { $in: [...stateClasses, ...doneStateClasses] } }, DOMAIN_STATUS) + try { + await client.move(DOMAIN_STATE, { _class: { $in: [...stateClasses, ...doneStateClasses] } }, DOMAIN_STATUS) + } catch (err) {} await client.update( DOMAIN_STATUS, diff --git a/packages/core/src/operations.ts b/packages/core/src/operations.ts index 7ee9ea01c2..44228ca7eb 100644 --- a/packages/core/src/operations.ts +++ b/packages/core/src/operations.ts @@ -1,5 +1,17 @@ -import { DocumentUpdate, Hierarchy, MixinData, MixinUpdate, ModelDb } from '.' -import type { Account, AttachedData, AttachedDoc, Class, Data, Doc, Mixin, Ref, Space, Timestamp } from './classes' +import { DocumentUpdate, Hierarchy, MixinData, MixinUpdate, ModelDb, toFindResult } from '.' +import type { + Account, + AnyAttribute, + AttachedData, + AttachedDoc, + Class, + Data, + Doc, + Mixin, + Ref, + Space, + Timestamp +} from './classes' import { Client } from './client' import core from './component' import type { DocumentQuery, FindOptions, FindResult, TxResult, WithLookup } from './storage' @@ -215,6 +227,7 @@ export class TxOperations implements Omit { ): Promise { const hierarchy = this.client.getHierarchy() if (hierarchy.isMixin(doc._class)) { + // TODO: Rework it is wrong, we need to split values to mixin update and original document update if mixed. const baseClass = hierarchy.getBaseClass(doc._class) return this.updateMixin(doc._id, baseClass, doc.space, doc._class, update, modifiedOn, modifiedBy) } @@ -291,8 +304,87 @@ export class ApplyOperations extends TxOperations { } async commit (): Promise { - return await ((await this.ops.tx( - this.ops.txFactory.createTxApplyIf(core.space.Tx, this.scope, this.matches, this.txes) - )) as Promise) + if (this.txes.length > 0) { + return await ((await this.ops.tx( + this.ops.txFactory.createTxApplyIf(core.space.Tx, this.scope, this.matches, this.txes) + )) as Promise) + } + return true + } +} + +/** + * @public + * + * Builder for TxOperations. + */ +export class TxBuilder extends TxOperations { + txes: TxCUD[] = [] + matches: DocumentClassQuery[] = [] + constructor (readonly hierarchy: Hierarchy, readonly modelDb: ModelDb, user: Ref) { + const txClient: Client = { + getHierarchy: () => this.hierarchy, + getModel: () => this.modelDb, + close: async () => {}, + findOne: async (_class, query, options?) => undefined, + findAll: async (_class, query, options?) => toFindResult([]), + tx: async (tx): Promise => { + if (this.hierarchy.isDerived(tx._class, core.class.TxCUD)) { + this.txes.push(tx as TxCUD) + } + return {} + } + } + super(txClient, user) + } +} + +/** + * @public + */ +export async function updateAttribute ( + client: TxOperations, + object: Doc, + _class: Ref>, + attribute: { key: string, attr: AnyAttribute }, + value: any, + modifyBy?: Ref +): Promise { + const doc = object + const attributeKey = attribute.key + if ((doc as any)[attributeKey] === value) return + const attr = attribute.attr + if (client.getHierarchy().isMixin(attr.attributeOf)) { + await client.updateMixin( + doc._id, + _class, + doc.space, + attr.attributeOf, + { [attributeKey]: value }, + Date.now(), + modifyBy + ) + } else { + if (client.getHierarchy().isDerived(attribute.attr.type._class, core.class.ArrOf)) { + const oldValue: any[] = (object as any)[attributeKey] ?? [] + const val: any[] = value + const toPull = oldValue.filter((it: any) => !val.includes(it)) + + const toPush = val.filter((it) => !oldValue.includes(it)) + if (toPull.length > 0) { + await client.update(object, { $pull: { [attributeKey]: { $in: toPull } } }, false, Date.now(), modifyBy) + } + if (toPush.length > 0) { + await client.update( + object, + { $push: { [attributeKey]: { $each: toPush, $position: 0 } } }, + false, + Date.now(), + modifyBy + ) + } + } else { + await client.update(object, { [attributeKey]: value }, false, Date.now(), modifyBy) + } } } diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 180b2fc2f2..a475e2402e 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -66,6 +66,6 @@ export interface ServerStorage extends LowLevelStorage { options?: FindOptions ) => Promise> tx: (ctx: MeasureContext, tx: Tx) => Promise<[TxResult, Tx[]]> - apply: (ctx: MeasureContext, tx: Tx[], broadcast: boolean) => Promise + apply: (ctx: MeasureContext, tx: Tx[], broadcast: boolean, updateTx: boolean) => Promise close: () => Promise } diff --git a/packages/presentation/src/attributes.ts b/packages/presentation/src/attributes.ts index cbf4e22223..df9131e1c1 100644 --- a/packages/presentation/src/attributes.ts +++ b/packages/presentation/src/attributes.ts @@ -1,4 +1,4 @@ -import core, { AnyAttribute, Class, Client, Doc, Ref, TxOperations } from '@hcengineering/core' +import { AnyAttribute, Client } from '@hcengineering/core' /** * @public @@ -8,37 +8,7 @@ export interface KeyedAttribute { attr: AnyAttribute } -export async function updateAttribute ( - client: TxOperations, - object: Doc, - _class: Ref>, - attribute: KeyedAttribute, - value: any -): Promise { - const doc = object - const attributeKey = attribute.key - if ((doc as any)[attributeKey] === value) return - const attr = attribute.attr - if (client.getHierarchy().isMixin(attr.attributeOf)) { - await client.updateMixin(doc._id, _class, doc.space, attr.attributeOf, { [attributeKey]: value }) - } else { - if (client.getHierarchy().isDerived(attribute.attr.type._class, core.class.ArrOf)) { - const oldvalue: any[] = (object as any)[attributeKey] ?? [] - const val: any[] = value - const toPull = oldvalue.filter((it: any) => !val.includes(it)) - - const toPush = val.filter((it) => !oldvalue.includes(it)) - if (toPull.length > 0) { - await client.update(object, { $pull: { [attributeKey]: { $in: toPull } } }) - } - if (toPush.length > 0) { - await client.update(object, { $push: { [attributeKey]: { $each: toPush, $position: 0 } } }) - } - } else { - await client.update(object, { [attributeKey]: value }) - } - } -} +export { updateAttribute } from '@hcengineering/core' export function getAttribute (client: Client, object: any, key: KeyedAttribute): any { // Check if attr is mixin and return it's value diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index 20c6350c2a..7c3738bdac 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -252,6 +252,9 @@ export function createQuery (dontDestroy?: boolean): LiveQuery { * @public */ export function getFileUrl (file: string, size: IconSize = 'full'): string { + if (file.includes('://')) { + return file + } const uploadUrl = getMetadata(plugin.metadata.UploadURL) const token = getMetadata(plugin.metadata.Token) const url = `${uploadUrl as string}?file=${file}&token=${token as string}&size=${size as string}` diff --git a/plugins/bitrix/src/sync.ts b/plugins/bitrix/src/sync.ts index 750f048953..0edb8c8a43 100644 --- a/plugins/bitrix/src/sync.ts +++ b/plugins/bitrix/src/sync.ts @@ -840,14 +840,35 @@ async function synchronizeUsers ( let totalUsers = 1 let next = 0 - const employees = new Map((await ops.client.findAll(contact.class.Employee, {})).map((it) => [it._id, it])) + const employeesList = await ops.client.findAll( + contact.class.Employee, + {}, + { + lookup: { + _id: { + channels: contact.class.Channel + } + } + } + ) + const employees = new Map(employeesList.map((it) => [it._id, it])) while (userList.size < totalUsers) { const users = await ops.bitrixClient.call('user.search', { start: next }) next = users.next totalUsers = users.total for (const u of users.result) { - const account = allEmployee.find((it) => it.email === u.EMAIL) + let account = allEmployee.find((it) => it.email === u.EMAIL) + + if (account === undefined) { + // Try to find from employee + employeesList.forEach((it) => { + if ((it.$lookup?.channels as Channel[])?.some((q) => q.value === u.EMAIL)) { + account = allEmployee.find((qit) => qit.employee === it._id) + } + }) + } + let accountId = account?._id if (accountId === undefined) { const employeeId = await ops.client.createDoc(contact.class.Employee, contact.space.Contacts, { diff --git a/plugins/chunter-resources/src/components/CommentPopup.svelte b/plugins/chunter-resources/src/components/CommentPopup.svelte index 3a3b03574e..abafb563bb 100644 --- a/plugins/chunter-resources/src/components/CommentPopup.svelte +++ b/plugins/chunter-resources/src/components/CommentPopup.svelte @@ -18,9 +18,10 @@ import chunter, { Comment } from '@hcengineering/chunter' import { createQuery } from '@hcengineering/presentation' - import { Label } from '@hcengineering/ui' + import { Label, resizeObserver } from '@hcengineering/ui' import { DocNavLink, ObjectPresenter } from '@hcengineering/view-resources' import CommentPresenter from './CommentPresenter.svelte' + import { createEventDispatcher } from 'svelte' export let objectId: Ref export let object: Doc @@ -35,9 +36,15 @@ }, { sort: { modifiedOn: SortingOrder.Descending } } ) + const dispatch = createEventDispatcher() -
+
{ + dispatch('changeContent') + }} +>
diff --git a/plugins/contact-assets/lang/en.json b/plugins/contact-assets/lang/en.json index 8589005218..4a93ee4116 100644 --- a/plugins/contact-assets/lang/en.json +++ b/plugins/contact-assets/lang/en.json @@ -28,6 +28,7 @@ "Person": "Person", "Organization": "Organization", "Employee": "Employee", + "DeleteEmployee": "Delete", "Value": "Value", "FullDescription": "Full description", "Phone": "Phone", @@ -66,9 +67,9 @@ "CreateEmployee": "Create an employee", "Inactive": "Inactive", "Birthday": "Birthday", - "UseImage": "Upload an image", - "UseGravatar": "Use Gravatar", - "UseColor": "Use color", + "UseImage": "Attached photo", + "UseGravatar": "Gravatar", + "UseColor": "Color", "NotSpecified": "Not specified", "CreatedOn": "Created", "Whatsapp": "Whatsapp", @@ -77,8 +78,11 @@ "ProfilePlaceholder": "Profile...", "CurrentEmployee": "Current employee", "MergeEmployee": "Merge employee", + "MergeEmployeeFrom": "From(inactive)", + "MergeEmployeeTo": "To employee", "DisplayName": "Display name", "SelectAvatar": "Select avatar", + "AvatarProvider": "Avatar provider", "GravatarsManaged": "Gravatars are managed through", "CategoryProjectMembers": "Project members", "AddMembersHeader": "Add members to {value}:", diff --git a/plugins/contact-assets/lang/ru.json b/plugins/contact-assets/lang/ru.json index d34325ff50..be0bc5fcfe 100644 --- a/plugins/contact-assets/lang/ru.json +++ b/plugins/contact-assets/lang/ru.json @@ -28,6 +28,7 @@ "Person": "Персона", "Organization": "Организация", "Employee": "Сотрудник", + "DeleteEmployee": "Удалить", "Value": "Значение", "FullDescription": "Полное описание", "Phone": "Телефон", @@ -66,9 +67,10 @@ "CreateEmployee": "Создать сотрудника", "Inactive": "Не активный", "Birthday": "День рождения", - "UseImage": "Загрузить фото", - "UseGravatar": "Использовать Gravatar", - "UseColor": "Использовать цвет", + "UseImage": "Загруженное Фото", + "UseGravatar": "Граватар", + "UseColor": "Цвет", + "AvatarProvider": "Тип аватара", "NotSpecified": "Не указан", "CreatedOn": "Создан", "Whatsapp": "Whatsapp", @@ -77,6 +79,8 @@ "ProfilePlaceholder": "Профиль...", "CurrentEmployee": "Текущий сотрудник", "MergeEmployee": "Объеденить сотрудника", + "MergeEmployeeFrom": "Из (неактивный)", + "MergeEmployeeTo": "В сотрудника", "DisplayName": "Отображаемое имя", "SelectAvatar": "Выбрать аватар", "GravatarsManaged": "Граватары управляются через", diff --git a/plugins/contact-resources/src/components/Avatar.svelte b/plugins/contact-resources/src/components/Avatar.svelte index 3cb5ac063a..93d77fabfc 100644 --- a/plugins/contact-resources/src/components/Avatar.svelte +++ b/plugins/contact-resources/src/components/Avatar.svelte @@ -55,7 +55,7 @@ if (!avatarProvider || avatarProvider.type === AvatarType.COLOR) { url = undefined - } else if (avatarProvider.type === AvatarType.IMAGE) { + } else if (avatarProvider?.type === AvatarType.IMAGE) { url = (await getResource(avatarProvider.getUrl))(avatar, size) } else { const uri = avatar.split('://')[1] diff --git a/plugins/contact-resources/src/components/EditEmployee.svelte b/plugins/contact-resources/src/components/EditEmployee.svelte index e519ce12c0..1e900daa27 100644 --- a/plugins/contact-resources/src/components/EditEmployee.svelte +++ b/plugins/contact-resources/src/components/EditEmployee.svelte @@ -14,7 +14,7 @@ // limitations under the License. --> {#if !isEqual(value, targetEmp, key)} -
- -
-
- { - onChange(key, e.detail) - }} - /> -
-
- +
+
+ {#if attribute?.label} +
+ +
+
+ { + selected = false + onChange(key, false) + }} + /> +
+ +
+
+
+
+ { + selected = true + onChange(key, true) + }} + /> +
+ +
{/if} + + diff --git a/plugins/contact-resources/src/components/MergeEmployee.svelte b/plugins/contact-resources/src/components/MergeEmployee.svelte index 0f87cb9e39..af7568dc5b 100644 --- a/plugins/contact-resources/src/components/MergeEmployee.svelte +++ b/plugins/contact-resources/src/components/MergeEmployee.svelte @@ -13,18 +13,17 @@ // limitations under the License. --> dispatch('close')} > -
- -
- {#if targetEmp} - - - - - - - - - {getName(item)} - - - {#each objectAttributes as attribute} - - {/each} - {#each mixins as mixin} - {@const attributes = getMixinAttributes(mixin)} - {#each attributes as attribute} - selectMixin(mixin, key, value)} - _class={mixin} - /> - {/each} - {/each} - {#each Array.from(targetConflict.values()) as conflict} - {@const val = valueConflict.get(conflict.provider)} - {#if val} -
- -
-
- { - selectChannel(e.detail, conflict, val) - }} - /> -
-
- -
- {/if} - {/each} -
-
- - {getName(result)} - - +
+
+
- {/if} + >> +
+ + +
+
+ {#key [targetEmployee, sourceEmployee]} + {#if targetEmp && sourceEmp} +
+ + + + + + + + {getName(item)} + + + {#each objectAttributes as attribute} + + {/each} + {#each mixins as mixin} + {@const attributes = getMixinAttributes(mixin)} + {#each attributes as attribute} + selectMixin(mixin, key, value)} + _class={mixin} + selected={toAny(mixinUpdate)?.[mixin]?.[attribute] !== undefined} + /> + {/each} + {/each} + {#each Array.from(oldChannels).concat(targetChannels) as channel} + {@const enabled = enabledChannels.get(channel._id) ?? true} +
+ +
+ { + enabledChannels.set(channel._id, e.detail) + enabledChannels = enabledChannels + }} + /> +
+
+ {/each} +
+
+ +
+ {/if} + {/key} diff --git a/plugins/contact-resources/src/components/SelectAvatarPopup.svelte b/plugins/contact-resources/src/components/SelectAvatarPopup.svelte index c3047080ef..598f507af1 100644 --- a/plugins/contact-resources/src/components/SelectAvatarPopup.svelte +++ b/plugins/contact-resources/src/components/SelectAvatarPopup.svelte @@ -19,11 +19,11 @@ import { Asset } from '@hcengineering/platform' import { AnySvelteComponent, DropdownLabelsIntl, Label, showPopup } from '@hcengineering/ui' + 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' - import { getAvatarTypeDropdownItems } from '../utils' - import presentation, { Card, getFileUrl } from '@hcengineering/presentation' export let avatar: string | undefined export let email: string | undefined @@ -160,12 +160,16 @@ dispatch('close') }} > - +
+
{#if selectedAvatarType === AvatarType.IMAGE}
diff --git a/plugins/contact-resources/src/index.ts b/plugins/contact-resources/src/index.ts index 4217ad1dd3..ddba4189eb 100644 --- a/plugins/contact-resources/src/index.ts +++ b/plugins/contact-resources/src/index.ts @@ -188,6 +188,22 @@ async function doContactQuery ( async function kickEmployee (doc: Employee): Promise { const client = getClient() const email = await client.findOne(contact.class.EmployeeAccount, { employee: doc._id }) + if (!doc.active) { + showPopup( + MessageBox, + { + label: contact.string.DeleteEmployee, + message: contact.string.DeleteEmployee + }, + undefined, + (res?: boolean) => { + if (res === true) { + void client.remove(doc) + } + } + ) + return + } if (email === undefined) { await client.update(doc, { active: false }) } else { diff --git a/plugins/contact-resources/src/plugin.ts b/plugins/contact-resources/src/plugin.ts index f299d66596..71b7f65c93 100644 --- a/plugins/contact-resources/src/plugin.ts +++ b/plugins/contact-resources/src/plugin.ts @@ -63,8 +63,11 @@ export default mergeIds(contactId, contact, { Inactive: '' as IntlString, NotSpecified: '' as IntlString, MergeEmployee: '' as IntlString, + MergeEmployeeFrom: '' as IntlString, + MergeEmployeeTo: '' as IntlString, SelectAvatar: '' as IntlString, GravatarsManaged: '' as IntlString, + AvatarProvider: '' as IntlString, CategoryProjectMembers: '' as IntlString, AddMembersHeader: '' as IntlString, @@ -73,7 +76,8 @@ export default mergeIds(contactId, contact, { CategoryCurrentUser: '' as IntlString, CategoryPreviousAssigned: '' as IntlString, CategoryProjectLead: '' as IntlString, - CategoryOther: '' as IntlString + CategoryOther: '' as IntlString, + DeleteEmployee: '' as IntlString }, function: { GetContactLink: '' as Resource<(doc: Doc, props: Record) => Promise>, diff --git a/plugins/contact-resources/src/utils.ts b/plugins/contact-resources/src/utils.ts index 1b5eaf211d..a462dfad64 100644 --- a/plugins/contact-resources/src/utils.ts +++ b/plugins/contact-resources/src/utils.ts @@ -266,4 +266,5 @@ export function getAvatarProviderId (avatar?: string | null): Ref @@ -34,10 +36,20 @@ } $: channels = (object.$lookup?.attachedTo as WithLookup)?.$lookup?.channels + + $: company = object?.$lookup?.space?.company
+
+ + {#if company} +
+ +
+ {/if} +
@@ -69,7 +81,9 @@
- +
+ +
{#if (object.attachments ?? 0) > 0} @@ -77,9 +91,14 @@
{/if} - {#if (object.comments ?? 0) > 0} + {#if (object.comments ?? 0) > 0 || (object.$lookup?.attachedTo !== undefined && (object.$lookup.attachedTo.comments ?? 0) > 0)}
- + {#if (object.comments ?? 0) > 0} + + {/if} + {#if object.$lookup?.attachedTo !== undefined && (object.$lookup.attachedTo.comments ?? 0) > 0} + + {/if}
{/if}
diff --git a/plugins/task-resources/src/components/AssigneePresenter.svelte b/plugins/task-resources/src/components/AssigneePresenter.svelte index aaf57ef936..2dca2da088 100644 --- a/plugins/task-resources/src/components/AssigneePresenter.svelte +++ b/plugins/task-resources/src/components/AssigneePresenter.svelte @@ -108,4 +108,5 @@ onEmployeeEdit={handleAssigneeEditorOpened} tooltipLabels={{ personLabel: task.string.TaskAssignee, placeholderLabel: task.string.TaskUnAssign }} /> + {/if} diff --git a/plugins/view-resources/src/components/filter/ObjectFilter.svelte b/plugins/view-resources/src/components/filter/ObjectFilter.svelte index 4d6e713c99..92a64de04a 100644 --- a/plugins/view-resources/src/components/filter/ObjectFilter.svelte +++ b/plugins/view-resources/src/components/filter/ObjectFilter.svelte @@ -190,7 +190,11 @@ {/if}
- {#if isStatus}{statusesCount[i]}{:else}{targets.get(value?._id)}{/if} + {#if isStatus} + {statusesCount[i] ?? ''} + {:else} + {targets.get(value?._id) ?? ''} + {/if}
diff --git a/pods/server/Dockerfile b/pods/server/Dockerfile index f4224c7908..295785e977 100644 --- a/pods/server/Dockerfile +++ b/pods/server/Dockerfile @@ -5,4 +5,4 @@ WORKDIR /usr/src/app COPY bundle.js ./ EXPOSE 8080 -CMD [ "node", "--enable-source-maps", "--inspect", "bundle.js" ] +CMD [ "node", "--enable-source-maps", "bundle.js" ] diff --git a/pods/server/src/server.ts b/pods/server/src/server.ts index 1966ed5628..91b0ad0f6f 100644 --- a/pods/server/src/server.ts +++ b/pods/server/src/server.ts @@ -69,13 +69,13 @@ import { serverLeadId } from '@hcengineering/server-lead' import { serverNotificationId } from '@hcengineering/server-notification' import { serverRecruitId } from '@hcengineering/server-recruit' import { serverRequestId } from '@hcengineering/server-request' -import { serverViewId } from '@hcengineering/server-view' import { serverSettingId } from '@hcengineering/server-setting' import { serverTagsId } from '@hcengineering/server-tags' import { serverTaskId } from '@hcengineering/server-task' import { serverTelegramId } from '@hcengineering/server-telegram' import { Token } from '@hcengineering/server-token' import { serverTrackerId } from '@hcengineering/server-tracker' +import { serverViewId } from '@hcengineering/server-view' import { BroadcastCall, ClientSession, start as startJsonRpc } from '@hcengineering/server-ws' import { activityId } from '@hcengineering/activity' @@ -109,31 +109,31 @@ import coreEng from '@hcengineering/core/src/lang/en.json' import loginEng from '@hcengineering/login-assets/lang/en.json' -import taskEn from '@hcengineering/task-assets/lang/en.json' -import viewEn from '@hcengineering/view-assets/lang/en.json' -import chunterEn from '@hcengineering/chunter-assets/lang/en.json' -import attachmentEn from '@hcengineering/attachment-assets/lang/en.json' -import contactEn from '@hcengineering/contact-assets/lang/en.json' -import recruitEn from '@hcengineering/recruit-assets/lang/en.json' import activityEn from '@hcengineering/activity-assets/lang/en.json' +import attachmentEn from '@hcengineering/attachment-assets/lang/en.json' import automationEn from '@hcengineering/automation-assets/lang/en.json' -import settingEn from '@hcengineering/setting-assets/lang/en.json' -import telegramEn from '@hcengineering/telegram-assets/lang/en.json' -import leadEn from '@hcengineering/lead-assets/lang/en.json' -import gmailEn from '@hcengineering/gmail-assets/lang/en.json' -import workbenchEn from '@hcengineering/workbench-assets/lang/en.json' -import inventoryEn from '@hcengineering/inventory-assets/lang/en.json' -import templatesEn from '@hcengineering/templates-assets/lang/en.json' -import notificationEn from '@hcengineering/notification-assets/lang/en.json' -import tagsEn from '@hcengineering/tags-assets/lang/en.json' -import calendarEn from '@hcengineering/calendar-assets/lang/en.json' -import trackerEn from '@hcengineering/tracker-assets/lang/en.json' -import boardEn from '@hcengineering/board-assets/lang/en.json' -import preferenceEn from '@hcengineering/preference-assets/lang/en.json' -import hrEn from '@hcengineering/hr-assets/lang/en.json' -import documentEn from '@hcengineering/document-assets/lang/en.json' import bitrixEn from '@hcengineering/bitrix-assets/lang/en.json' +import boardEn from '@hcengineering/board-assets/lang/en.json' +import calendarEn from '@hcengineering/calendar-assets/lang/en.json' +import chunterEn from '@hcengineering/chunter-assets/lang/en.json' +import contactEn from '@hcengineering/contact-assets/lang/en.json' +import documentEn from '@hcengineering/document-assets/lang/en.json' +import gmailEn from '@hcengineering/gmail-assets/lang/en.json' +import hrEn from '@hcengineering/hr-assets/lang/en.json' +import inventoryEn from '@hcengineering/inventory-assets/lang/en.json' +import leadEn from '@hcengineering/lead-assets/lang/en.json' +import notificationEn from '@hcengineering/notification-assets/lang/en.json' +import preferenceEn from '@hcengineering/preference-assets/lang/en.json' +import recruitEn from '@hcengineering/recruit-assets/lang/en.json' import requestEn from '@hcengineering/request-assets/lang/en.json' +import settingEn from '@hcengineering/setting-assets/lang/en.json' +import tagsEn from '@hcengineering/tags-assets/lang/en.json' +import taskEn from '@hcengineering/task-assets/lang/en.json' +import telegramEn from '@hcengineering/telegram-assets/lang/en.json' +import templatesEn from '@hcengineering/templates-assets/lang/en.json' +import trackerEn from '@hcengineering/tracker-assets/lang/en.json' +import viewEn from '@hcengineering/view-assets/lang/en.json' +import workbenchEn from '@hcengineering/workbench-assets/lang/en.json' addStringsLoader(coreId, async (lang: string) => coreEng) addStringsLoader(loginId, async (lang: string) => loginEng) diff --git a/server-plugins/contact-resources/src/index.ts b/server-plugins/contact-resources/src/index.ts index 0a942a90c7..b2662c9d80 100644 --- a/server-plugins/contact-resources/src/index.ts +++ b/server-plugins/contact-resources/src/index.ts @@ -14,24 +14,32 @@ // limitations under the License. // -import contact, { Contact, contactId, Employee, getName, Organization, Person } from '@hcengineering/contact' +import contact, { + Contact, + contactId, + Employee, + EmployeeAccount, + getName, + Organization, + Person +} from '@hcengineering/contact' import core, { - AnyAttribute, - ArrOf, + Account, AttachedDoc, - Class, Collection, concatLink, Doc, - Obj, Ref, RefTo, + Timestamp, Tx, + TxBuilder, TxRemoveDoc, - TxUpdateDoc + TxUpdateDoc, + updateAttribute } from '@hcengineering/core' import { getMetadata } from '@hcengineering/platform' -import serverCore, { TriggerControl } from '@hcengineering/server-core' +import serverCore, { AsyncTriggerControl, TriggerControl } from '@hcengineering/server-core' import { workbenchId } from '@hcengineering/workbench' /** @@ -65,6 +73,9 @@ export async function OnContactDelete ( if (avatar?.includes('://') && !avatar?.startsWith('image://')) { return [] } + if (avatar === '') { + return [] + } storageFx(async (adapter, bucket) => { await adapter.remove(bucket, [avatar]) @@ -96,25 +107,126 @@ export async function OnContactDelete ( return result } -async function mergeCollectionAttributes ( - control: TriggerControl, - attributes: Map, - oldValue: Ref, - newValue: Ref +async function updateAllRefs ( + control: AsyncTriggerControl, + sourceAccount: EmployeeAccount, + targetAccount: EmployeeAccount, + modifiedOn: Timestamp, + modifiedBy: Ref ): Promise { const res: Tx[] = [] + // Move all possible references to Account and Employee and replace to target one. + const reftos = (await control.modelDb.findAll(core.class.Attribute, { 'type._class': core.class.RefTo })).filter( + (it) => { + const to = it.type as RefTo + return to.to === contact.class.Employee || to.to === core.class.Account || to.to === contact.class.EmployeeAccount + } + ) + + for (const attr of reftos) { + if (attr.name === '_id') { + continue + } + const to = attr.type as RefTo + if (to.to === contact.class.Employee) { + const descendants = control.hierarchy.getDescendants(attr.attributeOf) + for (const d of descendants) { + if (control.hierarchy.findDomain(d) !== undefined) { + const values = await control.findAll(d, { [attr.name]: sourceAccount.employee }) + + const builder = new TxBuilder(control.hierarchy, control.modelDb, modifiedBy) + for (const v of values) { + await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount.employee, targetAccount._id) + } + await control.apply(builder.txes, true, true) + } + } + } + if ( + (to.to === contact.class.EmployeeAccount || to.to === core.class.Account) && + sourceAccount !== undefined && + targetAccount !== undefined + ) { + const descendants = control.hierarchy.getDescendants(attr.attributeOf) + for (const d of descendants) { + if (control.hierarchy.findDomain(d) !== undefined) { + const values = await control.findAll(d, { [attr.name]: sourceAccount._id }) + const builder = new TxBuilder(control.hierarchy, control.modelDb, modifiedBy) + for (const v of values) { + await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount._id, targetAccount._id) + } + await control.apply(builder.txes, true, true) + } + } + } + } + const arrs = await control.findAll(core.class.Attribute, { 'type._class': core.class.ArrOf }) + for (const attr of arrs) { + if (attr.name === '_id') { + continue + } + const to = attr.type as RefTo + if (to.to === contact.class.Employee) { + const descendants = control.hierarchy.getDescendants(attr.attributeOf) + for (const d of descendants) { + if (control.hierarchy.findDomain(d) !== undefined) { + const values = await control.findAll(attr.attributeOf, { [attr.name]: sourceAccount.employee }) + const builder = new TxBuilder(control.hierarchy, control.modelDb, modifiedBy) + for (const v of values) { + await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount.employee, targetAccount._id) + } + await control.apply(builder.txes, true, true) + } + } + } + if ( + (to.to === contact.class.EmployeeAccount || to.to === core.class.Account) && + sourceAccount !== undefined && + targetAccount !== undefined + ) { + const descendants = control.hierarchy.getDescendants(attr.attributeOf) + for (const d of descendants) { + if (control.hierarchy.findDomain(d) !== undefined) { + const values = await control.findAll(d, { [attr.name]: sourceAccount._id }) + const builder = new TxBuilder(control.hierarchy, control.modelDb, modifiedBy) + for (const v of values) { + await updateAttribute(builder, v, d, { key: attr.name, attr }, targetAccount._id, targetAccount._id) + } + await control.apply(builder.txes, true, true) + } + } + } + } + const employee = (await control.findAll(contact.class.Employee, { _id: sourceAccount.employee })).shift() + const builder = new TxBuilder(control.hierarchy, control.modelDb, modifiedBy) + await builder.remove(sourceAccount) + if (employee !== undefined) { + await builder.remove(employee) + } + await control.apply(builder.txes, true, true) + + return res +} + +async function mergeEmployee (control: AsyncTriggerControl, uTx: TxUpdateDoc): Promise { + if (uTx.operations.mergedTo === undefined) return [] + const target = uTx.operations.mergedTo + const res: Tx[] = [] + + const attributes = control.hierarchy.getAllAttributes(contact.class.Employee) + for (const attribute of attributes) { if (control.hierarchy.isDerived(attribute[1].type._class, core.class.Collection)) { if (attribute[1]._id === contact.class.Contact + '_channels') continue const collection = attribute[1].type as Collection - const allAttached = await control.findAll(collection.of, { attachedTo: oldValue }) + const allAttached = await control.findAll(collection.of, { attachedTo: uTx.objectId }) for (const attached of allAttached) { const tx = control.txFactory.createTxUpdateDoc(attached._class, attached.space, attached._id, { - attachedTo: newValue + attachedTo: target }) const parent = control.txFactory.createTxCollectionCUD( attached.attachedToClass, - newValue, + target, attached.space, attached.collection, tx @@ -123,125 +235,19 @@ async function mergeCollectionAttributes ( } } } - return res -} -async function processRefAttribute ( - control: TriggerControl, - clazz: Ref>, - attr: AnyAttribute, - key: string, - targetClasses: Ref>[], - oldValue: Ref, - newValue: Ref -): Promise { - const res: Tx[] = [] - if (attr.type._class === core.class.RefTo) { - if (targetClasses.includes((attr.type as RefTo).to)) { - const isMixin = control.hierarchy.isMixin(clazz) - const docs = await control.findAll(clazz, { [key]: oldValue }) - for (const doc of docs) { - if (isMixin) { - const tx = control.txFactory.createTxMixin(doc._id, doc._class, doc.space, clazz, { [key]: newValue }) - res.push(tx) - } else { - const tx = control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { [key]: newValue }) - res.push(tx) - } - } - } - } - return res -} + const oldEmployeeAccount = ( + await control.modelDb.findAll(contact.class.EmployeeAccount, { employee: uTx.objectId }) + )[0] + const newEmployeeAccount = (await control.modelDb.findAll(contact.class.EmployeeAccount, { employee: target }))[0] -async function processRefArrAttribute ( - control: TriggerControl, - clazz: Ref>, - attr: AnyAttribute, - key: string, - targetClasses: Ref>[], - oldValue: Ref, - newValue: Ref -): Promise { - const res: Tx[] = [] - if (attr.type._class === core.class.ArrOf) { - const arrOf = (attr.type as ArrOf>).of - if (arrOf._class === core.class.RefTo) { - if (targetClasses.includes((arrOf as RefTo).to)) { - const docs = await control.findAll(clazz, { [key]: oldValue }) - for (const doc of docs) { - const push = control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { - $push: { - [key]: newValue - } - }) - const pull = control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { - $pull: { - [key]: oldValue - } - }) - res.push(pull) - res.push(push) - } - } - } - } - return res -} - -async function updateAllRefs ( - control: TriggerControl, - _class: Ref>, - oldValue: Ref, - newValue: Ref -): Promise { - const res: Tx[] = [] - const attributes = control.hierarchy.getAllAttributes(_class) - const parent = control.hierarchy.getParentClass(_class) - const mixins = control.hierarchy.getDescendants(parent).filter((p) => control.hierarchy.isMixin(p)) - const colTxes = await mergeCollectionAttributes(control, attributes, oldValue, newValue) - res.push(...colTxes) - for (const mixin of mixins) { - const attributes = control.hierarchy.getOwnAttributes(mixin) - const txes = await mergeCollectionAttributes(control, attributes, oldValue, newValue) - res.push(...txes) - } - - const skip: Ref[] = [] - const allClasses = control.hierarchy.getDescendants(core.class.Doc) - const targetClasses = control.hierarchy.getDescendants(parent) - for (const clazz of allClasses) { - const domain = control.hierarchy.findDomain(clazz) - if (domain === undefined) continue - const attributes = control.hierarchy.getOwnAttributes(clazz) - for (const attribute of attributes) { - const key = attribute[0] - const attr = attribute[1] - if (key === '_id') continue - if (skip.includes(attr._id)) continue - const refs = await processRefAttribute(control, clazz, attr, key, targetClasses, oldValue, newValue) - res.push(...refs) - const arrRef = await processRefArrAttribute(control, clazz, attr, key, targetClasses, oldValue, newValue) - res.push(...arrRef) - } - } - return res -} - -async function mergeEmployee (control: TriggerControl, uTx: TxUpdateDoc): Promise { - if (uTx.operations.mergedTo === undefined) return [] - const target = uTx.operations.mergedTo - const res: Tx[] = [] - const employeeTxes = await updateAllRefs(control, contact.class.Employee, uTx.objectId, target) - res.push(...employeeTxes) - const oldEmployeeAccount = (await control.findAll(contact.class.EmployeeAccount, { employee: uTx.objectId }))[0] - const newEmployeeAccount = (await control.findAll(contact.class.EmployeeAccount, { employee: target }))[0] if (oldEmployeeAccount === undefined || newEmployeeAccount === undefined) return res const accountTxes = await updateAllRefs( control, - contact.class.EmployeeAccount, - oldEmployeeAccount._id, - newEmployeeAccount._id + oldEmployeeAccount, + newEmployeeAccount, + uTx.modifiedOn, + uTx.modifiedBy ) res.push(...accountTxes) return res @@ -250,7 +256,7 @@ async function mergeEmployee (control: TriggerControl, uTx: TxUpdateDoc { +export async function OnEmployeeUpdate (tx: Tx, control: AsyncTriggerControl): Promise { if (tx._class !== core.class.TxUpdateDoc) { return [] } diff --git a/server-plugins/contact/src/index.ts b/server-plugins/contact/src/index.ts index 851f80d917..4b73324d37 100644 --- a/server-plugins/contact/src/index.ts +++ b/server-plugins/contact/src/index.ts @@ -16,7 +16,7 @@ import type { Resource, Plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' -import type { TriggerFunc } from '@hcengineering/server-core' +import type { AsyncTriggerFunc, TriggerFunc } from '@hcengineering/server-core' import { Presenter } from '@hcengineering/server-notification' /** @@ -30,7 +30,7 @@ export const serverContactId = 'server-contact' as Plugin export default plugin(serverContactId, { trigger: { OnContactDelete: '' as Resource, - OnEmployeeUpdate: '' as Resource + OnEmployeeUpdate: '' as Resource }, function: { PersonHTMLPresenter: '' as Resource, diff --git a/server/backup/src/index.ts b/server/backup/src/index.ts index ecaae4ff04..f0b6293a59 100644 --- a/server/backup/src/index.ts +++ b/server/backup/src/index.ts @@ -119,14 +119,16 @@ async function loadDigest ( console.log('loaded', snapshot, dataBlob.length) const addedCount = parseInt(dataBlob.shift() ?? '0') - const added = dataBlob.splice(0, addedCount).map((it) => it.split(';')) - for (const [k, v] of added) { + const added = dataBlob.splice(0, addedCount) + for (const it of added) { + const [k, v] = it.split(';') result.set(k as Ref, v) } const updatedCount = parseInt(dataBlob.shift() ?? '0') - const updated = dataBlob.splice(0, updatedCount).map((it) => it.split(';')) - for (const [k, v] of updated) { + const updated = dataBlob.splice(0, updatedCount) + for (const it of updated) { + const [k, v] = it.split(';') result.set(k as Ref, v) } diff --git a/server/core/src/processor/index.ts b/server/core/src/processor/index.ts index 709344769e..2a3631ab75 100644 --- a/server/core/src/processor/index.ts +++ b/server/core/src/processor/index.ts @@ -46,6 +46,9 @@ export class AsyncTriggerProcessor { txFactory: this.factory, findAll: async (_class, query, options) => { return await this.storage.findAll(this.metrics, _class, query, options) + }, + apply: async (tx: Tx[], broadcast: boolean, updateTx: boolean): Promise => { + await this.storage.apply(this.metrics, tx, broadcast, updateTx) } } } @@ -91,7 +94,7 @@ export class AsyncTriggerProcessor { } } if (result.length > 0) { - await this.storage.apply(this.metrics, result, false) + await this.storage.apply(this.metrics, result, false, false) this.processing = this.doProcessing() } } @@ -114,9 +117,14 @@ export class AsyncTriggerProcessor { result.push(...(await f(doc.tx, this.control))) } } catch (err: any) {} - await this.storage.apply(this.metrics, [this.factory.createTxRemoveDoc(doc._class, doc.space, doc._id)], false) + await this.storage.apply( + this.metrics, + [this.factory.createTxRemoveDoc(doc._class, doc.space, doc._id)], + false, + false + ) - await this.storage.apply(this.metrics, result, true) + await this.storage.apply(this.metrics, result, true, false) } } } diff --git a/server/core/src/storage.ts b/server/core/src/storage.ts index 3ee87b406e..3f58bdfa6d 100644 --- a/server/core/src/storage.ts +++ b/server/core/src/storage.ts @@ -593,14 +593,26 @@ class TServerStorage implements ServerStorage { return { passed, onEnd } } - async apply (ctx: MeasureContext, tx: Tx[], broadcast: boolean): Promise { + async apply (ctx: MeasureContext, tx: Tx[], broadcast: boolean, updateTx: boolean): Promise { const triggerFx = new Effects() const cacheFind = createCacheFindAll(this) const txToStore = tx.filter( (it) => it.space !== core.space.DerivedTx && !this.hierarchy.isDerived(it._class, core.class.TxApplyIf) ) - await ctx.with('domain-tx', {}, async () => await this.getAdapter(DOMAIN_TX).tx(...txToStore)) + if (updateTx) { + const ops = new Map( + tx + .filter((it) => it._class === core.class.TxUpdateDoc) + .map((it) => [(it as TxUpdateDoc).objectId, (it as TxUpdateDoc).operations]) + ) + + if (ops.size > 0) { + await ctx.with('domain-tx-update', {}, async () => await this.getAdapter(DOMAIN_TX).update(DOMAIN_TX, ops)) + } + } else { + await ctx.with('domain-tx', {}, async () => await this.getAdapter(DOMAIN_TX).tx(...txToStore)) + } const removedMap = new Map, Doc>() await ctx.with('apply', {}, (ctx) => this.routeTx(ctx, removedMap, ...tx)) @@ -660,7 +672,7 @@ class TServerStorage implements ServerStorage { ;({ passed, onEnd } = await this.verifyApplyIf(ctx, applyIf, cacheFind)) result = passed if (passed) { - // Store apply if transaction's + // Store apply if transaction's if required await ctx.with('domain-tx', { _class, objClass }, async () => { const atx = await this.getAdapter(DOMAIN_TX) await atx.tx(...applyIf.txes) diff --git a/server/core/src/types.ts b/server/core/src/types.ts index dff08737dd..e369ca0421 100644 --- a/server/core/src/types.ts +++ b/server/core/src/types.ts @@ -117,6 +117,7 @@ export type TriggerFunc = (tx: Tx, ctrl: TriggerControl) => Promise export interface AsyncTriggerControl { txFactory: TxFactory findAll: Storage['findAll'] + apply: (tx: Tx[], broadcast: boolean, updateTx: boolean) => Promise hierarchy: Hierarchy modelDb: ModelDb } diff --git a/server/front/src/index.ts b/server/front/src/index.ts index 8a3f77e794..7c77bf08de 100644 --- a/server/front/src/index.ts +++ b/server/front/src/index.ts @@ -277,7 +277,12 @@ export function start ( const token = authHeader.split(' ')[1] const payload = decodeToken(token) const uuid = req.query.file as string + if (uuid === '') { + res.status(500).send() + return + } + // TODO: We need to allow delete only of user attached documents. (https://front.hc.engineering/workbench/platform/tracker/TSK-1081) await config.minio.remove(payload.workspace, [uuid]) const extra = await config.minio.list(payload.workspace, uuid)