TSK-1062: Fix merge properly (#2919)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-04-07 22:39:53 +07:00 committed by GitHub
parent be23c96c5a
commit 1ad84330cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 693 additions and 384 deletions

View File

@ -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",

View File

@ -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,

View File

@ -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],

View File

@ -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]
})
}

View File

@ -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,

View File

@ -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<Client, 'notify'> {
): Promise<TxResult> {
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<boolean> {
return await ((await this.ops.tx(
this.ops.txFactory.createTxApplyIf(core.space.Tx, this.scope, this.matches, this.txes)
)) as Promise<boolean>)
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<boolean>)
}
return true
}
}
/**
* @public
*
* Builder for TxOperations.
*/
export class TxBuilder extends TxOperations {
txes: TxCUD<Doc>[] = []
matches: DocumentClassQuery<Doc>[] = []
constructor (readonly hierarchy: Hierarchy, readonly modelDb: ModelDb, user: Ref<Account>) {
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<TxResult> => {
if (this.hierarchy.isDerived(tx._class, core.class.TxCUD)) {
this.txes.push(tx as TxCUD<Doc>)
}
return {}
}
}
super(txClient, user)
}
}
/**
* @public
*/
export async function updateAttribute (
client: TxOperations,
object: Doc,
_class: Ref<Class<Doc>>,
attribute: { key: string, attr: AnyAttribute },
value: any,
modifyBy?: Ref<Account>
): Promise<void> {
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)
}
}
}

View File

@ -66,6 +66,6 @@ export interface ServerStorage extends LowLevelStorage {
options?: FindOptions<T>
) => Promise<FindResult<T>>
tx: (ctx: MeasureContext, tx: Tx) => Promise<[TxResult, Tx[]]>
apply: (ctx: MeasureContext, tx: Tx[], broadcast: boolean) => Promise<Tx[]>
apply: (ctx: MeasureContext, tx: Tx[], broadcast: boolean, updateTx: boolean) => Promise<Tx[]>
close: () => Promise<void>
}

View File

