mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-17 13:54:11 +00:00
Merge employee (#2709)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
99bef1d311
commit
ec2235e26b
@ -156,6 +156,8 @@ export class TEmployee extends TPerson implements Employee {
|
||||
|
||||
@Prop(Collection(contact.class.Status), contact.string.Status)
|
||||
statuses?: number
|
||||
|
||||
mergedTo?: Ref<Employee>
|
||||
}
|
||||
|
||||
@Model(contact.class.EmployeeAccount, core.class.Account)
|
||||
@ -563,6 +565,30 @@ export function createModel (builder: Builder): void {
|
||||
contact.action.KickEmployee
|
||||
)
|
||||
|
||||
createAction(
|
||||
builder,
|
||||
{
|
||||
action: view.actionImpl.ShowPopup,
|
||||
actionProps: {
|
||||
component: contact.component.MergeEmployee,
|
||||
element: 'top',
|
||||
fillProps: {
|
||||
_object: 'value'
|
||||
}
|
||||
},
|
||||
label: contact.string.MergeEmployee,
|
||||
category: contact.category.Contact,
|
||||
target: contact.class.Employee,
|
||||
input: 'focus',
|
||||
context: {
|
||||
mode: ['context'],
|
||||
group: 'other'
|
||||
},
|
||||
secured: true
|
||||
},
|
||||
contact.action.MergeEmployee
|
||||
)
|
||||
|
||||
// Allow to use fuzzy search for mixins
|
||||
builder.mixin(contact.class.Contact, core.class.Class, core.mixin.FullTextSearchContext, {
|
||||
fullTextSummary: true
|
||||
|
@ -46,7 +46,8 @@ export default mergeIds(contactId, contact, {
|
||||
EmployeeEditor: '' as AnyComponent,
|
||||
CreateEmployee: '' as AnyComponent,
|
||||
AccountArrayEditor: '' as AnyComponent,
|
||||
ChannelFilter: '' as AnyComponent
|
||||
ChannelFilter: '' as AnyComponent,
|
||||
MergeEmployee: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Persons: '' as IntlString,
|
||||
@ -55,7 +56,6 @@ export default mergeIds(contactId, contact, {
|
||||
SearchOrganization: '' as IntlString,
|
||||
ContactInfo: '' as IntlString,
|
||||
Contact: '' as IntlString,
|
||||
Location: '' as IntlString,
|
||||
Channel: '' as IntlString,
|
||||
ChannelProvider: '' as IntlString,
|
||||
Value: '' as IntlString,
|
||||
@ -91,7 +91,8 @@ export default mergeIds(contactId, contact, {
|
||||
Contact: '' as Ref<ActionCategory>
|
||||
},
|
||||
action: {
|
||||
KickEmployee: '' as Ref<Action>
|
||||
KickEmployee: '' as Ref<Action>,
|
||||
MergeEmployee: '' as Ref<Action>
|
||||
},
|
||||
actionImpl: {
|
||||
KickEmployee: '' as ViewAction,
|
||||
|
@ -42,4 +42,8 @@ export function createModel (builder: Builder): void {
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverContact.trigger.OnContactDelete
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverContact.trigger.OnEmployeeUpdate
|
||||
})
|
||||
}
|
||||
|
@ -576,6 +576,7 @@ input.search {
|
||||
.max-w-2 { max-width: .5rem; }
|
||||
.max-w-9 { max-width: 2.25rem; }
|
||||
.max-w-30 { max-width: 7.5rem; }
|
||||
.max-w-40 { max-width: 10rem; }
|
||||
.max-w-60 { max-width: 15rem; }
|
||||
.max-w-80 { max-width: 20rem; }
|
||||
.max-w-240 { max-width: 60rem; }
|
||||
|
@ -14,11 +14,11 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Asset } from '@hcengineering/platform'
|
||||
import type { AnySvelteComponent } from '../types'
|
||||
import type { AnySvelteComponent, ButtonSize } from '../types'
|
||||
import Icon from './Icon.svelte'
|
||||
|
||||
export let icon: Asset | AnySvelteComponent | undefined
|
||||
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'large'
|
||||
export let size: ButtonSize = 'large'
|
||||
export let transparent: boolean = false
|
||||
export let selected: boolean = false
|
||||
export let primary: boolean = false
|
||||
@ -73,6 +73,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon-inline {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
.icon-small {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
|
@ -13,12 +13,24 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let on: boolean = false
|
||||
export let disabled: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<label class="toggle">
|
||||
<input class="chBox" type="checkbox" {disabled} bind:checked={on} on:change />
|
||||
<input
|
||||
class="chBox"
|
||||
type="checkbox"
|
||||
{disabled}
|
||||
bind:checked={on}
|
||||
on:change={(e) => {
|
||||
dispatch('change', on)
|
||||
}}
|
||||
/>
|
||||
<span class="toggle-switch" />
|
||||
</label>
|
||||
|
||||
|
@ -75,6 +75,7 @@
|
||||
"WhatsappPlaceholder": "Whatsapp",
|
||||
"Profile": "Profile",
|
||||
"ProfilePlaceholder": "Profile...",
|
||||
"CurrentEmployee": "Current employee"
|
||||
"CurrentEmployee": "Current employee",
|
||||
"MergeEmployee": "Merge employee"
|
||||
}
|
||||
}
|
@ -75,6 +75,7 @@
|
||||
"WhatsappPlaceholder": "Whatsapp",
|
||||
"Profile": "Профиль",
|
||||
"ProfilePlaceholder": "Профиль...",
|
||||
"CurrentEmployee": "Текущий сотрудник"
|
||||
"CurrentEmployee": "Текущий сотрудник",
|
||||
"MergeEmployee": "Объеденить сотрудника"
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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">
|
||||
import contact, { Channel, ChannelProvider } from '@hcengineering/contact'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { ButtonSize, CircleButton, tooltip } from '@hcengineering/ui'
|
||||
|
||||
export let value: Channel
|
||||
export let size: ButtonSize = 'small'
|
||||
let provider: ChannelProvider | undefined
|
||||
const providerQuery = createQuery()
|
||||
providerQuery.query(contact.class.ChannelProvider, { _id: value.provider }, (res) => ([provider] = res))
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center" use:tooltip={{ label: getEmbeddedLabel(value.value) }}>
|
||||
{#if provider}
|
||||
<CircleButton icon={provider.icon} {size} />
|
||||
<div class="overflow-label ml-2">{value.value}</div>
|
||||
{/if}
|
||||
</div>
|
@ -0,0 +1,50 @@
|
||||
<!--
|
||||
// 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">
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { getAttribute, getAttributeEditor, getClient } from '@hcengineering/presentation'
|
||||
import MergeComparer from './MergeComparer.svelte'
|
||||
|
||||
export let value: Employee
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let targetEmp: Employee
|
||||
export let key: string
|
||||
export let onChange: (key: string, value: boolean) => void
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const editor = getAttributeEditor(client, _class, key)
|
||||
const attribute = hierarchy.getAttribute(_class, key)
|
||||
</script>
|
||||
|
||||
{#await editor then instance}
|
||||
{#if instance}
|
||||
<MergeComparer {value} {targetEmp} {key} {onChange} cast={hierarchy.isMixin(_class) ? _class : undefined}>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
<svelte:component
|
||||
this={instance}
|
||||
type={attribute?.type}
|
||||
value={getAttribute(client, item, { key, attr: attribute })}
|
||||
readonly
|
||||
disabled
|
||||
space={item.space}
|
||||
{focus}
|
||||
object={item}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</MergeComparer>
|
||||
{/if}
|
||||
{/await}
|
@ -0,0 +1,56 @@
|
||||
<!--
|
||||
// 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">
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { Doc, Mixin, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Toggle } 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 onChange: (key: string, value: boolean) => void
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
function isEqual (value: Employee, targetEmp: Employee, key: string) {
|
||||
if (cast !== undefined) {
|
||||
value = hierarchy.as(value, cast)
|
||||
targetEmp = hierarchy.as(targetEmp, cast)
|
||||
}
|
||||
if (!(value as any)[key]) return true
|
||||
if (!(targetEmp as any)[key]) return true
|
||||
return (value as any)[key] === (targetEmp as any)[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>
|
||||
{/if}
|
279
plugins/contact-resources/src/components/MergeEmployee.svelte
Normal file
279
plugins/contact-resources/src/components/MergeEmployee.svelte
Normal file
@ -0,0 +1,279 @@
|
||||
<!--
|
||||
// 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">
|
||||
import { Channel, ChannelProvider, Employee, formatName } from '@hcengineering/contact'
|
||||
import core, { Doc, DocumentUpdate, Mixin, Ref, TxProcessor } from '@hcengineering/core'
|
||||
import { leaveWorkspace } from '@hcengineering/login-resources'
|
||||
import { Avatar, Card, createQuery, EmployeeBox, getClient } from '@hcengineering/presentation'
|
||||
import { DatePresenter, Grid, Toggle } from '@hcengineering/ui'
|
||||
import { isCollectionAttr, StringEditor } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import contact from '../plugin'
|
||||
import ChannelPresenter from './ChannelPresenter.svelte'
|
||||
import ChannelsDropdown from './ChannelsDropdown.svelte'
|
||||
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))
|
||||
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)
|
||||
})
|
||||
|
||||
function fillUpdate (value: Employee, target: Employee): DocumentUpdate<Employee> {
|
||||
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) {
|
||||
;(res as any)[key] = (value as any)[key]
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
function fillMixinUpdate (value: 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
|
||||
const attributes = hierarchy.getOwnAttributes(mixin)
|
||||
for (const attribute of attributes) {
|
||||
const key = attribute[0]
|
||||
const from = hierarchy.as(value, mixin)
|
||||
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>> = {}
|
||||
let result: Employee = value
|
||||
|
||||
function applyUpdate (update: DocumentUpdate<Employee>): void {
|
||||
result = hierarchy.clone(targetEmp)
|
||||
TxProcessor.applyUpdate(result, update)
|
||||
}
|
||||
|
||||
async function merge (): Promise<void> {
|
||||
if (targetEmp === undefined) return
|
||||
if (Object.keys(update).length > 0) {
|
||||
await client.update(targetEmp, update)
|
||||
}
|
||||
await client.update(value, { mergedTo: targetEmp._id, active: false })
|
||||
for (const channel of resultChannels.values()) {
|
||||
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 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>>, {})
|
||||
}
|
||||
}
|
||||
const account = await client.findOne(contact.class.EmployeeAccount, { employee: value._id })
|
||||
if (account !== undefined) {
|
||||
leaveWorkspace(account.email)
|
||||
}
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
function select (field: string, targetValue: boolean) {
|
||||
if (!targetValue) {
|
||||
;(update as any)[field] = (value as any)[field]
|
||||
} else {
|
||||
delete (update as any)[field]
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
targetConflict = targetConflict
|
||||
valueConflict = valueConflict
|
||||
return new Map(res.map((p) => [p.provider, p]))
|
||||
}
|
||||
|
||||
let resultChannels: Map<Ref<ChannelProvider>, Channel> = new Map()
|
||||
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) => {
|
||||
oldChannels = res
|
||||
resultChannels = mergeChannels(oldChannels, targetChannels)
|
||||
})
|
||||
|
||||
let targetChannels: Channel[] = []
|
||||
const targetChannelsQuery = createQuery()
|
||||
$: 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card
|
||||
label={contact.string.MergeEmployee}
|
||||
okLabel={contact.string.MergeEmployee}
|
||||
fullSize
|
||||
okAction={merge}
|
||||
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>
|
||||
{formatName(item.name)}
|
||||
</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} />
|
||||
{formatName(result.name)}
|
||||
<DatePresenter value={result.birthday} />
|
||||
<StringEditor value={result.city} readonly placeholder={contact.string.Location} />
|
||||
<ChannelsDropdown
|
||||
value={Array.from(resultChannels.values())}
|
||||
editable={false}
|
||||
kind={'link-bordered'}
|
||||
size={'small'}
|
||||
length={'full'}
|
||||
shape={'circle'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
@ -54,6 +54,7 @@ import PersonRefPresenter from './components/PersonRefPresenter.svelte'
|
||||
import EmployeeRefPresenter from './components/EmployeeRefPresenter.svelte'
|
||||
import ChannelFilter from './components/ChannelFilter.svelte'
|
||||
import AccountBox from './components/AccountBox.svelte'
|
||||
import MergeEmployee from './components/MergeEmployee.svelte'
|
||||
import contact from './plugin'
|
||||
import {
|
||||
employeeSort,
|
||||
@ -115,21 +116,24 @@ async function queryContact (
|
||||
async function kickEmployee (doc: Employee): Promise<void> {
|
||||
const client = getClient()
|
||||
const email = await client.findOne(contact.class.EmployeeAccount, { employee: doc._id })
|
||||
if (email === undefined) return
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: contact.string.KickEmployee,
|
||||
message: contact.string.KickEmployeeDescr
|
||||
},
|
||||
undefined,
|
||||
(res?: boolean) => {
|
||||
if (res === true) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
leaveWorkspace(email.email)
|
||||
if (email === undefined) {
|
||||
await client.update(doc, { active: false })
|
||||
} else {
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: contact.string.KickEmployee,
|
||||
message: contact.string.KickEmployeeDescr
|
||||
},
|
||||
undefined,
|
||||
(res?: boolean) => {
|
||||
if (res === true) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
leaveWorkspace(email.email)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
async function openChannelURL (doc: Channel): Promise<void> {
|
||||
if (doc.value.startsWith('http://') || doc.value.startsWith('https://')) {
|
||||
@ -176,7 +180,8 @@ export default async (): Promise<Resources> => ({
|
||||
EmployeeEditor,
|
||||
CreateEmployee,
|
||||
AccountArrayEditor,
|
||||
ChannelFilter
|
||||
ChannelFilter,
|
||||
MergeEmployee
|
||||
},
|
||||
completion: {
|
||||
EmployeeQuery: async (
|
||||
|
@ -42,6 +42,7 @@ export default mergeIds(contactId, contact, {
|
||||
SetStatus: '' as IntlString,
|
||||
ClearStatus: '' as IntlString,
|
||||
SaveStatus: '' as IntlString,
|
||||
Location: '' as IntlString,
|
||||
Cancel: '' as IntlString,
|
||||
StatusDueDate: '' as IntlString,
|
||||
StatusName: '' as IntlString,
|
||||
@ -59,7 +60,8 @@ export default mergeIds(contactId, contact, {
|
||||
Email: '' as IntlString,
|
||||
CreateEmployee: '' as IntlString,
|
||||
Inactive: '' as IntlString,
|
||||
NotSpecified: '' as IntlString
|
||||
NotSpecified: '' as IntlString,
|
||||
MergeEmployee: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
EmployeeSort: '' as SortFunc,
|
||||
|
@ -142,6 +142,7 @@ export interface Status extends AttachedDoc {
|
||||
*/
|
||||
export interface Employee extends Person {
|
||||
active: boolean
|
||||
mergedTo?: Ref<Employee>
|
||||
statuses?: number
|
||||
}
|
||||
|
||||
|
@ -22,8 +22,8 @@
|
||||
// export let label: IntlString
|
||||
export let placeholder: IntlString
|
||||
export let value: string
|
||||
export let focus: boolean
|
||||
export let onChange: (value: string) => void
|
||||
export let focus: boolean = false
|
||||
export let onChange: (value: string) => void = () => {}
|
||||
export let kind: 'no-border' | 'link' = 'no-border'
|
||||
export let readonly = false
|
||||
export let size: ButtonSize = 'small'
|
||||
|
@ -142,7 +142,8 @@ export {
|
||||
SortableListItem,
|
||||
MarkupEditor,
|
||||
TreeNode,
|
||||
TreeItem
|
||||
TreeItem,
|
||||
StringEditor
|
||||
}
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
|
@ -1,6 +1,6 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022 Hardcore Engineering Inc.
|
||||
// Copyright © 2021, 2022, 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
@ -14,8 +14,22 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import contact, { Contact, contactId, formatName, Organization, Person } from '@hcengineering/contact'
|
||||
import core, { concatLink, Doc, Tx, TxRemoveDoc } from '@hcengineering/core'
|
||||
import contact, { Contact, contactId, Employee, formatName, Organization, Person } from '@hcengineering/contact'
|
||||
import core, {
|
||||
AnyAttribute,
|
||||
ArrOf,
|
||||
AttachedDoc,
|
||||
Class,
|
||||
Collection,
|
||||
concatLink,
|
||||
Doc,
|
||||
Obj,
|
||||
Ref,
|
||||
RefTo,
|
||||
Tx,
|
||||
TxRemoveDoc,
|
||||
TxUpdateDoc
|
||||
} from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
@ -84,6 +98,179 @@ export async function OnContactDelete (
|
||||
return result
|
||||
}
|
||||
|
||||
async function mergeCollectionAttributes<T extends Doc> (
|
||||
control: TriggerControl,
|
||||
attributes: Map<string, AnyAttribute>,
|
||||
oldValue: Ref<T>,
|
||||
newValue: Ref<T>
|
||||
): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
for (const attribute of attributes) {
|
||||
if (control.hierarchy.isDerived(attribute[1].type._class, core.class.Collection)) {
|
||||
if (attribute[1]._id === contact.class.Contact + '_channels') continue
|
||||
const collection = attribute[1].type as Collection<AttachedDoc>
|
||||
const allAttached = await control.findAll(collection.of, { attachedTo: oldValue })
|
||||
for (const attached of allAttached) {
|
||||
const tx = control.txFactory.createTxUpdateDoc(attached._class, attached.space, attached._id, {
|
||||
attachedTo: newValue
|
||||
})
|
||||
const parent = control.txFactory.createTxCollectionCUD(
|
||||
attached.attachedToClass,
|
||||
newValue,
|
||||
attached.space,
|
||||
attached.collection,
|
||||
tx
|
||||
)
|
||||
res.push(parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async function processRefAttribute<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
|
||||
}
|
||||
|
||||
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.ArrOf) {
|
||||
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
|
||||
)
|
||||
res.push(...accountTxes)
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnEmployeeUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
if (tx._class !== core.class.TxUpdateDoc) {
|
||||
return []
|
||||
}
|
||||
|
||||
const uTx = tx as TxUpdateDoc<Employee>
|
||||
|
||||
if (!control.hierarchy.isDerived(uTx.objectClass, contact.class.Employee)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const result: Tx[] = []
|
||||
|
||||
const txes = await mergeEmployee(control, uTx)
|
||||
result.push(...txes)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -125,7 +312,8 @@ export function organizationTextPresenter (doc: Doc): string {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
OnContactDelete
|
||||
OnContactDelete,
|
||||
OnEmployeeUpdate
|
||||
},
|
||||
function: {
|
||||
PersonHTMLPresenter: personHTMLPresenter,
|
||||
|
@ -29,7 +29,8 @@ export const serverContactId = 'server-contact' as Plugin
|
||||
*/
|
||||
export default plugin(serverContactId, {
|
||||
trigger: {
|
||||
OnContactDelete: '' as Resource<TriggerFunc>
|
||||
OnContactDelete: '' as Resource<TriggerFunc>,
|
||||
OnEmployeeUpdate: '' as Resource<TriggerFunc>
|
||||
},
|
||||
function: {
|
||||
PersonHTMLPresenter: '' as Resource<Presenter>,
|
||||
|
Loading…
Reference in New Issue
Block a user