fix: allow inactive members in departments (#10983)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2026-07-17 16:43:14 +07:00
committed by GitHub
parent 293bc91888
commit 21dfcf0774
9 changed files with 110 additions and 53 deletions
+17 -2
View File
@@ -404,14 +404,29 @@ export function createModel (builder: Builder): void {
attachTo: hr.mixin.Staff,
descriptor: view.viewlet.Table,
config: [
'',
{ key: '', props: { showStatus: true } },
'department',
{
key: '$lookup.channels',
label: contact.string.ContactInfo,
sortingKey: ['$lookup.channels.lastMessage', 'channels']
},
'modifiedOn'
]
],
viewOptions: {
groupBy: [],
orderBy: [],
other: [
{
key: 'hideInactive',
type: 'toggle',
defaultValue: true,
actionTarget: 'query',
action: contact.function.HideInactive,
label: contact.string.HideInactive
}
]
}
},
hr.viewlet.TableMember
)
+5 -1
View File
@@ -136,7 +136,7 @@ async function rebuildDepartmentMembersFromStaff (client: MigrationClient): Prom
const persons = await client.find<Person>(DOMAIN_CONTACT, { _class: contact.class.Person })
for (const person of persons) {
const staff = client.hierarchy.asIf<Person, Staff>(person, hr.mixin.Staff)
if (staff?.department === undefined || !staff.active) {
if (staff?.department === undefined) {
continue
}
@@ -172,6 +172,10 @@ export const hrOperation: MigrateOperation = {
{
state: 'rebuildDepartmentMembersFromStaff',
func: rebuildDepartmentMembersFromStaff
},
{
state: 'rebuildDepartmentMembersFromStaffIncludingInactive',
func: rebuildDepartmentMembersFromStaff
}
])
},
@@ -78,7 +78,8 @@
$: dragPersonId = dragPerson?._id
$: members = new Set<Ref<Staff>>(value.members as Ref<Staff>[])
$: values = allEmployees.filter((it) => members.has(it._id) && it._id !== dragPersonId)
$: activeMembers = allEmployees.filter((it) => it.active && members.has(it._id))
$: values = activeMembers.filter((it) => it._id !== dragPersonId)
$: dragging = value._id === dragOver?._id && dragPersonId !== undefined
@@ -121,7 +122,7 @@
<div class="fs-title">
{value.name}
</div>
<Label label={hr.string.MemberCount} params={{ count: value.members.length }} />
<Label label={hr.string.MemberCount} params={{ count: activeMembers.length }} />
</div>
<PersonsPresenter value={values} bind:dragPerson showDragPerson={dragging} />
</div>
@@ -19,7 +19,7 @@
import { Department } from '@hcengineering/hr'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Button, IconAdd, Label, Scroller, Section, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { Viewlet, ViewletPreference } from '@hcengineering/view'
import { Viewlet, ViewletPreference, type ViewOptions } from '@hcengineering/view'
import { Table, ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources'
import hr from '../plugin'
import { addMember } from '../utils'
@@ -63,6 +63,7 @@
let preference: ViewletPreference | undefined
let loading = false
let viewlet: WithLookup<Viewlet> | undefined
let viewOptions: ViewOptions | undefined
</script>
<Section label={hr.string.Members}>
@@ -75,7 +76,7 @@
bind:loading
viewletQuery={{ _id: hr.viewlet.TableMember }}
/>
<ViewletSettingButton kind={'ghost'} bind:viewlet />
<ViewletSettingButton kind={'ghost'} bind:viewlet bind:viewOptions />
<Button id={hr.string.AddEmployee} icon={IconAdd} kind={'ghost'} on:click={add} />
</div>
</svelte:fragment>
@@ -87,6 +88,8 @@
_class={hr.mixin.Staff}
config={preference?.config ?? viewlet?.config ?? []}
options={viewlet?.options}
viewOptionsConfig={viewlet?.viewOptions?.other}
{viewOptions}
query={{ _id: { $in: members } }}
loadingProps={{ length: members.length }}
/>
@@ -19,7 +19,7 @@
import { type Department } from '@hcengineering/hr'
import { getClient } from '@hcengineering/presentation'
import { Button, IconAdd, Label, Section, showPopup, Scroller } from '@hcengineering/ui'
import { Viewlet, ViewletPreference } from '@hcengineering/view'
import { Viewlet, ViewletPreference, type ViewOptions } from '@hcengineering/view'
import { Table, ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources'
import hr from '../plugin'
@@ -60,6 +60,7 @@
let viewlet: Viewlet | undefined
let preference: ViewletPreference | undefined
let viewOptions: ViewOptions | undefined
</script>
<Section id="members" label={hr.string.Members} icon={hr.icon.Members}>
@@ -72,7 +73,7 @@
bind:loading
viewletQuery={{ _id: hr.viewlet.TableMember }}
/>
<ViewletSettingButton kind={'tertiary'} bind:viewlet />
<ViewletSettingButton kind={'tertiary'} bind:viewlet bind:viewOptions />
{#if !readonly}
<Button id={hr.string.AddMember} icon={IconAdd} kind={'ghost'} on:click={createApp} />
{/if}
@@ -83,9 +84,11 @@
{#if members.length > 0 && viewlet}
<Scroller horizontal noFade={false}>
<Table
_class={contact.mixin.Employee}
_class={hr.mixin.Staff}
config={preference?.config ?? viewlet.config}
options={viewlet.options}
viewOptionsConfig={viewlet.viewOptions?.other}
{viewOptions}
query={{ _id: { $in: members } }}
loadingProps={{ length: members.length }}
{readonly}
@@ -128,6 +128,7 @@
}
let resultOptions = options
let resultQuery: DocumentQuery<Doc> = query
const update = reduceCalls(async function (
_class: Ref<Class<Doc>>,
@@ -137,10 +138,12 @@
lookup: Lookup<Doc>,
associations: AssociationQuery[] | undefined,
limit: number,
options: FindOptions<Doc> | undefined
options: FindOptions<Doc> | undefined,
viewOptionsConfig: ViewOptionModel[] | undefined,
viewOptions: ViewOptions | undefined
) {
const p = await getResultQuery(hierarchy, query, viewOptionsConfig, viewOptions)
const resultQuery = mergeQueries(p, query)
resultQuery = mergeQueries(p, query)
loading += q.query(
_class,
resultQuery,
@@ -159,7 +162,18 @@
? 1
: 0
})
$: void update(_class, query, _sortKey, sortOrder, lookup, associations, limit, resultOptions)
$: void update(
_class,
query,
_sortKey,
sortOrder,
lookup,
associations,
limit,
resultOptions,
viewOptionsConfig,
viewOptions
)
$: void getResultOptions(options, viewOptionsConfig, viewOptions).then((p) => {
resultOptions = p
@@ -170,7 +184,7 @@
const qSlow = createQuery()
$: qSlow.query(
_class,
query,
resultQuery,
(result) => {
total = result.total
if (totalQuery === undefined) {
@@ -139,6 +139,7 @@
}
let resultOptions = options
let resultQuery: DocumentQuery<Doc> = query
const update = reduceCalls(async function (
_class: Ref<Class<Doc>>,
@@ -148,10 +149,12 @@
lookup: Lookup<Doc>,
associations: AssociationQuery[] | undefined,
limit: number,
options: FindOptions<Doc> | undefined
options: FindOptions<Doc> | undefined,
viewOptionsConfig: ViewOptionModel[] | undefined,
viewOptions: ViewOptions | undefined
) {
const p = await getResultQuery(hierarchy, query, viewOptionsConfig, viewOptions)
const resultQuery = mergeQueries(p, query)
resultQuery = mergeQueries(p, query)
loading += q.query(
_class,
resultQuery,
@@ -170,7 +173,18 @@
? 1
: 0
})
$: void update(_class, query, _sortKey, sortOrder, lookup, associations, limit, resultOptions)
$: void update(
_class,
query,
_sortKey,
sortOrder,
lookup,
associations,
limit,
resultOptions,
viewOptionsConfig,
viewOptions
)
$: void getResultOptions(options, viewOptionsConfig, viewOptions).then((p) => {
resultOptions = p
@@ -181,7 +195,7 @@
const qSlow = createQuery()
$: qSlow.query(
_class,
query,
resultQuery,
(result) => {
total = result.total
if (totalQuery === undefined) {
@@ -65,8 +65,9 @@
}
const changeToggle = (model: ViewOptionModel) => {
viewOptions[model.key] = !viewOptions[model.key]
dispatch('update', { key: model.key, value: viewOptions[model.key] })
const value = !(viewOptions[model.key] ?? model.defaultValue)
viewOptions[model.key] = value
dispatch('update', { key: model.key, value })
}
// checking if selector provides multiple choice options
@@ -143,12 +144,16 @@
>
<span class="overflow-label"><Label label={model.label} /></span>
{#if isToggleType(model)}
<Toggle
on={viewOptions[model.key] ?? model.defaultValue}
on:change={() => {
changeToggle(model)
}}
/>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span on:click|stopPropagation>
<Toggle
on={viewOptions[model.key] ?? model.defaultValue}
on:change={() => {
changeToggle(model)
}}
/>
</span>
{:else if isDropdownType(model)}
{@const items = model.values.filter(({ hidden }) => !hidden?.(viewOptions))}
<DropdownLabelsIntl
+25 -27
View File
@@ -196,9 +196,8 @@ export async function OnDepartmentStaff (txes: Tx[], control: TriggerControl): P
const removedDepartments = await buildHierarchy(lastDepartment, control)
const removed = removedDepartments.map((p) => p._id)
const pushIds = push.map((p) => p._id)
const added = push.filter((p) => !removed.includes(p._id))
const removedIds = exclude(pushIds, removed)
result.push(...getTxes(control.txFactory, [employee], added, removedIds))
result.push(...getTxes(control.txFactory, [employee], push, removedIds))
}
}
}
@@ -228,12 +227,10 @@ export async function OnDepartmentUpdate (txes: Tx[], control: TriggerControl):
const oldHierarchy = oldParent !== undefined ? await buildHierarchy(oldParent, control) : []
const newHierarchy = newParent !== undefined ? await buildHierarchy(newParent, control) : []
const oldHierarchyIds = oldHierarchy.map((p) => p._id)
const newHierarchyIds = newHierarchy.map((p) => p._id)
const added = newHierarchy.filter((p) => !oldHierarchyIds.includes(p._id))
const removed = oldHierarchy.filter((p) => !newHierarchyIds.includes(p._id)).map((p) => p._id)
result.push(...getTxes(control.txFactory, members, added, removed))
result.push(...getTxes(control.txFactory, members, newHierarchy, removed))
}
return result
}
@@ -309,29 +306,30 @@ export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise<
* @public
*/
export async function OnEmployeeDeactivate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
for (const tx of txes) {
if (core.class.TxMixin !== tx._class) {
continue
}
const ctx = tx as TxMixin<Person, Employee>
if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) {
continue
}
const employee = ctx.objectId as Ref<Employee>
const departments = await control.queryFind(control.ctx, hr.class.Department, {})
const removed = departments.filter((dep) => dep.members.some((p) => p === employee))
result.push(
...getTxes(
control.txFactory,
[employee],
[],
removed.map((p) => p._id)
)
)
}
return []
// const result: Tx[] = []
// for (const tx of txes) {
// if (core.class.TxMixin !== tx._class) {
// continue
// }
// const ctx = tx as TxMixin<Person, Employee>
// if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) {
// continue
// }
// const employee = ctx.objectId as Ref<Employee>
// const departments = await control.queryFind(control.ctx, hr.class.Department, {})
// const removed = departments.filter((dep) => dep.members.some((p) => p === employee))
// result.push(
// ...getTxes(
// control.txFactory,
// [employee],
// [],
// removed.map((p) => p._id)
// )
// )
// }
return result
// return result
}
// TODO: why we need specific email notifications instead of using general flow?