Files
Denis BykhovandGitHub 0e3cfbd949 Section locking (#10702)
* implement section locking functionality with role-based access control

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Fix formatting

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Add translations

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

---------

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
2026-03-28 19:35:37 +07:00

951 lines
30 KiB
TypeScript

//
// Copyright © 2025 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.
//
import activity from '@hcengineering/activity'
import card, { Card, cardId, MasterTag, Tag } from '@hcengineering/card'
import communication, { Direct } from '@hcengineering/communication'
import {
AddCollaboratorsEvent,
CardEventType,
CreatePeerEvent,
MessageEventType,
NotificationEventType,
PeerEventType,
RemoveCardEvent,
ThreadPatchEvent,
UpdateCardTypeEvent
} from '@hcengineering/communication-sdk-types'
import { CardPeer } from '@hcengineering/communication-types'
import contact, { Employee, formatName, Person } from '@hcengineering/contact'
import core, {
AccountUuid,
AnyAttribute,
ArrOf,
Class,
concatLink,
Data,
Doc,
DocumentUpdate,
fillDefaults,
generateId,
getDiffUpdate,
Mixin,
MixinUpdate,
notEmpty,
OperationDomain,
PersonId,
Ref,
RefTo,
Space,
splitMixinUpdate,
Tx,
TxCreateDoc,
TxMixin,
TxProcessor,
TxRemoveDoc,
TxUpdateDoc
} from '@hcengineering/core'
import { getMetadata, translate } from '@hcengineering/platform'
import { getEmployee, getPersonSpaces } from '@hcengineering/server-contact'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import setting from '@hcengineering/setting'
import view from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
async function OnAttribute (ctx: TxCreateDoc<AnyAttribute>[], control: TriggerControl): Promise<Tx[]> {
const attr = TxProcessor.createDoc2Doc(ctx[0])
if (control.hierarchy.isDerived(attr.attributeOf, card.class.Card)) {
const desc = control.hierarchy.getDescendants(attr.attributeOf)
const res: Tx[] = []
for (const des of desc) {
const viewlets = control.modelDb.findAllSync(view.class.Viewlet, { attachTo: des, variant: { $exists: false } })
for (const viewlet of viewlets) {
const updatedConfig = [...viewlet.config]
// let push it after grow for the list
if (viewlet.descriptor === view.viewlet.RelationshipTable) continue
if (viewlet.descriptor === view.viewlet.List) {
const index = viewlet.config.findIndex((p) => typeof p !== 'string' && p.displayProps?.grow === true)
if (index !== -1) {
updatedConfig.splice(index + 1, 0, attr.name)
} else {
updatedConfig.push(attr.name)
}
} else {
updatedConfig.push(attr.name)
}
res.push(
control.txFactory.createTxUpdateDoc(viewlet._class, viewlet.space, viewlet._id, {
config: updatedConfig
})
)
const prefs = await control.findAll(control.ctx, view.class.ViewletPreference, { attachedTo: viewlet._id })
for (const pref of prefs) {
const updatedPrefConfig = [...pref.config]
if (viewlet.descriptor === view.viewlet.List) {
const index = updatedPrefConfig.findIndex((p) => typeof p !== 'string' && p.displayProps?.grow === true)
if (index !== -1) {
updatedPrefConfig.splice(index + 1, 0, attr.name)
} else {
updatedPrefConfig.push(attr.name)
}
} else {
updatedPrefConfig.push(attr.name)
}
res.push(
control.txFactory.createTxUpdateDoc(pref._class, pref.space, pref._id, {
config: updatedPrefConfig
})
)
}
}
}
return res
}
return []
}
async function OnAttributeRemove (ctx: TxRemoveDoc<AnyAttribute>[], control: TriggerControl): Promise<Tx[]> {
const attr = control.removedMap.get(ctx[0].objectId) as AnyAttribute
if (attr === undefined) return []
if (control.hierarchy.isDerived(attr.attributeOf, card.class.Card)) {
const desc = control.hierarchy.getDescendants(attr.attributeOf)
const forbidden = new Set(desc.map((d) => `${d}.${attr.name}`))
forbidden.add(attr.name)
const res: Tx[] = []
const viewlets = control.modelDb.findAllSync(view.class.Viewlet, {})
for (const viewlet of viewlets) {
const filteredConfig = viewlet.config.filter((p) => {
const key = typeof p === 'string' ? p : (p as any).key
return !forbidden.has(key)
})
if (filteredConfig.length !== viewlet.config.length) {
res.push(
control.txFactory.createTxUpdateDoc(viewlet._class, viewlet.space, viewlet._id, {
config: filteredConfig
})
)
}
}
const prefs = await control.findAll(control.ctx, view.class.ViewletPreference, {})
for (const pref of prefs) {
const filteredPrefConfig = pref.config.filter((p) => {
const key = typeof p === 'string' ? p : (p as any).key
return !forbidden.has(key)
})
if (filteredPrefConfig.length !== pref.config.length) {
res.push(
control.txFactory.createTxUpdateDoc(pref._class, pref.space, pref._id, {
config: filteredPrefConfig
})
)
}
}
return res
}
return []
}
async function OnMasterTagRemove (ctx: TxUpdateDoc<MasterTag>[], control: TriggerControl): Promise<Tx[]> {
const updateTx = ctx[0]
if (updateTx.space === core.space.DerivedTx) return []
if (updateTx.operations.removed !== true) return []
const res: Tx[] = []
const desc = control.hierarchy.getDescendants(updateTx.objectId)
// should remove objects if masterTag
const cards = await control.findAll<Card>(control.ctx, updateTx.objectId, {})
for (const doc of cards) {
res.push(control.txFactory.createTxRemoveDoc(card.class.Card, doc.space, doc._id))
}
for (const des of desc) {
res.push(...(await removeTagRelations(control, des)))
}
const spaces = await control.findAll(control.ctx, card.class.CardSpace, {
types: updateTx.objectId
})
for (const space of spaces) {
res.push(
control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
types: space.types.filter((t) => t !== updateTx.objectId)
})
)
}
for (const des of desc) {
if (des === updateTx.objectId) continue
const _class = control.hierarchy.findClass(des)
if (_class === undefined) continue
if (_class._class === card.class.MasterTag) {
res.push(
control.txFactory.createTxUpdateDoc<MasterTag>(card.class.MasterTag, core.space.Model, des, {
removed: true
})
)
} else {
res.push(control.txFactory.createTxRemoveDoc(card.class.Tag, core.space.Model, des))
}
}
return res
}
async function OnTagRemove (ctx: TxRemoveDoc<Tag>[], control: TriggerControl): Promise<Tx[]> {
const removeTx = ctx[0]
const removedTag = control.removedMap.get(removeTx.objectId)
if (removedTag === undefined) return []
const res: Tx[] = []
const desc = control.hierarchy.getDescendants(removeTx.objectId)
for (const des of desc) {
if (des === removeTx.objectId) continue
res.push(control.txFactory.createTxRemoveDoc(card.class.Tag, core.space.Model, des))
}
res.push(...(await removeTagRelations(control, removeTx.objectId)))
return res
}
async function removeTagRelations (control: TriggerControl, tag: Ref<Tag | MasterTag>): Promise<Tx[]> {
const res: Tx[] = []
const viewlets = await control.findAll(control.ctx, view.class.Viewlet, {
attachTo: tag
})
for (const viewlet of viewlets) {
res.push(control.txFactory.createTxRemoveDoc(viewlet._class, viewlet.space, viewlet._id))
}
const attributes = control.modelDb.findAllSync(core.class.Attribute, {
attributeOf: tag
})
for (const attribute of attributes) {
res.push(control.txFactory.createTxRemoveDoc(attribute._class, attribute.space, attribute._id))
}
const removedRelation = new Set()
const relationsA = control.modelDb.findAllSync(core.class.Association, {
classA: tag
})
for (const rel of relationsA) {
removedRelation.add(rel._id)
res.push(control.txFactory.createTxRemoveDoc(core.class.Association, core.space.Model, rel._id))
}
const relationsB = control.modelDb.findAllSync(core.class.Association, {
classB: tag
})
for (const rel of relationsB) {
if (removedRelation.has(rel._id)) continue
res.push(control.txFactory.createTxRemoveDoc(core.class.Association, core.space.Model, rel._id))
}
return res
}
function extractObjectData<T extends Doc> (doc: T): Data<T> {
const dataKeys = ['_id', 'space', 'modifiedOn', 'modifiedBy', 'createdBy', 'createdOn']
const data: any = {}
for (const key in doc) {
if (dataKeys.includes(key)) {
continue
}
data[key] = doc[key]
}
return data as Data<T>
}
async function OnMasterTagCreate (ctx: TxCreateDoc<MasterTag | Tag>[], control: TriggerControl): Promise<Tx[]> {
const createTx = ctx[0]
const tag = TxProcessor.createDoc2Doc(createTx)
const res: Tx[] = []
res.push(
control.txFactory.createTxMixin(createTx.objectId, core.class.Mixin, core.space.Model, setting.mixin.Editable, {
value: true
})
)
res.push(
control.txFactory.createTxMixin(createTx.objectId, core.class.Mixin, core.space.Model, setting.mixin.UserMixin, {})
)
if (tag._class === card.class.MasterTag) {
const viewlets = await control.findAll(control.ctx, view.class.Viewlet, {
attachTo: tag.extends,
variant: { $exists: false }
})
const existingViewlets = await control.findAll(control.ctx, view.class.Viewlet, {
attachTo: createTx.objectId,
variant: { $exists: false }
})
for (const viewlet of viewlets) {
if (existingViewlets.find((it) => it.descriptor === viewlet.descriptor) !== undefined) continue
const base = extractObjectData(viewlet)
res.push(
control.txFactory.createTxCreateDoc(view.class.Viewlet, core.space.Model, {
...base,
attachTo: createTx.objectId
})
)
}
}
return res
}
async function OnCardRemove (ctx: TxRemoveDoc<Card>[], control: TriggerControl): Promise<Tx[]> {
const removeTx = ctx[0]
const removedCard = control.removedMap.get(removeTx.objectId) as Card
if (removedCard === undefined) return []
const res: Tx[] = []
const cards = await control.findAll(control.ctx, card.class.Card, { parent: removedCard._id })
for (const card of cards) {
res.push(control.txFactory.createTxRemoveDoc(card._class, card.space, card._id))
}
const toDelete: string[] = []
for (const key in removedCard.blobs ?? {}) {
const val = removedCard.blobs[key]
if (val === undefined) continue
const toDelete: string[] = []
toDelete.push(val.file)
}
if (toDelete.length > 0) {
await control.storageAdapter.remove(control.ctx, control.workspace, toDelete)
}
if (removedCard.parent != null) {
res.push(
control.txFactory.createTxUpdateDoc(card.class.Card, core.space.Workspace, removedCard.parent, {
$inc: {
children: -1
}
})
)
}
const favorites = await control.findAll(control.ctx, card.class.FavoriteCard, { attachedTo: removedCard._id })
for (const favorite of favorites) {
res.push(control.txFactory.createTxRemoveDoc(favorite._class, favorite.space, favorite._id))
}
const event: RemoveCardEvent = {
type: CardEventType.RemoveCard,
cardId: removedCard._id,
date: new Date(removeTx.createdOn ?? removeTx.modifiedOn),
socialId: removedCard.modifiedBy
}
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
event
})
return res
}
function unwrapPush (push: Record<string, any>): Record<string, any> {
const res: Record<string, any> = {}
for (const [key, value] of Object.entries(push)) {
if (value.$each !== undefined) {
res[key] = value.$each
} else {
res[key] = value
}
}
return res
}
async function OnCardUpdate (ctx: TxUpdateDoc<Card>[], control: TriggerControl): Promise<Tx[]> {
const updateTx = ctx[0]
const doc = (await control.findAll(control.ctx, card.class.Card, { _id: updateTx.objectId }))[0]
if (doc === undefined) return []
const res: Tx[] = []
if (updateTx.operations.parent !== undefined) {
const newParent = updateTx.operations.parent
const oldParent = doc.parentInfo[doc.parentInfo.length - 1]?._id
if (newParent != null) {
const parent = (await control.findAll(control.ctx, card.class.Card, { _id: newParent }))[0]
if (parent !== undefined) {
if (parent.parentInfo.findIndex((p) => p._id === doc._id) === -1) {
res.push(
control.txFactory.createTxUpdateDoc(parent._class, parent.space, parent._id, {
$inc: {
children: 1
}
})
)
res.push(
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
parentInfo: [
...parent.parentInfo,
{
_id: parent._id,
_class: parent._class,
title: parent.title
}
]
})
)
} else {
// rollback
return [
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
parent: oldParent ?? null
})
]
}
}
}
if (oldParent != null) {
const parent = (await control.findAll(control.ctx, card.class.Card, { _id: oldParent }))[0]
if (parent !== undefined) {
res.push(
control.txFactory.createTxUpdateDoc(parent._class, parent.space, parent._id, {
$inc: {
children: -1
}
})
)
}
}
}
if (updateTx.operations.title !== undefined) {
res.push(...(await updateParentInfoName(control, doc._id, updateTx.operations.title, doc._id)))
}
if ((updateTx.operations as any)._class !== undefined) {
const event: UpdateCardTypeEvent = {
type: CardEventType.UpdateCardType,
cardId: doc._id,
cardType: (updateTx.operations as any)._class,
socialId: updateTx.createdBy ?? updateTx.modifiedBy,
date: new Date(updateTx.createdOn ?? updateTx.modifiedOn)
}
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
event
})
}
res.push(...(await updatePeers(control, doc, updateTx)))
await updateCollaborators(control, updateTx.operations, doc._class, doc, updateTx.modifiedBy)
const push = (updateTx.operations as any).$push?.readonlySections as Ref<Class<Doc>> | undefined
const pull = (updateTx.operations as any).$pull?.readonlySections as Ref<Class<Doc>> | undefined
const sectionId = push ?? pull
if (sectionId !== undefined) {
const sectionClass = control.hierarchy.getClass(sectionId)
const label = sectionClass?.label ?? sectionId
const section = await translate(label, {})
res.push(
control.txFactory.createTxCreateDoc(activity.class.ActivityInfoMessage, doc.space, {
attachedTo: doc._id,
attachedToClass: doc._class,
message: push !== undefined ? card.string.SectionLocked : card.string.SectionUnlocked,
props: { section },
collection: 'activity'
})
)
}
return res
}
function getUpdateEmployees (
control: TriggerControl,
field: string,
value: any,
_class: Ref<Class<Doc>>
): Ref<Employee>[] | undefined {
const res: Ref<Employee>[] = []
const attr = control.hierarchy.findAttribute(_class, field)
if (attr === undefined) return
const parentType = attr.type._class === core.class.ArrOf ? (attr.type as ArrOf<Doc>).of : attr.type
if (parentType._class !== core.class.RefTo) return
const to = (parentType as RefTo<Doc>).to
if (control.hierarchy.isDerived(to, contact.mixin.Employee)) {
if (value != null) {
if (Array.isArray(value)) {
for (const val of value) {
res.push(val)
}
} else {
res.push(value)
}
}
}
return res
}
async function addCollaborators (
collaboratorRefs: Set<Ref<Employee>>,
control: TriggerControl,
doc: Card,
modifiedBy: PersonId
): Promise<void> {
if (collaboratorRefs.size > 0) {
const employees = await control.findAll(control.ctx, contact.mixin.Employee, {
_id: { $in: [...collaboratorRefs] }
})
const collaborators = employees.map((p) => p.personUuid).filter((p) => p != null)
const event: AddCollaboratorsEvent = {
type: NotificationEventType.AddCollaborators,
cardId: doc._id,
cardType: doc._class,
collaborators,
socialId: modifiedBy
}
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
event
})
}
}
async function updateCollaborators (
control: TriggerControl,
ops: MixinUpdate<Card, Card> | DocumentUpdate<Card>,
_class: Ref<Class<Doc>>,
doc: Card,
modifiedBy: PersonId
): Promise<void> {
const collaboratorRefs = new Set<Ref<Employee>>()
for (const [field, value] of Object.entries(ops)) {
if (field === '$push') {
const unwrap = unwrapPush(value)
for (const [field, value] of Object.entries(unwrap)) {
const col = getUpdateEmployees(control, field, value, _class)
if (col !== undefined) {
col.map((p) => collaboratorRefs.add(p))
}
}
}
if (field.startsWith('$')) continue
const col = getUpdateEmployees(control, field, value, _class)
if (col !== undefined) {
col.map((p) => collaboratorRefs.add(p))
}
}
await addCollaborators(collaboratorRefs, control, doc, modifiedBy)
}
async function updatePeers (control: TriggerControl, doc: Card, updateTx: TxUpdateDoc<Card>): Promise<Tx[]> {
if (updateTx.space === core.space.DerivedTx) return []
const isDirect = control.hierarchy.isDerived(doc._class, communication.type.Direct)
const isThreadFromDirect = (doc.parentInfo ?? []).some((it) =>
control.hierarchy.isDerived(it._class, communication.type.Direct)
)
if (!isDirect && !isThreadFromDirect) return []
delete updateTx.operations.title
delete updateTx.operations.parentInfo
delete updateTx.operations.parent
delete updateTx.operations.$inc
const peers = (
(
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
findPeers: { params: { kind: 'card', cardId: doc._id } }
})
).value as CardPeer[]
).flatMap((it) => it.members)
const res: Tx[] = []
for (const peer of peers) {
res.push(control.txFactory.createTxUpdateDoc(doc._class, peer.extra.space, peer.cardId, updateTx.operations))
}
return res
}
async function updateParentInfoName (
control: TriggerControl,
parent: Ref<Card>,
title: string,
originParent: Ref<Card>
): Promise<Tx[]> {
const res: Tx[] = []
const childs = await control.findAll(control.ctx, card.class.Card, { parent })
for (const child of childs) {
if (child._id === originParent) continue
const parentInfo = child.parentInfo
const index = parentInfo.findIndex((p) => p._id === parent)
if (index === -1) {
continue
}
parentInfo[index].title = title
res.push(
control.txFactory.createTxUpdateDoc(child._class, child.space, child._id, {
parentInfo
})
)
res.push(...(await updateParentInfoName(control, child._id, title, originParent)))
}
return res
}
async function OnThreadCreate (ctx: TxCreateDoc<Card>[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of ctx) {
const doc = TxProcessor.createDoc2Doc(tx)
if (doc.peerId != null) continue
const parent = doc.parentInfo?.[0]
if (parent == null) continue
if (!control.hierarchy.isDerived(parent._class, communication.type.Direct)) continue
const direct = (await control.findAll(control.ctx, parent._class, { _id: parent._id }, { limit: 1 }))[0] as Direct
if (direct == null) continue
const peerId = generateId()
res.push(control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { peerId }))
res.push(...(await createThreadCardPeers(direct, doc, control, peerId)))
}
return res
}
async function createThreadCardPeers (
direct: Direct,
doc: Card,
control: TriggerControl,
peerId: string
): Promise<Tx[]> {
const res: Tx[] = []
const cardIds = new Map<Ref<Card>, Ref<Space>>([[doc._id, doc.space]])
const members = direct.members ?? []
if (members.length === 0) return []
const thread = (
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
findThreads: { params: { threadId: doc._id } }
})
).value[0]
if (thread === undefined) return []
const messageId = thread.messageId
const directPeer = (
(
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
findPeers: { params: { kind: 'card', cardId: direct._id } }
})
).value as CardPeer[]
)[0]
const personSpaces = (await getPersonSpaces(control)).filter(
(it) => it._id !== doc.space && members.includes(it.person)
)
if (personSpaces.length === 0) return []
const accounts = (
await control.findAll(control.ctx, contact.mixin.Employee, {
_id: { $in: personSpaces.map((it) => it.person) as Ref<Employee>[] }
})
)
.map((it) => it.personUuid)
.filter(notEmpty)
if (accounts.length === 0) return []
// TODO: create directs in person_workspace
for (const personSpace of personSpaces) {
const _id = generateId<Card>()
const _class = doc._class
cardIds.set(_id, personSpace._id)
res.push(
control.txFactory.createTxCreateDoc(
_class,
personSpace._id,
{
...doc,
peerId
},
_id
)
)
const parentDirect = directPeer?.members?.find((m) => m.extra?.space === personSpace._id)
if (parentDirect !== undefined) {
const threadPatchEvent: ThreadPatchEvent = {
type: MessageEventType.ThreadPatch,
cardId: parentDirect.cardId,
messageId,
operation: {
opcode: 'attach',
threadId: _id,
threadType: _class
},
socialId: doc.modifiedBy
}
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
event: threadPatchEvent
})
}
}
if (cardIds.size > 1) {
let newValue = true
for (const [cardId, spaceId] of cardIds.entries()) {
const event: CreatePeerEvent = {
type: PeerEventType.CreatePeer,
workspaceId: control.workspace.uuid, // TODO: person_workspace
cardId,
kind: 'card',
value: peerId,
extra: { space: spaceId },
date: new Date(doc.modifiedOn),
options: { newValue }
}
await control.domainRequest(control.ctx, 'communication' as OperationDomain, { event })
newValue = false
}
}
return res
}
function getDirectTitle (employees: Employee[], me: Ref<Person>): string {
if (employees.length === 1) {
return employees.map((e) => formatName(e.name)).join(', ')
} else {
return employees
.filter((it) => it._id !== me)
.map((e) => formatName(e.name))
.join(', ')
}
}
async function createDirectCardPeers (
doc: Card,
members: Ref<Person>[],
control: TriggerControl,
peerId: string
): Promise<Tx[]> {
const res: Tx[] = []
const cardIds = new Map<Ref<Card>, Ref<Space>>([[doc._id, doc.space]])
if (members.length === 0) return []
const personSpaces = (await getPersonSpaces(control)).filter((it) => members.includes(it.person))
if (personSpaces.length <= 1) return []
const employees = await control.findAll(control.ctx, contact.mixin.Employee, {
_id: { $in: personSpaces.map((it) => it.person) as Ref<Employee>[] }
})
const accounts = employees.map((it) => it.personUuid).filter(notEmpty)
if (accounts.length === 0) return []
// TODO: create directs in person_workspace
for (const personSpace of personSpaces) {
if (personSpace._id === doc.space) continue
const _id = generateId<Card>()
const _class = doc._class
cardIds.set(_id, personSpace._id)
const title = getDirectTitle(employees, personSpace.person)
res.push(
control.txFactory.createTxCreateDoc(
_class,
personSpace._id,
{
...doc,
peerId,
title
},
_id
)
)
const event: AddCollaboratorsEvent = {
type: NotificationEventType.AddCollaborators,
cardId: _id,
cardType: _class,
collaborators: accounts,
socialId: doc.modifiedBy,
date: new Date(doc.modifiedOn + 1)
}
await control.domainRequest(control.ctx, 'communication' as OperationDomain, { event })
}
if (cardIds.size > 1) {
let newValue = true
for (const [cardId, spaceId] of cardIds.entries()) {
const event: CreatePeerEvent = {
type: PeerEventType.CreatePeer,
workspaceId: control.workspace.uuid, // TODO: person_workspace
cardId,
kind: 'card',
value: peerId,
extra: { space: spaceId },
date: new Date(doc.modifiedOn),
options: { newValue }
}
await control.domainRequest(control.ctx, 'communication' as OperationDomain, { event })
newValue = false
}
}
return res
}
async function OnDirectCreate (ctx: TxCreateDoc<Direct>[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of ctx) {
const doc = TxProcessor.createDoc2Doc(tx)
if (doc.peerId != null) continue
const members = doc.members ?? []
const peerId = generateId()
res.push(control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { peerId }))
res.push(...(await createDirectCardPeers(doc, members, control, peerId)))
}
return res
}
async function OnCardCreate (ctx: TxCreateDoc<Card>[], control: TriggerControl): Promise<Tx[]> {
const createTx = ctx[0]
const doc = TxProcessor.createDoc2Doc(createTx)
const res: Tx[] = []
if (doc.parent != null) {
const parent = (await control.findAll(control.ctx, card.class.Card, { _id: doc.parent }))[0]
if (parent !== undefined) {
res.push(
control.txFactory.createTxUpdateDoc(parent._class, parent.space, parent._id, {
$inc: {
children: 1
}
})
)
if ((doc.parentInfo?.length ?? 0) === 0) {
const parentInfo = [
...(parent.parentInfo ?? []),
{
_id: parent._id,
_class: parent._class,
title: parent.title
}
]
res.push(
control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, {
parentInfo
})
)
}
}
}
await createCollaborators(control, ctx)
return res
}
async function createCollaborators (control: TriggerControl, ctx: TxCreateDoc<Card>[]): Promise<void> {
for (const tx of ctx) {
const modifier = await getEmployee(control, tx.modifiedBy)
const collaborators: AccountUuid[] = []
if (modifier?.personUuid != null && modifier.active) {
collaborators.push(modifier.personUuid)
}
const personSpaces = await getPersonSpaces(control)
const personSpace = personSpaces.find((it) => it._id === tx.objectSpace)
if (personSpace != null && personSpace.person !== modifier?._id) {
const spacePerson = (await control.findAll(control.ctx, contact.class.Person, { _id: personSpace.person }))[0]
if (spacePerson?.personUuid != null) {
collaborators.push(spacePerson.personUuid as AccountUuid)
}
}
if (collaborators.length === 0) continue
const event: AddCollaboratorsEvent = {
type: NotificationEventType.AddCollaborators,
cardId: tx.objectId,
cardType: tx.objectClass,
collaborators,
socialId: tx.createdBy ?? tx.modifiedBy,
date: new Date((tx.createdOn ?? tx.modifiedOn) + 1)
}
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
event
})
}
}
export async function OnCardTag (ctx: TxMixin<Card, Card>[], control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
for (const tx of ctx) {
if (tx.space === core.space.DerivedTx) continue
if (tx._class !== core.class.TxMixin) continue
const target = tx.mixin
const to = control.hierarchy.getBaseClass(target)
const ancestors = control.hierarchy.getAncestors(target).filter((p) => control.hierarchy.isDerived(p, to))
const mixinAncestors: Ref<Mixin<Doc>>[] = []
const doc = (await control.findAll(control.ctx, tx.objectClass, { _id: tx.objectId }))[0]
if (doc === undefined) continue
for (const anc of ancestors) {
if (anc === target) continue
if (control.hierarchy.hasMixin(doc, anc)) break
if (anc === to) break
mixinAncestors.unshift(anc)
}
for (const anc of mixinAncestors) {
res.push(control.txFactory.createTxMixin(doc._id, doc._class, doc.space, anc, {}))
}
const updated = fillDefaults(control.hierarchy, control.hierarchy.as(control.hierarchy.clone(doc), target), target)
const diff = getDiffUpdate(doc, updated)
const splitted = splitMixinUpdate(control.hierarchy, diff, target, doc._class)
for (const it of splitted) {
if (control.hierarchy.isMixin(it[0])) {
res.push(control.txFactory.createTxMixin(doc._id, doc._class, doc.space, it[0], it[1]))
} else {
res.push(control.txFactory.createTxUpdateDoc(it[0], doc.space, doc._id, it[1]))
}
}
await updateCollaborators(control, tx.attributes, tx.mixin, doc, tx.modifiedBy)
}
return res
}
export async function CardTextPresenter (doc: Doc): Promise<string> {
const card = doc as Card
return card.title
}
export async function CardHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
const card = doc as Card
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
const path = `${workbenchId}/${control.workspace.url}/${cardId}/${card._id}`
const link = concatLink(front, path)
return `<a href='${link}'>${card.title}</a>`
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
function: {
CardTextPresenter,
CardHTMLPresenter
},
trigger: {
OnAttribute,
OnAttributeRemove,
OnMasterTagCreate,
OnMasterTagRemove,
OnTagRemove,
OnCardRemove,
OnCardCreate,
OnCardUpdate,
OnCardTag,
OnDirectCreate,
OnThreadCreate
}
})