@ -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<Class<Doc>>,
attribute: KeyedAttribute,
value: any
): Promise<void> {
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

View File

@ -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}`

View File

@ -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, {

View File

@ -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<Doc>
export let object: Doc
@ -35,9 +36,15 @@
},
{ sort: { modifiedOn: SortingOrder.Descending } }
)
const dispatch = createEventDispatcher()
</script>
<div class="flex flex-between flex-grow p-1 mb-4">
<div
class="flex flex-between flex-grow p-1 mb-4"
use:resizeObserver={() => {
dispatch('changeContent')
}}
>
<div class="fs-title">
<Label label={chunter.string.Comments} />
</div>

View File

@ -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}:",

View File

@ -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": "Граватары управляются через",

View File

@ -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]

View File

@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Employee, EmployeeAccount, getFirstName, getLastName, Person } from '@hcengineering/contact'
import { Channel, Employee, EmployeeAccount, getFirstName, getLastName, Person } from '@hcengineering/contact'
import { AccountRole, getCurrentAccount, Ref } from '@hcengineering/core'
import login from '@hcengineering/login'
import { getResource } from '@hcengineering/platform'
@ -22,12 +22,17 @@
import setting, { IntegrationType } from '@hcengineering/setting'
import { createFocusManager, EditBox, FocusHandler } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import { ChannelsDropdown } from '..'
import contact from '../plugin'
import Avatar from './Avatar.svelte'
import ChannelsEditor from './ChannelsEditor.svelte'
import EditableAvatar from './EditableAvatar.svelte'
export let object: Employee
export let readonly = false
export let channels: Channel[] | undefined = undefined
const client = getClient()
const account = getCurrentAccount() as EmployeeAccount
@ -35,7 +40,7 @@
let avatarEditor: EditableAvatar
$: owner = account.employee === object._id
$: editable = account.role >= AccountRole.Maintainer || owner
$: editable = !readonly && (account.role >= AccountRole.Maintainer || owner)
let firstName = getFirstName(object.name)
let lastName = getLastName(object.name)
let displayName = object.displayName ?? ''
@ -121,6 +126,7 @@
<EditBox
placeholder={contact.string.PersonFirstNamePlaceholder}
bind:value={firstName}
disabled={!editable}
on:change={firstNameChange}
focusIndex={1}
/>
@ -134,6 +140,7 @@
placeholder={contact.string.PersonLastNamePlaceholder}
bind:value={lastName}
on:change={lastNameChange}
disabled={!editable}
focusIndex={2}
/>
{:else}
@ -146,6 +153,7 @@
placeholder={contact.string.DisplayName}
bind:value={displayName}
on:change={changeDisplayName}
disabled={!editable}
focusIndex={1}
/>
{:else}
@ -166,14 +174,25 @@
<div class="separator" />
<div class="flex-row-center">
<ChannelsEditor
attachedTo={object._id}
attachedClass={object._class}
{editable}
bind:integrations
shape={'circle'}
focusIndex={10}
/>
{#if channels === undefined}
<ChannelsEditor
attachedTo={object._id}
attachedClass={object._class}
{editable}
bind:integrations
shape={'circle'}
focusIndex={10}
/>
{:else}
<ChannelsDropdown
value={channels}
editable={false}
kind={'link-bordered'}
size={'small'}
length={'full'}
shape={'circle'}
/>
{/if}
</div>
</div>
</div>

View File

@ -23,6 +23,7 @@
export let targetEmp: Employee
export let key: string
export let onChange: (key: string, value: boolean) => void
export let selected = false
const client = getClient()
const hierarchy = client.getHierarchy()
@ -32,7 +33,14 @@
{#await editor then instance}
{#if instance}
<MergeComparer {value} {targetEmp} {key} {onChange} cast={hierarchy.isMixin(_class) ? _class : undefined}>
<MergeComparer
{value}
{targetEmp}
{key}
{onChange}
cast={hierarchy.isMixin(_class) ? _class : undefined}
{selected}
>
<svelte:fragment slot="item" let:item>
<svelte:component
this={instance}

View File

@ -16,12 +16,13 @@
import { Employee } from '@hcengineering/contact'
import { Doc, Mixin, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Toggle } from '@hcengineering/ui'
import { CheckBox, Label } from '@hcengineering/ui'
export let value: Employee
export let targetEmp: Employee
export let cast: Ref<Mixin<Doc>> | undefined = undefined
export let key: string
export let selected = false
export let onChange: (key: string, value: boolean) => void
const client = getClient()
@ -36,21 +37,64 @@
if (!(targetEmp as any)[key]) return true
return (value as any)[key] === (targetEmp as any)[key]
}
$: attribute = hierarchy.findAttribute(cast ?? value._class, key)
</script>
{#if !isEqual(value, targetEmp, key)}
<div class="flex-center">
<slot name="item" item={value} />
</div>
<div class="flex-center">
<Toggle
on={true}
on:change={(e) => {
onChange(key, e.detail)
}}
/>
</div>
<div class="flex-center">
<slot name="item" item={targetEmp} />
<div class="box flex-row-center flex-between">
<div class="ml-4">
{#if attribute?.label}
<Label label={attribute.label} />
{:else}
{key}
{/if}
</div>
<div class="flex-center">
<div class="mr-2">
<CheckBox
circle
checked={selected}
on:value={(e) => {
selected = false
onChange(key, false)
}}
/>
</div>
<slot name="item" item={value} />
</div>
<div class="flex-row-center" />
<div class="flex-center">
<div class="mr-2">
<CheckBox
circle
checked={!selected}
on:value={(e) => {
selected = true
onChange(key, true)
}}
/>
</div>
<slot name="item" item={targetEmp} />
</div>
</div>
{/if}
<style lang="scss">
.box {
margin: 0.5rem;
padding: 0.5rem;
flex-shrink: 0;
border: 1px dashed var(--accent-color);
border-radius: 0.25rem;
font-weight: 500;
font-size: 0.75rem;
// text-transform: uppercase;
color: var(--accent-color);
&:hover {
color: var(--caption-color);
}
}
</style>

View File

@ -13,18 +13,17 @@
// limitations under the License.
-->
<script lang="ts">
import { Channel, ChannelProvider, Employee, getName } from '@hcengineering/contact'
import { Channel, Employee, getName } from '@hcengineering/contact'
import core, { Doc, DocumentUpdate, Mixin, Ref, TxProcessor } from '@hcengineering/core'
import login from '@hcengineering/login'
import { getResource } from '@hcengineering/platform'
import { Card, createQuery, getClient } from '@hcengineering/presentation'
import { DatePresenter, Grid, Toggle } from '@hcengineering/ui'
import { isCollectionAttr, StringEditor } from '@hcengineering/view-resources'
import { Toggle } from '@hcengineering/ui'
import { isCollectionAttr } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import contact from '../plugin'
import Avatar from './Avatar.svelte'
import ChannelPresenter from './ChannelPresenter.svelte'
import ChannelsDropdown from './ChannelsDropdown.svelte'
import EditEmployee from './EditEmployee.svelte'
import EmployeeBox from './EmployeeBox.svelte'
import MergeAttributeComparer from './MergeAttributeComparer.svelte'
import MergeComparer from './MergeComparer.svelte'
@ -35,20 +34,28 @@
const hierarchy = client.getHierarchy()
const parent = hierarchy.getParentClass(contact.class.Employee)
const mixins = hierarchy.getDescendants(parent).filter((p) => hierarchy.isMixin(p))
let sourceEmployee = value._id
let sourceEmp: Employee | undefined = undefined
let targetEmployee: Ref<Employee> | undefined = undefined
let targetEmp: Employee | undefined = undefined
const targetQuery = createQuery()
$: targetEmployee &&
targetQuery.query(contact.class.Employee, { _id: targetEmployee }, (res) => {
;[targetEmp] = res
update = fillUpdate(value, targetEmp)
mixinUpdate = fillMixinUpdate(value, targetEmp)
result = hierarchy.clone(targetEmp)
applyUpdate(update)
sourceEmployee &&
targetQuery.query(contact.class.Employee, { _id: { $in: [sourceEmployee, targetEmployee] } }, (res) => {
// ;[targetEmp] = res
sourceEmp = res.find((it) => it._id === sourceEmployee)
targetEmp = res.find((it) => it._id === targetEmployee)
if (sourceEmp && targetEmp) {
update = fillUpdate(sourceEmp, targetEmp)
mixinUpdate = fillMixinUpdate(sourceEmp, targetEmp)
applyUpdate(update)
}
})
function fillUpdate (value: Employee, target: Employee): DocumentUpdate<Employee> {
function fillUpdate (source: Employee, target: Employee): DocumentUpdate<Employee> {
const res: DocumentUpdate<Employee> = {}
const attributes = hierarchy.getOwnAttributes(contact.class.Employee)
for (const attribute of attributes) {
@ -56,20 +63,20 @@
if (attribute[1].hidden) continue
if (isCollectionAttr(hierarchy, { key, attr: attribute[1] })) continue
if ((target as any)[key] === undefined) {
;(res as any)[key] = (value as any)[key]
;(res as any)[key] = (source as any)[key]
}
}
return res
}
function fillMixinUpdate (value: Employee, target: Employee): Record<Ref<Mixin<Doc>>, DocumentUpdate<Doc>> {
function fillMixinUpdate (source: Employee, target: Employee): Record<Ref<Mixin<Doc>>, DocumentUpdate<Doc>> {
const res: Record<Ref<Mixin<Doc>>, DocumentUpdate<Doc>> = {}
for (const mixin of mixins) {
if (!hierarchy.hasMixin(value, mixin)) continue
if (!hierarchy.hasMixin(source, mixin)) continue
const attributes = hierarchy.getOwnAttributes(mixin)
for (const attribute of attributes) {
const key = attribute[0]
const from = hierarchy.as(value, mixin)
const from = hierarchy.as(source, mixin)
const to = hierarchy.as(target, mixin)
if ((from as any)[key] !== undefined && (to as any)[key] === undefined) {
const obj: DocumentUpdate<Doc> = res[mixin] ?? {}
@ -83,25 +90,32 @@
let update: DocumentUpdate<Employee> = {}
let mixinUpdate: Record<Ref<Mixin<Doc>>, DocumentUpdate<Doc>> = {}
let result: Employee = value
let result: Employee = { ...value }
function applyUpdate (update: DocumentUpdate<Employee>): void {
result = hierarchy.clone(targetEmp)
TxProcessor.applyUpdate(result, update)
const r = hierarchy.clone(targetEmp)
TxProcessor.applyUpdate(r, update)
result = r
}
async function merge (): Promise<void> {
if (targetEmp === undefined) return
if (sourceEmp === undefined || targetEmp === undefined) return
if (Object.keys(update).length > 0) {
if (update.avatar !== undefined || sourceEmp.avatar === targetEmp.avatar) {
// We replace avatar, we need to update source with target
await client.update(sourceEmp, { avatar: sourceEmp.avatar === targetEmp.avatar ? '' : targetEmp.avatar })
}
await client.update(targetEmp, update)
}
await client.update(value, { mergedTo: targetEmp._id, active: false })
await client.update(sourceEmp, { mergedTo: targetEmp._id, active: false })
for (const channel of resultChannels.values()) {
if (channel.attachedTo === targetEmp._id) continue
if (channel.attachedTo !== targetEmp._id) continue
await client.update(channel, { attachedTo: targetEmp._id })
const remove = targetConflict.get(channel.provider)
if (remove !== undefined) {
await client.remove(remove)
}
for (const old of oldChannels) {
if ((enabledChannels.get(old._id) ?? true) === false) {
await client.remove(old)
}
}
for (const mixin in mixinUpdate) {
@ -112,51 +126,48 @@
await client.createMixin(targetEmp._id, targetEmp._class, targetEmp.space, mixin as Ref<Mixin<Doc>>, {})
}
}
const account = await client.findOne(contact.class.EmployeeAccount, { employee: value._id })
if (account !== undefined) {
const leaveWorkspace = await getResource(login.function.LeaveWorkspace)
await leaveWorkspace(account.email)
}
dispatch('close')
}
function select (field: string, targetValue: boolean) {
if (!targetValue) {
;(update as any)[field] = (value as any)[field]
;(update as any)[field] = (sourceEmp as any)[field]
} else {
delete (update as any)[field]
}
update = update
applyUpdate(update)
}
function mergeChannels (oldChannels: Channel[], targetChannels: Channel[]): Map<Ref<ChannelProvider>, Channel> {
targetConflict.clear()
valueConflict.clear()
const res: Channel[] = [...targetChannels]
const map = new Map(targetChannels.map((p) => [p.provider, p]))
for (const channel of oldChannels) {
if (channel.provider === contact.channelProvider.Email) continue
const target = map.get(channel.provider)
if (target !== undefined) {
targetConflict.set(target.provider, target)
valueConflict.set(channel.provider, channel)
} else {
res.push(channel)
function mergeChannels (
oldChannels: Channel[],
targetChannels: Channel[],
enabledChannels: Map<Ref<Channel>, boolean>
): Channel[] {
const res: Channel[] = []
for (const channel of [...targetChannels, ...oldChannels]) {
// if (channel.provider === contact.channelProvider.Email) continue
const target = enabledChannels.get(channel._id) ?? true
if (target) {
// Add if missing
if (!res.some((it) => it.provider === channel.provider && it.value === channel.value)) {
res.push(channel)
}
}
}
targetConflict = targetConflict
valueConflict = valueConflict
return new Map(res.map((p) => [p.provider, p]))
return res
}
let resultChannels: Map<Ref<ChannelProvider>, Channel> = new Map()
let enabledChannels: Map<Ref<Channel>, boolean> = new Map()
let resultChannels: Channel[] = []
let oldChannels: Channel[] = []
let valueConflict: Map<Ref<ChannelProvider>, Channel> = new Map()
let targetConflict: Map<Ref<ChannelProvider>, Channel> = new Map()
const valueChannelsQuery = createQuery()
valueChannelsQuery.query(contact.class.Channel, { attachedTo: value._id }, (res) => {
$: valueChannelsQuery.query(contact.class.Channel, { attachedTo: sourceEmployee }, (res) => {
oldChannels = res
resultChannels = mergeChannels(oldChannels, targetChannels)
})
let targetChannels: Channel[] = []
@ -164,14 +175,9 @@
$: targetEmployee &&
targetChannelsQuery.query(contact.class.Channel, { attachedTo: targetEmployee }, (res) => {
targetChannels = res
resultChannels = mergeChannels(oldChannels, targetChannels)
})
function selectChannel (isTarget: boolean, targetChannel: Channel, value: Channel) {
const res = isTarget ? targetChannel : value
resultChannels.set(res.provider, res)
resultChannels = resultChannels
}
$: resultChannels = mergeChannels(oldChannels, targetChannels, enabledChannels)
const attributes = hierarchy.getAllAttributes(contact.class.Employee, core.class.Doc)
const ignoreKeys = ['name', 'avatar', 'createOn']
@ -194,6 +200,7 @@
}
mixinUpdate[mixin] = upd
}
const toAny = (a: any) => a
</script>
<Card
@ -204,74 +211,16 @@
canSave={targetEmp !== undefined}
onCancel={() => dispatch('close')}
>
<div class="flex-row-reverse">
<EmployeeBox
showNavigate={false}
label={contact.string.MergeEmployee}
docQuery={{ active: true, _id: { $ne: value._id } }}
bind:value={targetEmployee}
/>
</div>
{#if targetEmp}
<Grid column={3} rowGap={0.5} columnGap={0.5}>
<MergeComparer key="avatar" {value} {targetEmp} onChange={select}>
<svelte:fragment slot="item" let:item>
<Avatar avatar={item.avatar} size={'medium'} icon={contact.icon.Person} />
</svelte:fragment>
</MergeComparer>
<MergeComparer key="name" {value} {targetEmp} onChange={select}>
<svelte:fragment slot="item" let:item>
{getName(item)}
</svelte:fragment>
</MergeComparer>
{#each objectAttributes as attribute}
<MergeAttributeComparer
key={attribute[0]}
{value}
{targetEmp}
onChange={select}
_class={contact.class.Employee}
/>
{/each}
{#each mixins as mixin}
{@const attributes = getMixinAttributes(mixin)}
{#each attributes as attribute}
<MergeAttributeComparer
key={attribute}
{value}
{targetEmp}
onChange={(key, value) => selectMixin(mixin, key, value)}
_class={mixin}
/>
{/each}
{/each}
{#each Array.from(targetConflict.values()) as conflict}
{@const val = valueConflict.get(conflict.provider)}
{#if val}
<div class="flex-center max-w-40">
<ChannelPresenter value={val} />
</div>
<div class="flex-center">
<Toggle
on={true}
on:change={(e) => {
selectChannel(e.detail, conflict, val)
}}
/>
</div>
<div class="flex-center max-w-40">
<ChannelPresenter value={conflict} />
</div>
{/if}
{/each}
</Grid>
<div class="flex-col-center">
<Avatar avatar={result.avatar} size={'large'} icon={contact.icon.Person} />
{getName(result)}
<DatePresenter value={result.birthday} />
<StringEditor value={result.city} readonly placeholder={contact.string.Location} />
<div class="flex-row flex-between">
<div class="flex-row-center">
<EmployeeBox
showNavigate={false}
label={contact.string.MergeEmployeeFrom}
docQuery={{ active: { $in: [true, false] } }}
bind:value={sourceEmployee}
/>
<ChannelsDropdown
value={Array.from(resultChannels.values())}
value={oldChannels}
editable={false}
kind={'link-bordered'}
size={'small'}
@ -279,5 +228,85 @@
shape={'circle'}
/>
</div>
{/if}
>>
<div class="flex-row-center">
<EmployeeBox
showNavigate={false}
label={contact.string.MergeEmployeeTo}
docQuery={{ active: true }}
bind:value={targetEmployee}
/>
<ChannelsDropdown
value={targetChannels}
editable={false}
kind={'link-bordered'}
size={'small'}
length={'full'}
shape={'circle'}
/>
</div>
</div>
{#key [targetEmployee, sourceEmployee]}
{#if targetEmp && sourceEmp}
<div class="flex-col flex-grow">
<MergeComparer
key="avatar"
value={sourceEmp}
{targetEmp}
onChange={select}
selected={update.avatar !== undefined}
>
<svelte:fragment slot="item" let:item>
<Avatar avatar={item.avatar} size={'x-large'} icon={contact.icon.Person} />
</svelte:fragment>
</MergeComparer>
<MergeComparer key="name" value={sourceEmp} {targetEmp} onChange={select} selected={update.name !== undefined}>
<svelte:fragment slot="item" let:item>
{getName(item)}
</svelte:fragment>
</MergeComparer>
{#each objectAttributes as attribute}
<MergeAttributeComparer
key={attribute[0]}
value={sourceEmp}
{targetEmp}
onChange={select}
_class={contact.class.Employee}
selected={toAny(update)[attribute[0]] !== undefined}
/>
{/each}
{#each mixins as mixin}
{@const attributes = getMixinAttributes(mixin)}
{#each attributes as attribute}
<MergeAttributeComparer
key={attribute}
value={sourceEmp}
{targetEmp}
onChange={(key, value) => 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}
<div class="flex-row-center flex-between">
<ChannelPresenter value={channel} />
<div class="flex-center">
<Toggle
on={enabled}
on:change={(e) => {
enabledChannels.set(channel._id, e.detail)
enabledChannels = enabledChannels
}}
/>
</div>
</div>
{/each}
</div>
<div class="flex-col-center antiPopup p-4">
<EditEmployee object={result} readonly channels={resultChannels} />
</div>
{/if}
{/key}
</Card>

View File

@ -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')
}}
>
<DropdownLabelsIntl
items={getAvatarTypeDropdownItems(hasGravatar)}
label={contact.string.SelectAvatar}
bind:selected={selectedAvatarType}
on:selected={handleDropdownSelection}
/>
<div class="flex-row-center">
<Label label={contact.string.AvatarProvider} />
<DropdownLabelsIntl
kind={'link-bordered'}
items={getAvatarTypeDropdownItems(hasGravatar)}
label={contact.string.SelectAvatar}
bind:selected={selectedAvatarType}
on:selected={handleDropdownSelection}
/>
</div>
{#if selectedAvatarType === AvatarType.IMAGE}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="cursor-pointer" on:click|self={handleImageAvatarClick}>

View File

@ -188,6 +188,22 @@ async function doContactQuery<T extends Contact> (
async function kickEmployee (doc: Employee): Promise<void> {
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 {

View File

@ -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<string, any>) => Promise<Location>>,

View File

@ -266,4 +266,5 @@ export function getAvatarProviderId (avatar?: string | null): Ref<AvatarProvider
case AvatarType.COLOR:
return contact.avatarProvider.Color
}
return contact.avatarProvider.Image
}

View File

@ -16,14 +16,16 @@
import { AttachmentsPresenter } from '@hcengineering/attachment-resources'
import { CommentsPresenter } from '@hcengineering/chunter-resources'
import contact, { getName } from '@hcengineering/contact'
import { Avatar } from '@hcengineering/contact-resources'
import { Hierarchy, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Avatar } from '@hcengineering/contact-resources'
import type { Applicant, Candidate } from '@hcengineering/recruit'
import recruit from '@hcengineering/recruit'
import { AssigneePresenter } from '@hcengineering/task-resources'
import tracker from '@hcengineering/tracker'
import { Component, showPanel } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { ObjectPresenter } from '@hcengineering/view-resources'
import ApplicationPresenter from './ApplicationPresenter.svelte'
export let object: WithLookup<Applicant>
@ -34,10 +36,20 @@
}
$: channels = (object.$lookup?.attachedTo as WithLookup<Candidate>)?.$lookup?.channels
$: company = object?.$lookup?.space?.company
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-col pt-2 pb-2 pr-4 pl-4 cursor-pointer" on:click={showCandidate}>
<div class="p-1 flex-between">
<ObjectPresenter _class={recruit.class.Vacancy} objectId={object.space} value={object.$lookup?.space} />
{#if company}
<div class="ml-2">
<ObjectPresenter _class={contact.class.Organization} objectId={company} />
</div>
{/if}
</div>
<div class="flex-between mb-3">
<div class="flex-row-center">
<Avatar avatar={object.$lookup?.attachedTo?.avatar} size={'medium'} />
@ -69,7 +81,9 @@
<div class="flex-between">
<div class="flex-row-center">
<div class="sm-tool-icon step-lr75">
<ApplicationPresenter value={object} />
<div class="mr-2">
<ApplicationPresenter value={object} />
</div>
<Component is={tracker.component.RelatedIssueSelector} props={{ object }} />
</div>
{#if (object.attachments ?? 0) > 0}
@ -77,9 +91,14 @@
<AttachmentsPresenter value={object.attachments} {object} />
</div>
{/if}
{#if (object.comments ?? 0) > 0}
{#if (object.comments ?? 0) > 0 || (object.$lookup?.attachedTo !== undefined && (object.$lookup.attachedTo.comments ?? 0) > 0)}
<div class="step-lr75">
<CommentsPresenter value={object.comments} {object} />
{#if (object.comments ?? 0) > 0}
<CommentsPresenter value={object.comments} {object} />
{/if}
{#if object.$lookup?.attachedTo !== undefined && (object.$lookup.attachedTo.comments ?? 0) > 0}
<CommentsPresenter value={object.$lookup?.attachedTo?.comments} object={object.$lookup?.attachedTo} />
{/if}
</div>
{/if}
</div>

View File

@ -108,4 +108,5 @@
onEmployeeEdit={handleAssigneeEditorOpened}
tooltipLabels={{ personLabel: task.string.TaskAssignee, placeholderLabel: task.string.TaskUnAssign }}
/>
<!-- TODO: Change assignee -->
{/if}

View File

@ -190,7 +190,11 @@
{/if}
</div>
<div class="dark-color ml-2">
{#if isStatus}{statusesCount[i]}{:else}{targets.get(value?._id)}{/if}
{#if isStatus}
{statusesCount[i] ?? ''}
{:else}
{targets.get(value?._id) ?? ''}
{/if}
</div>
</div>
</button>

View File

@ -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" ]

View File

@ -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)

View File

@ -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<T extends Doc> (
control: TriggerControl,
attributes: Map<string, AnyAttribute>,
oldValue: Ref<T>,
newValue: Ref<T>
async function updateAllRefs (
control: AsyncTriggerControl,
sourceAccount: EmployeeAccount,
targetAccount: EmployeeAccount,
modifiedOn: Timestamp,
modifiedBy: Ref<Account>
): Promise<Tx[]> {
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<Doc>
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<Doc>
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<Doc>
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<Employee>): Promise<Tx[]> {
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<AttachedDoc>
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<T extends Doc> (
}
}
}
return res
}
async function processRefAttribute<T extends Doc> (
control: TriggerControl,
clazz: Ref<Class<Obj>>,
attr: AnyAttribute,
key: string,
targetClasses: Ref<Class<Obj>>[],
oldValue: Ref<T>,
newValue: Ref<T>
): Promise<Tx[]> {
const res: Tx[] = []
if (attr.type._class === core.class.RefTo) {
if (targetClasses.includes((attr.type as RefTo<Doc>).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<T extends Doc> (
control: TriggerControl,
clazz: Ref<Class<Obj>>,
attr: AnyAttribute,
key: string,
targetClasses: Ref<Class<Obj>>[],
oldValue: Ref<T>,
newValue: Ref<T>
): Promise<Tx[]> {
const res: Tx[] = []
if (attr.type._class === core.class.ArrOf) {
const arrOf = (attr.type as ArrOf<RefTo<Doc>>).of
if (arrOf._class === core.class.RefTo) {
if (targetClasses.includes((arrOf as RefTo<Doc>).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<T extends Doc> (
control: TriggerControl,
_class: Ref<Class<T>>,
oldValue: Ref<T>,
newValue: Ref<T>
): Promise<Tx[]> {
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<AnyAttribute>[] = []
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<Employee>): Promise<Tx[]> {
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<Employee
/**
* @public
*/
export async function OnEmployeeUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
export async function OnEmployeeUpdate (tx: Tx, control: AsyncTriggerControl): Promise<Tx[]> {
if (tx._class !== core.class.TxUpdateDoc) {
return []
}

View File

@ -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<TriggerFunc>,
OnEmployeeUpdate: '' as Resource<TriggerFunc>
OnEmployeeUpdate: '' as Resource<AsyncTriggerFunc>
},
function: {
PersonHTMLPresenter: '' as Resource<Presenter>,

View File

@ -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<Doc>, 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<Doc>, v)
}

View File

@ -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<void> => {
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)
}
}
}

View File

@ -593,14 +593,26 @@ class TServerStorage implements ServerStorage {
return { passed, onEnd }
}
async apply (ctx: MeasureContext, tx: Tx[], broadcast: boolean): Promise<Tx[]> {
async apply (ctx: MeasureContext, tx: Tx[], broadcast: boolean, updateTx: boolean): Promise<Tx[]> {
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<Tx>).objectId, (it as TxUpdateDoc<Tx>).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<Ref<Doc>, 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)

View File

@ -117,6 +117,7 @@ export type TriggerFunc = (tx: Tx, ctrl: TriggerControl) => Promise<Tx[]>
export interface AsyncTriggerControl {
txFactory: TxFactory
findAll: Storage['findAll']
apply: (tx: Tx[], broadcast: boolean, updateTx: boolean) => Promise<void>
hierarchy: Hierarchy
modelDb: ModelDb
}

View File

@ -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)