2023-03-03 17:02:11 +00:00
|
|
|
<!--
|
|
|
|
// Copyright © 2023 Hardcore Engineering Inc.
|
|
|
|
//
|
|
|
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License. You may
|
|
|
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
//
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
-->
|
|
|
|
<script lang="ts">
|
2023-04-07 15:39:53 +00:00
|
|
|
import { Channel, Employee, getName } from '@hcengineering/contact'
|
2023-03-03 17:02:11 +00:00
|
|
|
import core, { Doc, DocumentUpdate, Mixin, Ref, TxProcessor } from '@hcengineering/core'
|
2023-03-22 02:48:57 +00:00
|
|
|
import { Card, createQuery, getClient } from '@hcengineering/presentation'
|
2023-04-07 15:39:53 +00:00
|
|
|
import { Toggle } from '@hcengineering/ui'
|
|
|
|
import { isCollectionAttr } from '@hcengineering/view-resources'
|
2023-03-03 17:02:11 +00:00
|
|
|
import { createEventDispatcher } from 'svelte'
|
|
|
|
import contact from '../plugin'
|
2023-03-22 02:48:57 +00:00
|
|
|
import Avatar from './Avatar.svelte'
|
2023-03-03 17:02:11 +00:00
|
|
|
import ChannelPresenter from './ChannelPresenter.svelte'
|
|
|
|
import ChannelsDropdown from './ChannelsDropdown.svelte'
|
2023-04-07 15:39:53 +00:00
|
|
|
import EditEmployee from './EditEmployee.svelte'
|
2023-03-22 02:48:57 +00:00
|
|
|
import EmployeeBox from './EmployeeBox.svelte'
|
2023-03-03 17:02:11 +00:00
|
|
|
import MergeAttributeComparer from './MergeAttributeComparer.svelte'
|
|
|
|
import MergeComparer from './MergeComparer.svelte'
|
|
|
|
|
|
|
|
export let value: Employee
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
const client = getClient()
|
|
|
|
const hierarchy = client.getHierarchy()
|
|
|
|
const parent = hierarchy.getParentClass(contact.class.Employee)
|
|
|
|
const mixins = hierarchy.getDescendants(parent).filter((p) => hierarchy.isMixin(p))
|
2023-04-07 15:39:53 +00:00
|
|
|
|
|
|
|
let sourceEmployee = value._id
|
|
|
|
let sourceEmp: Employee | undefined = undefined
|
|
|
|
|
2023-03-03 17:02:11 +00:00
|
|
|
let targetEmployee: Ref<Employee> | undefined = undefined
|
|
|
|
let targetEmp: Employee | undefined = undefined
|
|
|
|
|
|
|
|
const targetQuery = createQuery()
|
|
|
|
$: targetEmployee &&
|
2023-04-07 15:39:53 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-03-03 17:02:11 +00:00
|
|
|
})
|
|
|
|
|
2023-04-07 15:39:53 +00:00
|
|
|
function fillUpdate (source: Employee, target: Employee): DocumentUpdate<Employee> {
|
2023-03-03 17:02:11 +00:00
|
|
|
const res: DocumentUpdate<Employee> = {}
|
|
|
|
const attributes = hierarchy.getOwnAttributes(contact.class.Employee)
|
|
|
|
for (const attribute of attributes) {
|
|
|
|
const key = attribute[0]
|
|
|
|
if (attribute[1].hidden) continue
|
|
|
|
if (isCollectionAttr(hierarchy, { key, attr: attribute[1] })) continue
|
|
|
|
if ((target as any)[key] === undefined) {
|
2023-04-07 15:39:53 +00:00
|
|
|
;(res as any)[key] = (source as any)[key]
|
2023-03-03 17:02:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2023-04-07 15:39:53 +00:00
|
|
|
function fillMixinUpdate (source: Employee, target: Employee): Record<Ref<Mixin<Doc>>, DocumentUpdate<Doc>> {
|
2023-03-03 17:02:11 +00:00
|
|
|
const res: Record<Ref<Mixin<Doc>>, DocumentUpdate<Doc>> = {}
|
|
|
|
for (const mixin of mixins) {
|
2023-04-07 15:39:53 +00:00
|
|
|
if (!hierarchy.hasMixin(source, mixin)) continue
|
2023-03-03 17:02:11 +00:00
|
|
|
const attributes = hierarchy.getOwnAttributes(mixin)
|
|
|
|
for (const attribute of attributes) {
|
|
|
|
const key = attribute[0]
|
2023-04-07 15:39:53 +00:00
|
|
|
const from = hierarchy.as(source, mixin)
|
2023-03-03 17:02:11 +00:00
|
|
|
const to = hierarchy.as(target, mixin)
|
|
|
|
if ((from as any)[key] !== undefined && (to as any)[key] === undefined) {
|
|
|
|
const obj: DocumentUpdate<Doc> = res[mixin] ?? {}
|
|
|
|
;(obj as any)[key] = (from as any)[key]
|
|
|
|
res[mixin] = obj
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
let update: DocumentUpdate<Employee> = {}
|
|
|
|
let mixinUpdate: Record<Ref<Mixin<Doc>>, DocumentUpdate<Doc>> = {}
|
2023-04-07 15:39:53 +00:00
|
|
|
|
|
|
|
let result: Employee = { ...value }
|
2023-03-03 17:02:11 +00:00
|
|
|
|
|
|
|
function applyUpdate (update: DocumentUpdate<Employee>): void {
|
2023-04-07 15:39:53 +00:00
|
|
|
const r = hierarchy.clone(targetEmp)
|
|
|
|
TxProcessor.applyUpdate(r, update)
|
|
|
|
result = r
|
2023-03-03 17:02:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function merge (): Promise<void> {
|
2023-04-07 15:39:53 +00:00
|
|
|
if (sourceEmp === undefined || targetEmp === undefined) return
|
2023-03-03 17:02:11 +00:00
|
|
|
if (Object.keys(update).length > 0) {
|
2023-04-07 15:39:53 +00:00
|
|
|
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 })
|
|
|
|
}
|
2023-03-03 17:02:11 +00:00
|
|
|
await client.update(targetEmp, update)
|
|
|
|
}
|
2023-04-07 15:39:53 +00:00
|
|
|
await client.update(sourceEmp, { mergedTo: targetEmp._id, active: false })
|
2023-03-03 17:02:11 +00:00
|
|
|
for (const channel of resultChannels.values()) {
|
2023-04-07 15:39:53 +00:00
|
|
|
if (channel.attachedTo !== targetEmp._id) continue
|
2023-03-03 17:02:11 +00:00
|
|
|
await client.update(channel, { attachedTo: targetEmp._id })
|
2023-04-07 15:39:53 +00:00
|
|
|
}
|
|
|
|
for (const old of oldChannels) {
|
|
|
|
if ((enabledChannels.get(old._id) ?? true) === false) {
|
|
|
|
await client.remove(old)
|
2023-03-03 17:02:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const mixin in mixinUpdate) {
|
|
|
|
const attrs = (mixinUpdate as any)[mixin]
|
|
|
|
if (Object.keys(attrs).length > 0) {
|
|
|
|
await client.updateMixin(targetEmp._id, targetEmp._class, targetEmp.space, mixin as Ref<Mixin<Doc>>, attrs)
|
|
|
|
} else if (!hierarchy.hasMixin(targetEmp, mixin as Ref<Mixin<Doc>>)) {
|
|
|
|
await client.createMixin(targetEmp._id, targetEmp._class, targetEmp.space, mixin as Ref<Mixin<Doc>>, {})
|
|
|
|
}
|
|
|
|
}
|
2023-04-07 15:39:53 +00:00
|
|
|
|
2023-03-03 17:02:11 +00:00
|
|
|
dispatch('close')
|
|
|
|
}
|
|
|
|
|
|
|
|
function select (field: string, targetValue: boolean) {
|
|
|
|
if (!targetValue) {
|
2023-04-07 15:39:53 +00:00
|
|
|
;(update as any)[field] = (sourceEmp as any)[field]
|
2023-03-03 17:02:11 +00:00
|
|
|
} else {
|
|
|
|
delete (update as any)[field]
|
|
|
|
}
|
2023-04-07 15:39:53 +00:00
|
|
|
update = update
|
2023-03-03 17:02:11 +00:00
|
|
|
applyUpdate(update)
|
|
|
|
}
|
|
|
|
|
2023-04-07 15:39:53 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-03-03 17:02:11 +00:00
|
|
|
}
|
|
|
|
}
|
2023-04-07 15:39:53 +00:00
|
|
|
return res
|
2023-03-03 17:02:11 +00:00
|
|
|
}
|
|
|
|
|
2023-04-07 15:39:53 +00:00
|
|
|
let enabledChannels: Map<Ref<Channel>, boolean> = new Map()
|
|
|
|
|
|
|
|
let resultChannels: Channel[] = []
|
2023-03-03 17:02:11 +00:00
|
|
|
let oldChannels: Channel[] = []
|
|
|
|
const valueChannelsQuery = createQuery()
|
2023-04-07 15:39:53 +00:00
|
|
|
|
|
|
|
$: valueChannelsQuery.query(contact.class.Channel, { attachedTo: sourceEmployee }, (res) => {
|
2023-03-03 17:02:11 +00:00
|
|
|
oldChannels = res
|
|
|
|
})
|
|
|
|
|
|
|
|
let targetChannels: Channel[] = []
|
|
|
|
const targetChannelsQuery = createQuery()
|
|
|
|
$: targetEmployee &&
|
|
|
|
targetChannelsQuery.query(contact.class.Channel, { attachedTo: targetEmployee }, (res) => {
|
|
|
|
targetChannels = res
|
|
|
|
})
|
|
|
|
|
2023-04-07 15:39:53 +00:00
|
|
|
$: resultChannels = mergeChannels(oldChannels, targetChannels, enabledChannels)
|
2023-03-03 17:02:11 +00:00
|
|
|
|
|
|
|
const attributes = hierarchy.getAllAttributes(contact.class.Employee, core.class.Doc)
|
|
|
|
const ignoreKeys = ['name', 'avatar', 'createOn']
|
|
|
|
const objectAttributes = Array.from(attributes.entries()).filter(
|
|
|
|
(p) => !p[1].hidden && !ignoreKeys.includes(p[0]) && !isCollectionAttr(hierarchy, { key: p[0], attr: p[1] })
|
|
|
|
)
|
|
|
|
|
|
|
|
function getMixinAttributes (mixin: Ref<Mixin<Doc>>): string[] {
|
|
|
|
const attr = hierarchy.getOwnAttributes(mixin)
|
|
|
|
const res = Array.from(attr.entries()).filter((p) => !isCollectionAttr(hierarchy, { key: p[0], attr: p[1] }))
|
|
|
|
return res.map((p) => p[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
function selectMixin (mixin: Ref<Mixin<Doc>>, field: string, targetValue: boolean) {
|
|
|
|
const upd = mixinUpdate[mixin] ?? {}
|
|
|
|
if (!targetValue) {
|
|
|
|
;(upd as any)[field] = (value as any)[field]
|
|
|
|
} else {
|
|
|
|
delete (upd as any)[field]
|
|
|
|
}
|
|
|
|
mixinUpdate[mixin] = upd
|
|
|
|
}
|
2023-04-07 15:39:53 +00:00
|
|
|
const toAny = (a: any) => a
|
2023-03-03 17:02:11 +00:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<Card
|
|
|
|
label={contact.string.MergeEmployee}
|
|
|
|
okLabel={contact.string.MergeEmployee}
|
|
|
|
fullSize
|
|
|
|
okAction={merge}
|
|
|
|
canSave={targetEmp !== undefined}
|
|
|
|
onCancel={() => dispatch('close')}
|
2023-04-07 16:59:18 +00:00
|
|
|
on:changeContent
|
2023-03-03 17:02:11 +00:00
|
|
|
>
|
2023-04-07 15:39:53 +00:00
|
|
|
<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={oldChannels}
|
|
|
|
editable={false}
|
|
|
|
kind={'link-bordered'}
|
|
|
|
size={'small'}
|
|
|
|
length={'full'}
|
|
|
|
shape={'circle'}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
>>
|
|
|
|
<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>
|
2023-03-03 17:02:11 +00:00
|
|
|
</div>
|
2023-04-07 15:39:53 +00:00
|
|
|
{#key [targetEmployee, sourceEmployee]}
|
|
|
|
{#if targetEmp && sourceEmp}
|
|
|
|
<div class="flex-col flex-grow">
|
|
|
|
<MergeComparer
|
|
|
|
key="avatar"
|
|
|
|
value={sourceEmp}
|
2023-03-03 17:02:11 +00:00
|
|
|
{targetEmp}
|
|
|
|
onChange={select}
|
2023-04-07 15:39:53 +00:00
|
|
|
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}
|
2023-03-03 17:02:11 +00:00
|
|
|
<MergeAttributeComparer
|
2023-04-07 15:39:53 +00:00
|
|
|
key={attribute[0]}
|
|
|
|
value={sourceEmp}
|
2023-03-03 17:02:11 +00:00
|
|
|
{targetEmp}
|
2023-04-07 15:39:53 +00:00
|
|
|
onChange={select}
|
|
|
|
_class={contact.class.Employee}
|
|
|
|
selected={toAny(update)[attribute[0]] !== undefined}
|
2023-03-03 17:02:11 +00:00
|
|
|
/>
|
|
|
|
{/each}
|
2023-04-07 15:39:53 +00:00
|
|
|
{#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}
|
2023-03-03 17:02:11 +00:00
|
|
|
/>
|
2023-04-07 15:39:53 +00:00
|
|
|
{/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>
|
2023-03-03 17:02:11 +00:00
|
|
|
</div>
|
2023-04-07 15:39:53 +00:00
|
|
|
{/each}
|
|
|
|
</div>
|
|
|
|
<div class="flex-col-center antiPopup p-4">
|
|
|
|
<EditEmployee object={result} readonly channels={resultChannels} />
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
{/key}
|
2023-03-03 17:02:11 +00:00
|
|
|
</Card>
|