diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index a0f33937a4..71ebf62cf5 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -156,6 +156,8 @@ export class TEmployee extends TPerson implements Employee { @Prop(Collection(contact.class.Status), contact.string.Status) statuses?: number + + mergedTo?: Ref } @Model(contact.class.EmployeeAccount, core.class.Account) @@ -563,6 +565,30 @@ export function createModel (builder: Builder): void { contact.action.KickEmployee ) + createAction( + builder, + { + action: view.actionImpl.ShowPopup, + actionProps: { + component: contact.component.MergeEmployee, + element: 'top', + fillProps: { + _object: 'value' + } + }, + label: contact.string.MergeEmployee, + category: contact.category.Contact, + target: contact.class.Employee, + input: 'focus', + context: { + mode: ['context'], + group: 'other' + }, + secured: true + }, + contact.action.MergeEmployee + ) + // Allow to use fuzzy search for mixins builder.mixin(contact.class.Contact, core.class.Class, core.mixin.FullTextSearchContext, { fullTextSummary: true diff --git a/models/contact/src/plugin.ts b/models/contact/src/plugin.ts index 7f719c1d7a..2082a6e6cc 100644 --- a/models/contact/src/plugin.ts +++ b/models/contact/src/plugin.ts @@ -46,7 +46,8 @@ export default mergeIds(contactId, contact, { EmployeeEditor: '' as AnyComponent, CreateEmployee: '' as AnyComponent, AccountArrayEditor: '' as AnyComponent, - ChannelFilter: '' as AnyComponent + ChannelFilter: '' as AnyComponent, + MergeEmployee: '' as AnyComponent }, string: { Persons: '' as IntlString, @@ -55,7 +56,6 @@ export default mergeIds(contactId, contact, { SearchOrganization: '' as IntlString, ContactInfo: '' as IntlString, Contact: '' as IntlString, - Location: '' as IntlString, Channel: '' as IntlString, ChannelProvider: '' as IntlString, Value: '' as IntlString, @@ -91,7 +91,8 @@ export default mergeIds(contactId, contact, { Contact: '' as Ref }, action: { - KickEmployee: '' as Ref + KickEmployee: '' as Ref, + MergeEmployee: '' as Ref }, actionImpl: { KickEmployee: '' as ViewAction, diff --git a/models/server-contact/src/index.ts b/models/server-contact/src/index.ts index 14062ad286..062a147f53 100644 --- a/models/server-contact/src/index.ts +++ b/models/server-contact/src/index.ts @@ -42,4 +42,8 @@ export function createModel (builder: Builder): void { builder.createDoc(serverCore.class.Trigger, core.space.Model, { trigger: serverContact.trigger.OnContactDelete }) + + builder.createDoc(serverCore.class.Trigger, core.space.Model, { + trigger: serverContact.trigger.OnEmployeeUpdate + }) } diff --git a/packages/theme/styles/_layouts.scss b/packages/theme/styles/_layouts.scss index 4ff720e74f..9c8cf5bba1 100644 --- a/packages/theme/styles/_layouts.scss +++ b/packages/theme/styles/_layouts.scss @@ -576,6 +576,7 @@ input.search { .max-w-2 { max-width: .5rem; } .max-w-9 { max-width: 2.25rem; } .max-w-30 { max-width: 7.5rem; } +.max-w-40 { max-width: 10rem; } .max-w-60 { max-width: 15rem; } .max-w-80 { max-width: 20rem; } .max-w-240 { max-width: 60rem; } diff --git a/packages/ui/src/components/CircleButton.svelte b/packages/ui/src/components/CircleButton.svelte index 78c2879bac..06605747e6 100644 --- a/packages/ui/src/components/CircleButton.svelte +++ b/packages/ui/src/components/CircleButton.svelte @@ -14,11 +14,11 @@ --> diff --git a/plugins/contact-assets/lang/en.json b/plugins/contact-assets/lang/en.json index b23af7be1e..e895bc3c27 100644 --- a/plugins/contact-assets/lang/en.json +++ b/plugins/contact-assets/lang/en.json @@ -75,6 +75,7 @@ "WhatsappPlaceholder": "Whatsapp", "Profile": "Profile", "ProfilePlaceholder": "Profile...", - "CurrentEmployee": "Current employee" + "CurrentEmployee": "Current employee", + "MergeEmployee": "Merge employee" } } \ No newline at end of file diff --git a/plugins/contact-assets/lang/ru.json b/plugins/contact-assets/lang/ru.json index 32fafa00fa..faf318ccd9 100644 --- a/plugins/contact-assets/lang/ru.json +++ b/plugins/contact-assets/lang/ru.json @@ -75,6 +75,7 @@ "WhatsappPlaceholder": "Whatsapp", "Profile": "Профиль", "ProfilePlaceholder": "Профиль...", - "CurrentEmployee": "Текущий сотрудник" + "CurrentEmployee": "Текущий сотрудник", + "MergeEmployee": "Объеденить сотрудника" } } \ No newline at end of file diff --git a/plugins/contact-resources/src/components/ChannelPresenter.svelte b/plugins/contact-resources/src/components/ChannelPresenter.svelte new file mode 100644 index 0000000000..5d55b1bf7b --- /dev/null +++ b/plugins/contact-resources/src/components/ChannelPresenter.svelte @@ -0,0 +1,34 @@ + + + +
+ {#if provider} + +
{value.value}
+ {/if} +
diff --git a/plugins/contact-resources/src/components/MergeAttributeComparer.svelte b/plugins/contact-resources/src/components/MergeAttributeComparer.svelte new file mode 100644 index 0000000000..b02c600e36 --- /dev/null +++ b/plugins/contact-resources/src/components/MergeAttributeComparer.svelte @@ -0,0 +1,50 @@ + + + +{#await editor then instance} + {#if instance} + + + + + + {/if} +{/await} diff --git a/plugins/contact-resources/src/components/MergeComparer.svelte b/plugins/contact-resources/src/components/MergeComparer.svelte new file mode 100644 index 0000000000..263251d63d --- /dev/null +++ b/plugins/contact-resources/src/components/MergeComparer.svelte @@ -0,0 +1,56 @@ + + + +{#if !isEqual(value, targetEmp, key)} +
+ +
+
+ { + onChange(key, e.detail) + }} + /> +
+
+ +
+{/if} diff --git a/plugins/contact-resources/src/components/MergeEmployee.svelte b/plugins/contact-resources/src/components/MergeEmployee.svelte new file mode 100644 index 0000000000..df353dbb1e --- /dev/null +++ b/plugins/contact-resources/src/components/MergeEmployee.svelte @@ -0,0 +1,279 @@ + + + + dispatch('close')} +> +
+ +
+ {#if targetEmp} + + + + + + + + + {formatName(item.name)} + + + {#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} +
+
+ + {formatName(result.name)} + + + +
+ {/if} +
diff --git a/plugins/contact-resources/src/index.ts b/plugins/contact-resources/src/index.ts index 4dce17dd54..71c598530b 100644 --- a/plugins/contact-resources/src/index.ts +++ b/plugins/contact-resources/src/index.ts @@ -54,6 +54,7 @@ import PersonRefPresenter from './components/PersonRefPresenter.svelte' import EmployeeRefPresenter from './components/EmployeeRefPresenter.svelte' import ChannelFilter from './components/ChannelFilter.svelte' import AccountBox from './components/AccountBox.svelte' +import MergeEmployee from './components/MergeEmployee.svelte' import contact from './plugin' import { employeeSort, @@ -115,21 +116,24 @@ async function queryContact ( async function kickEmployee (doc: Employee): Promise { const client = getClient() const email = await client.findOne(contact.class.EmployeeAccount, { employee: doc._id }) - if (email === undefined) return - showPopup( - MessageBox, - { - label: contact.string.KickEmployee, - message: contact.string.KickEmployeeDescr - }, - undefined, - (res?: boolean) => { - if (res === true) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - leaveWorkspace(email.email) + if (email === undefined) { + await client.update(doc, { active: false }) + } else { + showPopup( + MessageBox, + { + label: contact.string.KickEmployee, + message: contact.string.KickEmployeeDescr + }, + undefined, + (res?: boolean) => { + if (res === true) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + leaveWorkspace(email.email) + } } - } - ) + ) + } } async function openChannelURL (doc: Channel): Promise { if (doc.value.startsWith('http://') || doc.value.startsWith('https://')) { @@ -176,7 +180,8 @@ export default async (): Promise => ({ EmployeeEditor, CreateEmployee, AccountArrayEditor, - ChannelFilter + ChannelFilter, + MergeEmployee }, completion: { EmployeeQuery: async ( diff --git a/plugins/contact-resources/src/plugin.ts b/plugins/contact-resources/src/plugin.ts index b8f7f20905..08897bac00 100644 --- a/plugins/contact-resources/src/plugin.ts +++ b/plugins/contact-resources/src/plugin.ts @@ -42,6 +42,7 @@ export default mergeIds(contactId, contact, { SetStatus: '' as IntlString, ClearStatus: '' as IntlString, SaveStatus: '' as IntlString, + Location: '' as IntlString, Cancel: '' as IntlString, StatusDueDate: '' as IntlString, StatusName: '' as IntlString, @@ -59,7 +60,8 @@ export default mergeIds(contactId, contact, { Email: '' as IntlString, CreateEmployee: '' as IntlString, Inactive: '' as IntlString, - NotSpecified: '' as IntlString + NotSpecified: '' as IntlString, + MergeEmployee: '' as IntlString }, function: { EmployeeSort: '' as SortFunc, diff --git a/plugins/contact/src/index.ts b/plugins/contact/src/index.ts index 3cb4df2a64..83cb8f4695 100644 --- a/plugins/contact/src/index.ts +++ b/plugins/contact/src/index.ts @@ -142,6 +142,7 @@ export interface Status extends AttachedDoc { */ export interface Employee extends Person { active: boolean + mergedTo?: Ref statuses?: number } diff --git a/plugins/view-resources/src/components/StringEditor.svelte b/plugins/view-resources/src/components/StringEditor.svelte index 9c297f34dd..f886bea5fb 100644 --- a/plugins/view-resources/src/components/StringEditor.svelte +++ b/plugins/view-resources/src/components/StringEditor.svelte @@ -22,8 +22,8 @@ // export let label: IntlString export let placeholder: IntlString export let value: string - export let focus: boolean - export let onChange: (value: string) => void + export let focus: boolean = false + export let onChange: (value: string) => void = () => {} export let kind: 'no-border' | 'link' = 'no-border' export let readonly = false export let size: ButtonSize = 'small' diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index e88aaa0827..9d9b64419e 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -142,7 +142,8 @@ export { SortableListItem, MarkupEditor, TreeNode, - TreeItem + TreeItem, + StringEditor } export default async (): Promise => ({ diff --git a/server-plugins/contact-resources/src/index.ts b/server-plugins/contact-resources/src/index.ts index f5e0b982a8..c8f90dc0ef 100644 --- a/server-plugins/contact-resources/src/index.ts +++ b/server-plugins/contact-resources/src/index.ts @@ -1,6 +1,6 @@ // // Copyright © 2020, 2021 Anticrm Platform Contributors. -// Copyright © 2021, 2022 Hardcore Engineering Inc. +// Copyright © 2021, 2022, 2023 Hardcore Engineering Inc. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may @@ -14,8 +14,22 @@ // limitations under the License. // -import contact, { Contact, contactId, formatName, Organization, Person } from '@hcengineering/contact' -import core, { concatLink, Doc, Tx, TxRemoveDoc } from '@hcengineering/core' +import contact, { Contact, contactId, Employee, formatName, Organization, Person } from '@hcengineering/contact' +import core, { + AnyAttribute, + ArrOf, + AttachedDoc, + Class, + Collection, + concatLink, + Doc, + Obj, + Ref, + RefTo, + Tx, + TxRemoveDoc, + TxUpdateDoc +} from '@hcengineering/core' import login from '@hcengineering/login' import { getMetadata } from '@hcengineering/platform' import type { TriggerControl } from '@hcengineering/server-core' @@ -84,6 +98,179 @@ export async function OnContactDelete ( return result } +async function mergeCollectionAttributes ( + control: TriggerControl, + attributes: Map, + oldValue: Ref, + newValue: Ref +): Promise { + const res: Tx[] = [] + 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 }) + for (const attached of allAttached) { + const tx = control.txFactory.createTxUpdateDoc(attached._class, attached.space, attached._id, { + attachedTo: newValue + }) + const parent = control.txFactory.createTxCollectionCUD( + attached.attachedToClass, + newValue, + attached.space, + attached.collection, + tx + ) + res.push(parent) + } + } + } + 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 +} + +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.ArrOf) { + 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 + ) + res.push(...accountTxes) + return res +} + +/** + * @public + */ +export async function OnEmployeeUpdate (tx: Tx, control: TriggerControl): Promise { + if (tx._class !== core.class.TxUpdateDoc) { + return [] + } + + const uTx = tx as TxUpdateDoc + + if (!control.hierarchy.isDerived(uTx.objectClass, contact.class.Employee)) { + return [] + } + + const result: Tx[] = [] + + const txes = await mergeEmployee(control, uTx) + result.push(...txes) + + return result +} + /** * @public */ @@ -125,7 +312,8 @@ export function organizationTextPresenter (doc: Doc): string { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { - OnContactDelete + OnContactDelete, + OnEmployeeUpdate }, function: { PersonHTMLPresenter: personHTMLPresenter, diff --git a/server-plugins/contact/src/index.ts b/server-plugins/contact/src/index.ts index c77f65a798..851f80d917 100644 --- a/server-plugins/contact/src/index.ts +++ b/server-plugins/contact/src/index.ts @@ -29,7 +29,8 @@ export const serverContactId = 'server-contact' as Plugin */ export default plugin(serverContactId, { trigger: { - OnContactDelete: '' as Resource + OnContactDelete: '' as Resource, + OnEmployeeUpdate: '' as Resource }, function: { PersonHTMLPresenter: '' as Resource,