mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-10 03:37:43 +02:00
509 lines
15 KiB
TypeScript
509 lines
15 KiB
TypeScript
//
|
|
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
|
// 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
|
|
// 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 card from '@hcengineering/card'
|
|
import contact, {
|
|
Channel,
|
|
Contact,
|
|
contactId,
|
|
Employee,
|
|
formatContactName,
|
|
formatName,
|
|
getFirstName,
|
|
getLastName,
|
|
getName,
|
|
Organization,
|
|
Person,
|
|
PersonSpace,
|
|
type UserProfile
|
|
} from '@hcengineering/contact'
|
|
import core, {
|
|
AccountRole,
|
|
AccountUuid,
|
|
concatLink,
|
|
Doc,
|
|
Hierarchy,
|
|
MarkupBlobRef,
|
|
Ref,
|
|
SocialIdType,
|
|
type Space,
|
|
SpaceType,
|
|
systemAccountUuid,
|
|
readOnlyGuestAccountUuid,
|
|
Tx,
|
|
TxCreateDoc,
|
|
TxCUD,
|
|
TxMixin,
|
|
TxRemoveDoc,
|
|
TxUpdateDoc,
|
|
TypedSpace,
|
|
TxFactory,
|
|
PermissionsGrant
|
|
} from '@hcengineering/core'
|
|
import { getMetadata } from '@hcengineering/platform'
|
|
import { makeRank } from '@hcengineering/rank'
|
|
import { getAccountBySocialId, getCurrentPerson } from '@hcengineering/server-contact'
|
|
import serverCore, { TriggerControl } from '@hcengineering/server-core'
|
|
import { workbenchId } from '@hcengineering/workbench'
|
|
|
|
export async function OnSpaceTypeMembers (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
|
const result: Tx[] = []
|
|
for (const tx of txes) {
|
|
const ctx = tx as TxUpdateDoc<SpaceType>
|
|
const newMember = ctx.operations.$push?.members as AccountUuid
|
|
if (newMember !== undefined) {
|
|
const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId })
|
|
for (const space of spaces) {
|
|
if (space.members.includes(newMember)) continue
|
|
const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
|
|
$push: {
|
|
members: newMember
|
|
}
|
|
})
|
|
result.push(pushTx)
|
|
}
|
|
}
|
|
const oldMember = ctx.operations.$pull?.members as AccountUuid
|
|
if (ctx.operations.$pull?.members !== undefined) {
|
|
const spaces = await control.findAll(control.ctx, core.class.Space, { type: ctx.objectId })
|
|
for (const space of spaces) {
|
|
if (!space.members.includes(oldMember)) continue
|
|
const pullTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
|
|
$pull: {
|
|
members: oldMember
|
|
}
|
|
})
|
|
result.push(pullTx)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
async function getGrantSpaces (control: TriggerControl, grant?: PermissionsGrant): Promise<Space[]> {
|
|
const spacesRefs = grant?.spaces
|
|
if (spacesRefs == null) {
|
|
return []
|
|
}
|
|
|
|
const grantSpaces = await control.findAll(control.ctx, core.class.Space, { _id: { $in: spacesRefs } })
|
|
const allowedGrantedSpaces: Space[] = []
|
|
|
|
for (const space of grantSpaces) {
|
|
if (!space.private) {
|
|
allowedGrantedSpaces.push(space)
|
|
continue
|
|
}
|
|
|
|
const grantedBy = grant?.grantedBy
|
|
if (grantedBy == null) {
|
|
control.ctx.warn('Granted private space access without grantor is not allowed', { space })
|
|
continue
|
|
}
|
|
|
|
if (space.members.includes(grantedBy)) {
|
|
allowedGrantedSpaces.push(space)
|
|
} else {
|
|
control.ctx.warn('Granted private space access but the grantor is not a member of the workspace', {
|
|
space,
|
|
grantedBy
|
|
})
|
|
}
|
|
}
|
|
|
|
return allowedGrantedSpaces
|
|
}
|
|
|
|
export async function OnEmployeeCreate (_txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
|
const result: Tx[] = []
|
|
|
|
const systemTxFactory = new TxFactory(core.account.System, false)
|
|
const systemTxes: Tx[] = []
|
|
|
|
for (const tx of _txes) {
|
|
const mixinTx = tx as TxMixin<Person, Employee>
|
|
if (mixinTx.attributes.active !== true) continue
|
|
|
|
const person = (await control.findAll(control.ctx, contact.class.Person, { _id: mixinTx.objectId }))[0]
|
|
const account = person?.personUuid as AccountUuid
|
|
if (account === undefined) continue
|
|
|
|
const txes = await createPersonSpace(account, mixinTx.objectId, control)
|
|
result.push(...txes)
|
|
|
|
const emp = control.hierarchy.as(person, contact.mixin.Employee)
|
|
if (emp.role === 'GUEST') {
|
|
let readOnlyGuestSpaces: Space[] = []
|
|
const readonlyEmployees = await control.findAll(control.ctx, contact.mixin.Employee, {
|
|
personUuid: readOnlyGuestAccountUuid
|
|
})
|
|
if (readonlyEmployees.length !== 0) {
|
|
const readonlyEmployee = readonlyEmployees[0]
|
|
if (readonlyEmployee.active) {
|
|
readOnlyGuestSpaces = await control.findAll(control.ctx, core.class.Space, {
|
|
members: readOnlyGuestAccountUuid
|
|
})
|
|
}
|
|
}
|
|
|
|
const grantSpaces = await getGrantSpaces(control, control.ctx.contextData.grant)
|
|
|
|
for (const space of [...readOnlyGuestSpaces, ...grantSpaces]) {
|
|
if (space._class === contact.class.PersonSpace || space.members.includes(account)) continue
|
|
|
|
const pushTx = systemTxFactory.createTxUpdateDoc(space._class, space.space, space._id, {
|
|
$push: {
|
|
members: account
|
|
}
|
|
})
|
|
systemTxes.push(pushTx)
|
|
}
|
|
|
|
const collabs = await control.findAll(control.ctx, core.class.Collaborator, {
|
|
collaborator: readOnlyGuestAccountUuid
|
|
})
|
|
|
|
for (const collab of collabs) {
|
|
const pushTx = systemTxFactory.createTxCreateDoc(core.class.Collaborator, collab.space, {
|
|
attachedTo: collab.attachedTo,
|
|
collaborator: account,
|
|
attachedToClass: collab.attachedToClass,
|
|
collection: 'collaborators'
|
|
})
|
|
systemTxes.push(pushTx)
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
const spaces = await control.findAll(control.ctx, core.class.Space, { autoJoin: true })
|
|
const grantSpaces = await getGrantSpaces(control, control.ctx.contextData.grant)
|
|
|
|
for (const space of [...spaces, ...grantSpaces]) {
|
|
if (space.members.includes(account)) continue
|
|
|
|
const pushTx = systemTxFactory.createTxUpdateDoc(space._class, space.space, space._id, {
|
|
$push: {
|
|
members: account
|
|
}
|
|
})
|
|
systemTxes.push(pushTx)
|
|
}
|
|
}
|
|
|
|
await control.apply(control.ctx, systemTxes)
|
|
|
|
const account = control.ctx.contextData.account
|
|
if (account.role !== AccountRole.Owner) return result
|
|
|
|
const typedSpaces: Space[] = [
|
|
...(await control.findAll(control.ctx, core.class.TypedSpace, {})),
|
|
...(await control.findAll(control.ctx, card.class.CardSpace, { space: core.space.Space }))
|
|
]
|
|
|
|
for (const space of typedSpaces) {
|
|
if (space === undefined) continue
|
|
|
|
const owners = space.owners ?? []
|
|
|
|
if (owners.length === 0 || (owners.length === 1 && owners[0] === systemAccountUuid)) {
|
|
result.push(
|
|
control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, {
|
|
owners: [account.uuid]
|
|
})
|
|
)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
export async function OnTypedSpaceCreate (_txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
|
const result: Tx[] = []
|
|
for (const tx of _txes) {
|
|
const ctx = tx as TxCreateDoc<TypedSpace>
|
|
const owners = ctx.attributes.owners ?? []
|
|
|
|
if (owners.length === 0 || (owners.length === 1 && owners[0] === systemAccountUuid)) {
|
|
const members = ctx.attributes.members
|
|
if (members.length === 0) continue
|
|
result.push(
|
|
control.txFactory.createTxUpdateDoc(ctx.objectClass, ctx.space, ctx.objectId, {
|
|
owners: [members[0]]
|
|
})
|
|
)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
export async function OnPersonCreate (_txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
|
const result: Tx[] = []
|
|
for (const tx of _txes) {
|
|
const ctx = tx as TxCreateDoc<Person>
|
|
const userProfileTx = control.txFactory.createTxCreateDoc<UserProfile>(
|
|
contact.class.UserProfile,
|
|
card.space.Default,
|
|
{
|
|
person: ctx.objectId,
|
|
title: formatName(ctx.attributes.name),
|
|
rank: makeRank(undefined, undefined),
|
|
content: '' as MarkupBlobRef,
|
|
parentInfo: [],
|
|
blobs: {}
|
|
}
|
|
)
|
|
|
|
result.push(userProfileTx)
|
|
result.push(
|
|
control.txFactory.createTxUpdateDoc<Person>(ctx.objectClass, ctx.objectSpace, ctx.objectId, {
|
|
profile: userProfileTx.objectId
|
|
})
|
|
)
|
|
}
|
|
return result
|
|
}
|
|
|
|
async function createPersonSpace (
|
|
account: AccountUuid,
|
|
person: Ref<Person>,
|
|
control: TriggerControl
|
|
): Promise<TxCUD<PersonSpace>[]> {
|
|
const personSpace = (await control.findAll(control.ctx, contact.class.PersonSpace, { person }, { limit: 1 }))[0]
|
|
if (personSpace !== undefined) return []
|
|
|
|
return [
|
|
control.txFactory.createTxCreateDoc(contact.class.PersonSpace, core.space.Space, {
|
|
name: 'Personal space',
|
|
description: '',
|
|
private: true,
|
|
archived: false,
|
|
person,
|
|
members: [account]
|
|
})
|
|
]
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function OnContactDelete (
|
|
txes: Tx[],
|
|
{ findAll, hierarchy, storageAdapter, workspace, removedMap, txFactory, ctx }: TriggerControl
|
|
): Promise<Tx[]> {
|
|
const result: Tx[] = []
|
|
for (const tx of txes) {
|
|
const rmTx = tx as TxRemoveDoc<Contact>
|
|
|
|
const removeContact = removedMap.get(rmTx.objectId) as Contact
|
|
if (removeContact === undefined) {
|
|
continue
|
|
}
|
|
|
|
const members = await findAll(ctx, contact.class.Member, { contact: removeContact._id })
|
|
for (const member of members) {
|
|
const removeTx = txFactory.createTxRemoveDoc(member._class, member.space, member._id)
|
|
const tx = txFactory.createTxCollectionCUD(
|
|
member.attachedToClass,
|
|
member.attachedTo,
|
|
member.space,
|
|
member.collection,
|
|
removeTx
|
|
)
|
|
result.push(tx)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function OnChannelUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
|
const result: Tx[] = []
|
|
|
|
for (const tx of txes) {
|
|
const uTx = tx as TxUpdateDoc<Channel>
|
|
|
|
if (uTx.operations.$inc?.items !== undefined) {
|
|
const doc = (await control.findAll(control.ctx, uTx.objectClass, { _id: uTx.objectId }, { limit: 1 }))[0]
|
|
const account = await getAccountBySocialId(control, tx.modifiedBy)
|
|
if (doc !== undefined && account != null) {
|
|
const tx = control.txFactory.createTxCreateDoc(core.class.Collaborator, doc.space, {
|
|
attachedTo: doc._id,
|
|
attachedToClass: doc._class,
|
|
collection: 'collaborators',
|
|
collaborator: account
|
|
})
|
|
result.push(tx)
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function personHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
|
const person = doc as Person
|
|
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
|
const path = `${workbenchId}/${control.workspace.url}/${contactId}/${doc._id}`
|
|
const link = concatLink(front, path)
|
|
return `<a href="${link}">${getName(control.hierarchy, person, control.branding?.lastNameFirst)}</a>`
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function personTextPresenter (doc: Doc, control: TriggerControl): string {
|
|
const person = doc as Person
|
|
return `${getName(control.hierarchy, person, control.branding?.lastNameFirst)}`
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export async function organizationHTMLPresenter (doc: Doc, control: TriggerControl): Promise<string> {
|
|
const organization = doc as Organization
|
|
const front = control.branding?.front ?? getMetadata(serverCore.metadata.FrontUrl) ?? ''
|
|
const path = `${workbenchId}/${control.workspace.url}/${contactId}/${doc._id}`
|
|
const link = concatLink(front, path)
|
|
return `<a href="${link}">${organization.name}</a>`
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function organizationTextPresenter (doc: Doc): string {
|
|
const organization = doc as Organization
|
|
return `${organization.name}`
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
export function contactNameProvider (
|
|
doc: Doc,
|
|
parent: Doc | undefined,
|
|
space: Space | undefined,
|
|
hierarchy: Hierarchy,
|
|
mode: string
|
|
): string {
|
|
if (parent !== undefined && hierarchy.isDerived(parent._class, contact.class.Contact)) {
|
|
return formatContactName(hierarchy, parent._class, (parent as Contact).name ?? '', mode)
|
|
}
|
|
return formatContactName(hierarchy, doc._class, (doc as Contact).name ?? '', mode)
|
|
}
|
|
|
|
export async function getCurrentEmployeeName (control: TriggerControl, context: Record<string, Doc>): Promise<string> {
|
|
const person = await getCurrentPerson(control)
|
|
|
|
return person !== undefined ? formatName(person.name, control.branding?.lastNameFirst) : ''
|
|
}
|
|
|
|
export async function getCurrentEmployeeEmail (control: TriggerControl, context: Record<string, Doc>): Promise<string> {
|
|
const person = await getCurrentPerson(control)
|
|
if (person === undefined) return ''
|
|
|
|
const emailSocialId = (
|
|
await control.findAll(control.ctx, contact.class.SocialIdentity, {
|
|
attachedTo: person._id,
|
|
attachedToClass: contact.class.Person,
|
|
type: SocialIdType.EMAIL
|
|
})
|
|
)[0]
|
|
if (emailSocialId === undefined) return ''
|
|
|
|
return emailSocialId.value
|
|
}
|
|
|
|
export async function getCurrentEmployeePosition (
|
|
control: TriggerControl,
|
|
context: Record<string, Doc>
|
|
): Promise<string | undefined> {
|
|
const person = await getCurrentPerson(control)
|
|
if (person === undefined) return ''
|
|
|
|
return control.hierarchy.as(person, contact.mixin.Employee)?.position ?? ''
|
|
}
|
|
|
|
export async function getContactName (
|
|
control: TriggerControl,
|
|
context: Record<string, Doc>
|
|
): Promise<string | undefined> {
|
|
const value = context[contact.class.Contact] as Contact
|
|
if (value === undefined) return
|
|
if (control.hierarchy.isDerived(value._class, contact.class.Person)) {
|
|
return getName(control.hierarchy, value, control.branding?.lastNameFirst)
|
|
} else {
|
|
return value.name
|
|
}
|
|
}
|
|
|
|
export async function getContactLastName (
|
|
control: TriggerControl,
|
|
context: Record<string, Doc>
|
|
): Promise<string | undefined> {
|
|
const value = context[contact.class.Contact] as Contact
|
|
if (value === undefined) return
|
|
if (control.hierarchy.isDerived(value._class, contact.class.Person)) {
|
|
return getLastName(value.name)
|
|
} else {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
export async function getContactFirstName (
|
|
control: TriggerControl,
|
|
context: Record<string, Doc>
|
|
): Promise<string | undefined> {
|
|
const value = context[contact.class.Contact] as Contact
|
|
if (value === undefined) return
|
|
if (control.hierarchy.isDerived(value._class, contact.class.Person)) {
|
|
return getFirstName(value.name)
|
|
} else {
|
|
return value.name
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
export default async () => ({
|
|
trigger: {
|
|
OnEmployeeCreate,
|
|
OnTypedSpaceCreate,
|
|
OnPersonCreate,
|
|
OnContactDelete,
|
|
OnChannelUpdate,
|
|
OnSpaceTypeMembers
|
|
},
|
|
function: {
|
|
PersonHTMLPresenter: personHTMLPresenter,
|
|
PersonTextPresenter: personTextPresenter,
|
|
OrganizationHTMLPresenter: organizationHTMLPresenter,
|
|
OrganizationTextPresenter: organizationTextPresenter,
|
|
ContactNameProvider: contactNameProvider,
|
|
GetCurrentEmployeeName: getCurrentEmployeeName,
|
|
GetCurrentEmployeeEmail: getCurrentEmployeeEmail,
|
|
GetContactName: getContactName,
|
|
GetCurrentEmployeePosition: getCurrentEmployeePosition,
|
|
GetContactFirstName: getContactFirstName,
|
|
GetContactLastName: getContactLastName
|
|
}
|
|
})
|