mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-03 08:57:14 +03:00
TSK-1062: Fix merge properly (#2919)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
be23c96c5a
commit
1ad84330cd
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
|
@ -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]
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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}`
|
||||
|
@ -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, {
|
||||
|
@ -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>
|
||||
|
@ -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}:",
|
||||
|
@ -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": "Граватары управляются через",
|
||||
|
@ -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]
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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 {
|
||||
|
@ -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>>,
|
||||
|
@ -266,4 +266,5 @@ export function getAvatarProviderId (avatar?: string | null): Ref<AvatarProvider
|
||||
case AvatarType.COLOR:
|
||||
return contact.avatarProvider.Color
|
||||
}
|
||||
return contact.avatarProvider.Image
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -108,4 +108,5 @@
|
||||
onEmployeeEdit={handleAssigneeEditorOpened}
|
||||
tooltipLabels={{ personLabel: task.string.TaskAssignee, placeholderLabel: task.string.TaskUnAssign }}
|
||||
/>
|
||||
<!-- TODO: Change assignee -->
|
||||
{/if}
|
||||
|
@ -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>
|
||||
|
@ -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" ]
|
||||
|
@ -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)
|
||||
|
@ -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 []
|
||||
}
|
||||
|
@ -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>,
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user