diff --git a/common/scripts/version.txt b/common/scripts/version.txt index 293d8d75c6..74327e84bd 100644 --- a/common/scripts/version.txt +++ b/common/scripts/version.txt @@ -1 +1 @@ -"0.7.145" +"0.7.145" \ No newline at end of file diff --git a/models/activity/src/notification.ts b/models/activity/src/notification.ts index 521d85b8a6..c7c05922ce 100644 --- a/models/activity/src/notification.ts +++ b/models/activity/src/notification.ts @@ -1,8 +1,9 @@ import notification from '@hcengineering/notification' -import core from '@hcengineering/core' +import core, { type ClassCollaborators } from '@hcengineering/core' import { type Builder } from '@hcengineering/model' import activity from './plugin' +import { type ActivityMessage, type DocUpdateMessage } from '@hcengineering/activity' export function buildNotifications (builder: Builder): void { builder.createDoc( @@ -41,11 +42,13 @@ export function buildNotifications (builder: Builder): void { enabledTypes: [activity.ids.AddReactionNotification] }) - builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: activity.class.ActivityMessage, fields: ['createdBy', 'repliedPersons'] }) - builder.mixin(activity.class.DocUpdateMessage, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: activity.class.DocUpdateMessage, fields: ['createdBy', 'repliedPersons'] }) diff --git a/models/calendar/src/index.ts b/models/calendar/src/index.ts index 904209223f..8b473541f2 100644 --- a/models/calendar/src/index.ts +++ b/models/calendar/src/index.ts @@ -35,7 +35,8 @@ import { type Markup, type Ref, type Timestamp, - type PersonId + type PersonId, + type ClassCollaborators } from '@hcengineering/core' import { ArrOf, @@ -247,7 +248,8 @@ export function createModel (builder: Builder): void { calendar.ids.CalendarNotificationGroup ) - builder.mixin(calendar.class.Event, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: calendar.class.Event, fields: ['participants'] }) diff --git a/models/chunter/src/notifications.ts b/models/chunter/src/notifications.ts index bc9b8c696b..2578ce45d4 100644 --- a/models/chunter/src/notifications.ts +++ b/models/chunter/src/notifications.ts @@ -19,13 +19,17 @@ import core from '@hcengineering/model-core' import activity from '@hcengineering/activity' import chunter from './plugin' +import { type ClassCollaborators } from '@hcengineering/core' +import { type Channel, type DirectMessage } from '@hcengineering/chunter' export function defineNotifications (builder: Builder): void { - builder.mixin(chunter.class.DirectMessage, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: chunter.class.DirectMessage, fields: ['members'] }) - builder.mixin(chunter.class.Channel, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: chunter.class.Channel, fields: ['members'] }) diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index db1cd0cc2b..4abee5afa4 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -36,6 +36,7 @@ import { } from '@hcengineering/contact' import { AccountRole, + type ClassCollaborators, DOMAIN_MODEL, DateRangeMode, IndexKind, @@ -615,7 +616,8 @@ export function createModel (builder: Builder): void { inlineEditor: contact.component.ContactArrayEditor }) - builder.mixin(contact.class.Contact, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: contact.class.Contact, fields: [] }) @@ -623,7 +625,8 @@ export function createModel (builder: Builder): void { component: contact.component.ChannelPanel }) - builder.mixin(contact.class.Channel, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: contact.class.Channel, fields: ['modifiedBy'] }) diff --git a/models/controlled-documents/src/index.ts b/models/controlled-documents/src/index.ts index 93bf5e9b40..5eb65a7784 100644 --- a/models/controlled-documents/src/index.ts +++ b/models/controlled-documents/src/index.ts @@ -16,6 +16,7 @@ import activity from '@hcengineering/activity' import contact from '@hcengineering/contact' import documentsPlugin, { + type ControlledDocument, documentsId, DocumentState, type Document, @@ -37,7 +38,7 @@ import setting from '@hcengineering/setting' import tags from '@hcengineering/tags' import textEditor from '@hcengineering/text-editor' -import { AccountRole, type Class, type Doc, type Ref } from '@hcengineering/core' +import { AccountRole, type ClassCollaborators, type Class, type Doc, type Ref } from '@hcengineering/core' import { type Action } from '@hcengineering/view' import { definePermissions } from './permissions' import documents from './plugin' @@ -976,7 +977,8 @@ export function defineNotifications (builder: Builder): void { components: { input: { component: chunter.component.ChatMessageInput } } }) - builder.mixin(documents.class.ControlledDocument, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: documents.class.ControlledDocument, fields: ['author', 'owner', 'reviewers', 'approvers', 'coAuthors'] }) diff --git a/models/core/src/core.ts b/models/core/src/core.ts index 5e83792388..ad9798ee40 100644 --- a/models/core/src/core.ts +++ b/models/core/src/core.ts @@ -14,24 +14,28 @@ // import { - type PersonId, + type AccountUuid, type AnyAttribute, type ArrOf, type Association, type AttachedDoc, type Blob, type Class, + type ClassCollaborators, type ClassifierKind, + type Collaborator, type Collection, type Configuration, type ConfigurationElement, type Doc, type Domain, DOMAIN_BLOB, + DOMAIN_COLLABORATOR, DOMAIN_CONFIGURATION, DOMAIN_MIGRATION, DOMAIN_MODEL, DOMAIN_RELATION, + DOMAIN_SEQUENCE, type DomainIndexConfiguration, type Enum, type EnumOf, @@ -43,18 +47,18 @@ import { type MigrationState, type Mixin, type Obj, + type PersonId, type PluginConfiguration, type Ref, type RefTo, type Relation, - type Space, type Sequence, + type Space, type Timestamp, type TransientConfiguration, type Type, type TypeAny, - type Version, - DOMAIN_SEQUENCE + type Version } from '@hcengineering/core' import { Hidden, @@ -66,10 +70,10 @@ import { TypeBoolean, TypeFileSize, TypeIntlString, + TypePersonId, TypeRef, TypeString, TypeTimestamp, - TypePersonId, UX } from '@hcengineering/model' import { getEmbeddedLabel, type IntlString, type Plugin } from '@hcengineering/platform' @@ -390,3 +394,15 @@ export class TSequence extends TDoc implements Sequence { sequence!: number } + +@Model(core.class.ClassCollaborators, core.class.Doc, DOMAIN_MODEL) +export class TClassCollaborators extends TDoc implements ClassCollaborators { + attachedTo!: Ref> + fields!: (keyof Doc)[] + provideSecurity?: boolean +} + +@Model(core.class.Collaborator, core.class.Doc, DOMAIN_COLLABORATOR) +export class TCollaborator extends TAttachedDoc implements Collaborator { + collaborator!: AccountUuid +} diff --git a/models/core/src/index.ts b/models/core/src/index.ts index d30190a37a..a138a1bf6d 100644 --- a/models/core/src/index.ts +++ b/models/core/src/index.ts @@ -69,7 +69,9 @@ import { TTypeString, TTypeTimestamp, TVersion, - TSequence + TSequence, + TClassCollaborators, + TCollaborator } from './core' import { definePermissions } from './permissions' import { TPermission, TRole, TSpace, TSpaceType, TSpaceTypeDescriptor, TSystemSpace, TTypedSpace } from './security' @@ -161,7 +163,9 @@ export function createModel (builder: Builder): void { TAssociation, TDomainIndexConfiguration, TBenchmarkDoc, - TTransientConfiguration + TTransientConfiguration, + TClassCollaborators, + TCollaborator ) builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, { diff --git a/models/document/src/index.ts b/models/document/src/index.ts index ecd8bc5da4..94eb41ae01 100644 --- a/models/document/src/index.ts +++ b/models/document/src/index.ts @@ -14,7 +14,16 @@ // import activity from '@hcengineering/activity' -import type { CollectionSize, MarkupBlobRef, Domain, Rank, Ref, Role, RolesAssignment } from '@hcengineering/core' +import type { + CollectionSize, + MarkupBlobRef, + Domain, + Rank, + Ref, + Role, + RolesAssignment, + ClassCollaborators +} from '@hcengineering/core' import { AccountUuid, AccountRole, IndexKind } from '@hcengineering/core' import { type Document, @@ -423,7 +432,8 @@ function defineDocument (builder: Builder): void { builder.mixin(document.class.Document, core.class.Class, activity.mixin.ActivityDoc, {}) - builder.mixin(document.class.Document, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: document.class.Document, fields: ['createdBy', 'modifiedBy'] }) diff --git a/models/lead/src/index.ts b/models/lead/src/index.ts index 56964a75bf..0db4cce2b1 100644 --- a/models/lead/src/index.ts +++ b/models/lead/src/index.ts @@ -15,7 +15,7 @@ // To help typescript locate view plugin properly import activity from '@hcengineering/activity' -import { AccountRole, SortingOrder, type FindOptions } from '@hcengineering/core' +import { AccountRole, type ClassCollaborators, SortingOrder, type FindOptions } from '@hcengineering/core' import { leadId, type Lead } from '@hcengineering/lead' import { type Builder } from '@hcengineering/model' import chunter from '@hcengineering/model-chunter' @@ -539,7 +539,8 @@ export function createModel (builder: Builder): void { filters: ['attachedTo'] }) - builder.mixin(lead.class.Lead, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: lead.class.Lead, fields: ['createdBy', 'assignee'] }) diff --git a/models/love/src/index.ts b/models/love/src/index.ts index 4dc7bb34a4..2326f2a4a7 100644 --- a/models/love/src/index.ts +++ b/models/love/src/index.ts @@ -24,7 +24,9 @@ import { type Timestamp, DOMAIN_TRANSIENT, DateRangeMode, - IndexKind + IndexKind, + type ClassCollaborators, + type AccountUuid } from '@hcengineering/core' import { type DevicesPreference, @@ -157,6 +159,8 @@ export class TParticipantInfo extends TDoc implements ParticipantInfo { y!: number sessionId!: string | null + + account!: AccountUuid | null } @Model(love.class.JoinRequest, core.class.Doc, DOMAIN_TRANSIENT) @@ -663,8 +667,10 @@ export function createModel (builder: Builder): void { enabledTypes: [love.ids.MeetingMinutesChatNotification] }) - builder.mixin(love.class.MeetingMinutes, core.class.Class, notification.mixin.ClassCollaborators, { - fields: ['createdBy'] + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: love.class.MeetingMinutes, + fields: ['createdBy'], + provideSecurity: true }) builder.mixin(love.class.Room, core.class.Class, core.mixin.IndexConfiguration, { diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index 69caac5bbf..26fa7ca3f4 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -18,9 +18,12 @@ import activity, { type ActivityMessage } from '@hcengineering/activity' import { type PersonSpace } from '@hcengineering/contact' import { AccountRole, + type Collaborator, + type CollectionSize, DOMAIN_MODEL, + DOMAIN_TRANSIENT, IndexKind, - type PersonId, + type AccountUuid, type AttachedDoc, type Class, type Collection, @@ -29,41 +32,40 @@ import { type DocumentQuery, type IndexingConfiguration, type Markup, + type PersonId, type Ref, type Space, type Timestamp, type Tx, - type TxCUD, - DOMAIN_TRANSIENT, - type AccountUuid + type TxCUD } from '@hcengineering/core' import { - ArrOf, + Collection as CollectionType, Index, Mixin, Model, Prop, + TypeAccountUuid, TypeBoolean, TypeDate, TypeIntlString, TypeMarkup, TypeRef, UX, - type Builder, - TypeAccountUuid + type Builder } from '@hcengineering/model' import core, { TClass, TDoc } from '@hcengineering/model-core' import preference, { TPreference } from '@hcengineering/model-preference' import view, { createAction, template } from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' import { - notificationId, - DOMAIN_USER_NOTIFY, - DOMAIN_NOTIFICATION, + type Collaborators, DOMAIN_DOC_NOTIFY, + DOMAIN_NOTIFICATION, + DOMAIN_USER_NOTIFY, + notificationId, type ActivityInboxNotification, type ActivityNotificationViewlet, - type NotificationType, type BrowserNotification, type CommonInboxNotification, type DocNotifyContext, @@ -78,6 +80,7 @@ import { type NotificationProviderDefaults, type NotificationProviderSetting, type NotificationTemplate, + type NotificationType, type NotificationTypeSetting, type PushSubscription, type PushSubscriptionKeys @@ -88,7 +91,7 @@ import { type AnyComponent, type Location } from '@hcengineering/ui/src/types' import notification from './plugin' -export { notificationId, DOMAIN_USER_NOTIFY, DOMAIN_NOTIFICATION, DOMAIN_DOC_NOTIFY } from '@hcengineering/notification' +export { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION, DOMAIN_USER_NOTIFY, notificationId } from '@hcengineering/notification' export { notificationOperation } from './migration' export { notification as default } @@ -155,17 +158,11 @@ export class TNotificationProviderSetting extends TPreference implements Notific enabled!: boolean } -@Mixin(notification.mixin.ClassCollaborators, core.class.Class) -export class TClassCollaborators extends TClass { - fields!: string[] -} - @Mixin(notification.mixin.Collaborators, core.class.Doc) @UX(notification.string.Collaborators) -export class TCollaborators extends TDoc { - @Prop(ArrOf(TypeAccountUuid()), notification.string.Collaborators) - @Index(IndexKind.Indexed) - collaborators!: AccountUuid[] +export class TCollaborators extends TDoc implements Collaborators { + @Prop(CollectionType(core.class.Collaborator), notification.string.Collaborators) + collaborators!: CollectionSize } @Mixin(notification.mixin.NotificationObjectPresenter, core.class.Class) @@ -342,12 +339,11 @@ export const notificationActionTemplates = template({ export function createModel (builder: Builder): void { builder.createModel( + TCollaborators, TBrowserNotification, TNotificationType, TNotificationGroup, TNotificationPreferencesGroup, - TClassCollaborators, - TCollaborators, TNotificationObjectPresenter, TNotificationPreview, TDocNotifyContext, @@ -434,34 +430,39 @@ export function createModel (builder: Builder): void { label: notification.string.Collaborators, group: notification.ids.NotificationGroup, txClasses: [], - objectClass: notification.mixin.Collaborators, + objectClass: core.class.Collaborator, defaultEnabled: true }, notification.ids.CollaboratoAddNotification ) - builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, { - presenter: notification.component.NotificationCollaboratorsChanged, - messageMatch: { - _class: activity.class.DocUpdateMessage, - 'attributeUpdates.attrClass': notification.mixin.Collaborators - } - }) + builder.createDoc( + activity.class.DocUpdateMessageViewlet, + core.space.Model, + { + objectClass: core.class.Collaborator, + action: 'create', + icon: notification.icon.Notifications, + component: notification.component.CollaboratorsChanged + }, + notification.ids.CollaboratorsAddMessage + ) builder.createDoc( activity.class.DocUpdateMessageViewlet, core.space.Model, { - objectClass: notification.mixin.Collaborators, - action: 'update', + objectClass: core.class.Collaborator, + action: 'remove', icon: notification.icon.Notifications, - label: notification.string.ChangeCollaborators + component: notification.component.CollaboratorsChanged }, - notification.ids.CollaboratorsChangedMessage + notification.ids.CollaboratorsRemoveMessage ) - builder.mixin(notification.mixin.Collaborators, core.class.Class, activity.mixin.ActivityAttributeUpdatesPresenter, { - presenter: notification.component.CollaboratorsChanged + builder.mixin(core.class.Collaborator, core.class.Class, view.mixin.CollectionEditor, { + editor: notification.component.CollaboratorEditor, + inlineEditor: notification.component.CollaboratorEditor }) createAction( diff --git a/models/notification/src/migration.ts b/models/notification/src/migration.ts index 3c7bc95fd3..17e1f3d947 100644 --- a/models/notification/src/migration.ts +++ b/models/notification/src/migration.ts @@ -23,7 +23,10 @@ import core, { type DocumentQuery, type Ref, type Space, - type AccountUuid + type AccountUuid, + DOMAIN_COLLABORATOR, + type Collaborator, + generateId } from '@hcengineering/core' import { migrateSpace, @@ -39,7 +42,8 @@ import notification, { type PushSubscription, type BrowserNotification, type DocNotifyContext, - type InboxNotification + type InboxNotification, + type OldCollaborators } from '@hcengineering/notification' import { DOMAIN_PREFERENCE } from '@hcengineering/preference' @@ -239,6 +243,78 @@ export async function migrateDuplicateContexts (client: MigrationClient): Promis } } +async function migrateCollaborators (client: MigrationClient): Promise { + const alreadyMigrated = await client.find(DOMAIN_COLLABORATOR, {}, { limit: 1 }) + if (alreadyMigrated.length > 0) { + client.logger.log('collaborators already migrated, skipping', {}) + return + } + const hierarchy = client.hierarchy + client.logger.log('processing extract collaborators ', {}) + for (const domain of client.hierarchy.domains()) { + if (['tx'].includes(domain)) continue + client.logger.log('processing domain ', { domain }) + let processed = 0 + const iterator = await client.traverse(domain, {}) + + try { + while (true) { + const docs = await iterator.next(200) + if (docs === null || docs.length === 0) { + break + } + + const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + + const collabs: Collaborator[] = [] + + for (const doc of docs) { + const mixin = hierarchy.as(doc, notification.mixin.Collaborators) as any as OldCollaborators + const oldCollaborators = mixin.collaborators + + if (oldCollaborators === undefined || oldCollaborators.length === 0) continue + + for (const collab of oldCollaborators) { + collabs.push({ + _id: generateId(), + _class: core.class.Collaborator, + space: doc.space, + collaborator: collab, + attachedTo: doc._id, + attachedToClass: doc._class, + collection: 'collaborators', + modifiedOn: Date.now(), + modifiedBy: core.account.System + }) + } + + operations.push({ + filter: { _id: doc._id }, + update: { + $unset: { + 'notification:mixin:Collaborators': true + } + } + }) + } + + if (operations.length > 0) { + await client.bulk(domain, operations) + await client.create(DOMAIN_COLLABORATOR, collabs) + } + + processed += docs.length + client.logger.log('...processed', { count: processed }) + } + + client.logger.log('finished processing domain ', { domain, processed }) + } finally { + await iterator.close() + } + } + client.logger.log('finished processing collaborators ', {}) +} + /** * Migrates old accounts to new accounts/social ids. * Should be applied to prodcution directly without applying migrateSocialIdsToAccountUuids @@ -268,8 +344,10 @@ async function migrateAccounts (client: MigrationClient): Promise { const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + const collabs: Collaborator[] = [] + for (const doc of docs) { - const mixin = hierarchy.as(doc, notification.mixin.Collaborators) + const mixin = hierarchy.as(doc, notification.mixin.Collaborators) as any as OldCollaborators const oldCollaborators = mixin.collaborators if (oldCollaborators === undefined || oldCollaborators.length === 0) continue @@ -281,11 +359,25 @@ async function migrateAccounts (client: MigrationClient): Promise { accountUuidByOldAccount ) + for (const collab of newCollaborators) { + collabs.push({ + _id: generateId(), + _class: core.class.Collaborator, + space: doc.space, + collaborator: collab, + attachedTo: doc._id, + attachedToClass: doc._class, + collection: 'collaborators', + modifiedOn: Date.now(), + modifiedBy: core.account.System + }) + } + operations.push({ filter: { _id: doc._id }, update: { - [`${notification.mixin.Collaborators}`]: { - collaborators: newCollaborators + $unset: { + 'notification:mixin:Collaborators': true } } }) @@ -293,6 +385,7 @@ async function migrateAccounts (client: MigrationClient): Promise { if (operations.length > 0) { await client.bulk(domain, operations) + await client.create(DOMAIN_COLLABORATOR, collabs) } processed += docs.length @@ -751,6 +844,11 @@ export const notificationOperation: MigrateOperation = { state: 'accounts-to-social-ids-v2', mode: 'upgrade', func: migrateAccounts + }, + { + state: 'migrate-collaborators', + mode: 'upgrade', + func: migrateCollaborators } ]) }, diff --git a/models/notification/src/plugin.ts b/models/notification/src/plugin.ts index 5d0a661d19..1647f62ab0 100644 --- a/models/notification/src/plugin.ts +++ b/models/notification/src/plugin.ts @@ -27,7 +27,6 @@ export default mergeIds(notificationId, notification, { PlatformNotification: '' as IntlString, BrowserNotification: '' as IntlString, EmailNotification: '' as IntlString, - Collaborators: '' as IntlString, Archive: '' as IntlString, MarkAsUnread: '' as IntlString, MarkAsRead: '' as IntlString, @@ -42,7 +41,8 @@ export default mergeIds(notificationId, notification, { Inbox: '' as Ref }, ids: { - CollaboratorsChangedMessage: '' as Ref + CollaboratorsAddMessage: '' as Ref, + CollaboratorsRemoveMessage: '' as Ref }, component: { NotificationSettings: '' as AnyComponent, diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index ac1c12ada7..d13695cffd 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -14,7 +14,7 @@ // import activity from '@hcengineering/activity' -import { AccountRole, SortingOrder, type Lookup, type Ref } from '@hcengineering/core' +import { AccountRole, type ClassCollaborators, SortingOrder, type Lookup, type Ref } from '@hcengineering/core' import { type Builder } from '@hcengineering/model' import calendar from '@hcengineering/model-calendar' import chunter from '@hcengineering/model-chunter' @@ -30,7 +30,7 @@ import view, { createAction, showColorsViewOption, actionTemplates as viewTempla import workbench, { createNavigateAction, type Application } from '@hcengineering/model-workbench' import notification from '@hcengineering/notification' import { type IntlString } from '@hcengineering/platform' -import { recruitId, type Applicant, RecruitEvents } from '@hcengineering/recruit' +import { recruitId, type Applicant, RecruitEvents, type Vacancy } from '@hcengineering/recruit' import setting from '@hcengineering/setting' import { type KeyBinding, type ViewOptionModel, type ViewOptionsModel } from '@hcengineering/view' @@ -84,7 +84,8 @@ export function createModel (builder: Builder): void { editor: recruit.component.VacancyList }) - builder.mixin(recruit.class.Vacancy, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: recruit.class.Vacancy, fields: ['createdBy'] }) @@ -96,7 +97,8 @@ export function createModel (builder: Builder): void { inlineEditor: view.component.ArrayEditor }) - builder.mixin(recruit.class.Applicant, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: recruit.class.Applicant, fields: ['createdBy', 'assignee'] }) diff --git a/models/request/src/index.ts b/models/request/src/index.ts index ddaa1a8e1d..ed3526079a 100644 --- a/models/request/src/index.ts +++ b/models/request/src/index.ts @@ -16,7 +16,7 @@ import activity from '@hcengineering/activity' import type { Person } from '@hcengineering/contact' import contact from '@hcengineering/contact' -import { type Timestamp, type Domain, type Ref, type Tx } from '@hcengineering/core' +import { type Timestamp, type Domain, type Ref, type Tx, type ClassCollaborators } from '@hcengineering/core' import { ArrOf, type Builder, @@ -108,7 +108,8 @@ export function createModel (builder: Builder): void { presenter: request.component.RequestView }) - builder.mixin(request.class.Request, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: request.class.Request, fields: ['requested', 'createdBy'] }) diff --git a/models/setting/src/index.ts b/models/setting/src/index.ts index dc6dbb7fef..bc8e5b8537 100644 --- a/models/setting/src/index.ts +++ b/models/setting/src/index.ts @@ -15,7 +15,15 @@ import activity from '@hcengineering/activity' import contact from '@hcengineering/contact' -import { AccountRole, DOMAIN_MODEL, type Blob, type Domain, type Ref, type AccountUuid } from '@hcengineering/core' +import { + AccountRole, + DOMAIN_MODEL, + type Blob, + type Domain, + type Ref, + type AccountUuid, + type ClassCollaborators +} from '@hcengineering/core' import { Mixin, Model, type Builder, UX } from '@hcengineering/model' import core, { TClass, TConfiguration, TDoc } from '@hcengineering/model-core' import view, { createAction } from '@hcengineering/model-view' @@ -146,7 +154,8 @@ export function createModel (builder: Builder): void { setting.ids.SettingsWidget ) - builder.mixin(setting.class.Integration, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: setting.class.Integration, fields: ['modifiedBy'] }) diff --git a/models/time/src/index.ts b/models/time/src/index.ts index 3cf22ac236..83032d37a3 100644 --- a/models/time/src/index.ts +++ b/models/time/src/index.ts @@ -28,7 +28,8 @@ import { type Type, DateRangeMode, IndexKind, - AccountRole + AccountRole, + type ClassCollaborators } from '@hcengineering/core' import lead from '@hcengineering/lead' import { @@ -386,7 +387,8 @@ export function createModel (builder: Builder): void { enabledTypes: [time.ids.ToDoCreated] }) - builder.mixin(time.class.ToDo, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: time.class.ToDo, fields: ['user'] }) diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index e8f1d4f6ab..0563ea0a1f 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -15,7 +15,7 @@ import activity from '@hcengineering/activity' import chunter from '@hcengineering/chunter' -import { AccountRole, type Ref, type Status } from '@hcengineering/core' +import { AccountRole, type ClassCollaborators, type Ref, type Status } from '@hcengineering/core' import { type Builder } from '@hcengineering/model' import core from '@hcengineering/model-core' import { generateClassNotificationTypes } from '@hcengineering/model-notification' @@ -25,7 +25,7 @@ import view from '@hcengineering/model-view' import workbench from '@hcengineering/model-workbench' import notification from '@hcengineering/notification' import setting from '@hcengineering/setting' -import pluginState, { trackerId } from '@hcengineering/tracker' +import pluginState, { type Issue, trackerId } from '@hcengineering/tracker' import type { TaskStatusFactory } from '@hcengineering/task' import { PaletteColorIndexes } from '@hcengineering/ui/src/colors' @@ -502,7 +502,8 @@ export function createModel (builder: Builder): void { defineSortAndGrouping(builder) - builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: tracker.class.Issue, fields: ['createdBy', 'assignee'] }) diff --git a/models/training/src/index.ts b/models/training/src/index.ts index dcd9c2e091..cb539af8e0 100644 --- a/models/training/src/index.ts +++ b/models/training/src/index.ts @@ -31,7 +31,14 @@ import { TrainingSpecialIds } from '@hcengineering/training' -import { AccountRole, type Data, type FindOptions, type Permission, type Ref } from '@hcengineering/core' +import { + AccountRole, + ClassCollaborators, + type Data, + type FindOptions, + type Permission, + type Ref +} from '@hcengineering/core' import { Prop, type Builder } from '@hcengineering/model' import contacts from '@hcengineering/model-contact' @@ -569,8 +576,9 @@ function defineTrainingRequest (builder: Builder): void { titleProvider: training.function.TrainingRequestObjectTitleProvider }) - builder.mixin(training.class.TrainingRequest, core.class.Class, notification.mixin.ClassCollaborators, { - fields: ['trainees'] as Array + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: training.class.TrainingRequest, + fields: ['trainees'] }) builder.createDoc( diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 6fca816f2d..a069bf9dbf 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -411,6 +411,11 @@ export const DOMAIN_TRANSIENT = 'transient' as Domain */ export const DOMAIN_RELATION = 'relation' as Domain +/** + * @public + */ +export const DOMAIN_COLLABORATOR = 'collaborator' as Domain + /** * @public */ @@ -870,3 +875,13 @@ export interface AccountInfo { } export type SocialKey = Pick + +export interface ClassCollaborators extends Doc { + attachedTo: Ref> + fields: (keyof T)[] // PersonId | Ref | PersonId[] | Ref[] + provideSecurity?: boolean // If true, will provide security for collaborators +} + +export interface Collaborator extends AttachedDoc { + collaborator: AccountUuid +} diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 28a6361df3..3cf9fa75ca 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -60,7 +60,9 @@ import type { TypedSpace, UserStatus, Version, - AccountUuid + AccountUuid, + ClassCollaborators, + Collaborator } from './classes' import { type Status, type StatusCategory } from './status' import type { @@ -148,6 +150,7 @@ export default plugin(coreId, { Version: '' as Ref>, PluginConfiguration: '' as Ref>, UserStatus: '' as Ref>, + TypeRelatedDocument: '' as Ref>>, DomainIndexConfiguration: '' as Ref>, @@ -161,7 +164,9 @@ export default plugin(coreId, { FullTextSearchContext: '' as Ref>, Association: '' as Ref>, Relation: '' as Ref>, - Sequence: '' as Ref> + Sequence: '' as Ref>, + ClassCollaborators: '' as Ref>>, + Collaborator: '' as Ref> }, icon: { TypeString: '' as Asset, diff --git a/plugins/chunter-resources/src/components/threads/Threads.svelte b/plugins/chunter-resources/src/components/threads/Threads.svelte index 31417db26a..a6004fc31a 100644 --- a/plugins/chunter-resources/src/components/threads/Threads.svelte +++ b/plugins/chunter-resources/src/components/threads/Threads.svelte @@ -13,21 +13,22 @@ // limitations under the License. --> + + diff --git a/plugins/notification-resources/src/components/activity/CollaboratorsChanged.svelte b/plugins/notification-resources/src/components/activity/CollaboratorsChanged.svelte index a7c438a0e3..5453b751eb 100644 --- a/plugins/notification-resources/src/components/activity/CollaboratorsChanged.svelte +++ b/plugins/notification-resources/src/components/activity/CollaboratorsChanged.svelte @@ -17,74 +17,29 @@ import { Icon, IconAdd, IconDelete, Label } from '@hcengineering/ui' import { employeeRefByAccountUuidStore, PersonRefPresenter } from '@hcengineering/contact-resources' import { Person } from '@hcengineering/contact' - import { type Ref, type AccountUuid, notEmpty } from '@hcengineering/core' - import activity, { DocAttributeUpdates } from '@hcengineering/activity' + import { type Ref, type AccountUuid, notEmpty, Doc, Collaborator } from '@hcengineering/core' + import activity, { DisplayDocUpdateMessage, DocAttributeUpdates } from '@hcengineering/activity' import notification from '@hcengineering/notification' - export let value: DocAttributeUpdates + export let message: DisplayDocUpdateMessage + export let value: Collaborator - $: removed = getPersonRefs(value.removed, $employeeRefByAccountUuidStore) - $: added = getPersonRefs(value.added.length > 0 ? value.added : value.set, $employeeRefByAccountUuidStore) - - function getPersonRefs ( - values: DocAttributeUpdates['removed' | 'added' | 'set'], - personRefByAccountUuid: Map> - ): Ref[] { - const persons = new Set( - values - .map((value) => { - if (typeof value !== 'string') { - return undefined - } - - const person = personRefByAccountUuid.get(value as AccountUuid) - - if (person === undefined) { - return undefined - } - - return person - }) - .filter(notEmpty) - ) - - return Array.from(persons) - } - - $: hasDifferentChanges = added.length > 0 && removed.length > 0 + $: person = $employeeRefByAccountUuidStore.get(value.collaborator) - {#if hasDifferentChanges} - - {#if added.length > 0} + {#if person !== undefined} - {#if hasDifferentChanges} - - {/if} - {#each added as add} - - {/each} - - {/if} - - {#if removed.length > 0} - - {#if hasDifferentChanges} - - {/if} - {#each removed as remove} - - {/each} + {/if} @@ -94,6 +49,7 @@ display: flex; flex-wrap: wrap; align-items: center; + color: var(--global-primary-TextColor); gap: 0.5rem; } diff --git a/plugins/notification-resources/src/inboxNotificationsClient.ts b/plugins/notification-resources/src/inboxNotificationsClient.ts index a56269202a..764223c6b7 100644 --- a/plugins/notification-resources/src/inboxNotificationsClient.ts +++ b/plugins/notification-resources/src/inboxNotificationsClient.ts @@ -13,13 +13,12 @@ // limitations under the License. // import activity from '@hcengineering/activity' -import { - type Account, - type Client, +import core, { SortingOrder, getCurrentAccount, toIdMap, - type Class, + type Account, + type Client, type Doc, type IdMap, type Ref, @@ -28,7 +27,6 @@ import { } from '@hcengineering/core' import notification, { type ActivityInboxNotification, - type Collaborators, type DocNotifyContext, type InboxNotification, type InboxNotificationsClient @@ -173,46 +171,25 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { await op.commit() } - async forceReadDoc (_id: Ref, _class: Ref>): Promise { - const context = this._contextByDoc.get(_id) + async forceReadDoc (doc: Doc): Promise { + const context = this._contextByDoc.get(doc._id) if (context !== undefined) { - await this.readDoc(_id) + await this.readDoc(doc._id) return } const client = getClient() - const doc = await client.findOne(_class, { _id }) - if (doc === undefined) { - return - } + const current = await client.findOne(core.class.Collaborator, { + attachedTo: doc._id, + collaborator: getCurrentAccount().uuid + }) - const hierarchy = client.getHierarchy() - const collaboratorsMixin = hierarchy.as(doc, notification.mixin.Collaborators) - - if (collaboratorsMixin.collaborators === undefined) { - await client.createMixin( - collaboratorsMixin._id, - collaboratorsMixin._class, - collaboratorsMixin.space, - notification.mixin.Collaborators, - { - collaborators: [getCurrentAccount().uuid] - } - ) - } else if (collaboratorsMixin.collaborators.includes(getCurrentAccount().uuid)) { - await client.updateMixin( - collaboratorsMixin._id, - collaboratorsMixin._class, - collaboratorsMixin.space, - notification.mixin.Collaborators, - { - $push: { - collaborators: getCurrentAccount().primarySocialId - } - } - ) + if (current === undefined) { + await client.addCollection(core.class.Collaborator, doc.space, doc._id, doc._class, 'collaborators', { + collaborator: getCurrentAccount().uuid + }) } } diff --git a/plugins/notification-resources/src/index.ts b/plugins/notification-resources/src/index.ts index c356418d42..eb17ebfbe7 100644 --- a/plugins/notification-resources/src/index.ts +++ b/plugins/notification-resources/src/index.ts @@ -26,6 +26,7 @@ import CommonInboxNotificationPresenter from './components/inbox/CommonInboxNoti import MentionInboxNotificationPresenter from './components/inbox/MentionInboxNotificationPresenter.svelte' import NotificationCollaboratorsChanged from './components/NotificationCollaboratorsChanged.svelte' import ReactionNotificationPresenter from './components/ReactionNotificationPresenter.svelte' +import CollaboratorEditor from './components/CollaboratorEditor.svelte' import GeneralPreferencesGroup from './components/settings/GeneralPreferencesGroup.svelte' import { unsubscribe, @@ -69,7 +70,8 @@ export default async (): Promise => ({ MentionInboxNotificationPresenter, NotificationCollaboratorsChanged, ReactionNotificationPresenter, - GeneralPreferencesGroup + GeneralPreferencesGroup, + CollaboratorEditor }, function: { // eslint-disable-next-line @typescript-eslint/unbound-method diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts index bee80416c2..042ced64fa 100644 --- a/plugins/notification-resources/src/utils.ts +++ b/plugins/notification-resources/src/utils.ts @@ -30,7 +30,6 @@ import chunter, { type ThreadMessage } from '@hcengineering/chunter' import core, { type Class, type Doc, - type DocumentUpdate, getCurrentAccount, type Ref, SortingOrder, @@ -39,9 +38,9 @@ import core, { } from '@hcengineering/core' import notification, { type ActivityInboxNotification, - type Collaborators, type DisplayInboxNotification, type DocNotifyContext, + getClassCollaborators, type InboxNotification, type MentionInboxNotification, notificationId, @@ -64,8 +63,8 @@ import { } from '@hcengineering/ui' import view, { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view' import { getObjectLinkId, parseLinkId } from '@hcengineering/view-resources' -import { get, writable } from 'svelte/store' import type { LocationData } from '@hcengineering/workbench' +import { get, writable } from 'svelte/store' import { InboxNotificationsClientImpl } from './inboxNotificationsClient' import { type InboxData, type InboxNotificationsFilter } from './types' @@ -235,36 +234,22 @@ export async function subscribeDoc ( ): Promise { const myAcc = getCurrentAccount() const hierarchy = client.getHierarchy() - - if (hierarchy.classHierarchyMixin(docClass, notification.mixin.ClassCollaborators) === undefined) return + const classCollaborators = getClassCollaborators(client.getModel(), hierarchy, docClass) + if (classCollaborators === undefined) return const target = doc ?? (await client.findOne(docClass, { _id: docId })) if (target === undefined) return - if (hierarchy.hasMixin(target, notification.mixin.Collaborators)) { - const collab = hierarchy.as(target, notification.mixin.Collaborators) - let collabUpdate: DocumentUpdate | undefined - const includesMe = collab.collaborators.includes(myAcc.uuid) - - if (includesMe && op === 'remove') { - collabUpdate = { - $pull: { - collaborators: myAcc.uuid - } - } - } else if (!includesMe && op === 'add') { - collabUpdate = { - $push: { - collaborators: myAcc.uuid - } - } - } - - if (collabUpdate !== undefined) { - await client.updateMixin(collab._id, collab._class, collab.space, notification.mixin.Collaborators, collabUpdate) - } - } else if (op === 'add') { - await client.createMixin(docId, docClass, target.space, notification.mixin.Collaborators, { - collaborators: [myAcc.uuid] + const current = await client.findOne(core.class.Collaborator, { + attachedTo: docId, + collaborator: myAcc.uuid + }) + if (op === 'remove') { + if (current === undefined) return // already removed + await client.remove(current) + } else { + if (current !== undefined) return // already added + await client.addCollection(core.class.Collaborator, target.space, target._id, target._class, 'collaborators', { + collaborator: myAcc.uuid }) } } diff --git a/plugins/notification/src/index.ts b/plugins/notification/src/index.ts index 454025be79..591ec7dd66 100644 --- a/plugins/notification/src/index.ts +++ b/plugins/notification/src/index.ts @@ -30,7 +30,9 @@ import { Tx, TxCUD, TxOperations, - AccountUuid + AccountUuid, + Collaborator, + CollectionSize } from '@hcengineering/core' import type { Asset, IntlString, Metadata, Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' @@ -43,6 +45,7 @@ import { PersonSpace } from '@hcengineering/contact' import { Readable, Writable } from './types' export * from './types' +export * from './utils' export const DOMAIN_NOTIFICATION = 'notification' as Domain export const DOMAIN_DOC_NOTIFY = 'notification-dnc' as Domain @@ -185,13 +188,6 @@ export interface NotificationTypeSetting extends Preference { enabled: boolean } -/** - * @public - */ -export interface ClassCollaborators extends Class { - fields: string[] // PersonId | Ref | PersonId[] | Ref[] -} - /** * @public */ @@ -203,6 +199,13 @@ export interface NotificationObjectPresenter extends Class { * @public */ export interface Collaborators extends Doc { + collaborators: CollectionSize +} + +/** + * @public + */ +export interface OldCollaborators extends Doc { collaborators: AccountUuid[] } @@ -307,7 +310,7 @@ export interface InboxNotificationsClient { inboxNotificationsByContext: Readable, InboxNotification[]>> readDoc: (_id: Ref) => Promise - forceReadDoc: (_id: Ref, _class: Ref>) => Promise + forceReadDoc: (doc: Doc) => Promise readNotifications: (client: TxOperations, ids: Array>) => Promise unreadNotifications: (client: TxOperations, ids: Array>) => Promise archiveNotifications: (client: TxOperations, ids: Array>) => Promise @@ -339,7 +342,6 @@ export type NotifyFunc = (title: string, body: string, _id?: string, onClick?: ( */ const notification = plugin(notificationId, { mixin: { - ClassCollaborators: '' as Ref>, Collaborators: '' as Ref>, NotificationObjectPresenter: '' as Ref>, NotificationPreview: '' as Ref>, @@ -386,7 +388,8 @@ const notification = plugin(notificationId, { DocNotifyContextPresenter: '' as AnyComponent, NotificationCollaboratorsChanged: '' as AnyComponent, ReactionNotificationPresenter: '' as AnyComponent, - GeneralPreferencesGroup: '' as AnyComponent + GeneralPreferencesGroup: '' as AnyComponent, + CollaboratorEditor: '' as AnyComponent }, action: { PinDocNotifyContext: '' as Ref, @@ -435,7 +438,8 @@ const notification = plugin(notificationId, { SoundNotificationsDescription: '' as IntlString, Sound: '' as IntlString, NoAccessToObject: '' as IntlString, - ViewIn: '' as IntlString + ViewIn: '' as IntlString, + Collaborators: '' as IntlString }, function: { Notify: '' as Resource, diff --git a/plugins/notification/src/utils.ts b/plugins/notification/src/utils.ts new file mode 100644 index 0000000000..81895f4b6e --- /dev/null +++ b/plugins/notification/src/utils.ts @@ -0,0 +1,37 @@ +// +// 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 core, { Class, ClassCollaborators, Doc, Hierarchy, ModelDb, Ref } from '@hcengineering/core' + +export function getClassCollaborators ( + model: ModelDb, + hiearachy: Hierarchy, + _id: Ref> +): ClassCollaborators | undefined { + const ancestors = hiearachy.getAncestors(_id) + const collabs = new Map( + model + .findAllSync(core.class.ClassCollaborators, { + attachedTo: { $in: ancestors } + }) + .map((c) => [c.attachedTo, c]) + ) + for (const ancestor of ancestors) { + const res = collabs.get(ancestor) + if (res !== undefined) { + return res + } + } +} diff --git a/plugins/recruit-resources/src/components/EditVacancy.svelte b/plugins/recruit-resources/src/components/EditVacancy.svelte index bcc301ee4a..ca2d51a537 100644 --- a/plugins/recruit-resources/src/components/EditVacancy.svelte +++ b/plugins/recruit-resources/src/components/EditVacancy.svelte @@ -15,15 +15,7 @@ --> -{#each mixins as mixin} +{#each _mixins as mixin} {@const to = !hierarchy.hasMixin(mixin, setting.mixin.UserMixin) ? object._class : mixin.extends} {#if !hierarchy.hasMixin(mixin, setting.mixin.Editable) || hierarchy.as(mixin, setting.mixin.Editable).value} {#key mixin._id} @@ -51,7 +57,7 @@ {ignoreKeys} {to} {readonly} - {allowedCollections} + allowedCollections={_allowedCollections} {showHeader} on:update /> diff --git a/plugins/view-resources/src/components/EditDoc.svelte b/plugins/view-resources/src/components/EditDoc.svelte index 18c6cd7a82..8451c7f951 100644 --- a/plugins/view-resources/src/components/EditDoc.svelte +++ b/plugins/view-resources/src/components/EditDoc.svelte @@ -114,7 +114,7 @@ let ignoreKeys: string[] = [] let activityOptions = { enabled: true, showInput: true } - let allowedCollections: string[] = [] + let allowedCollections: string[] = ['collaborators'] let collectionArrays: string[] = [] let inplaceAttributes: string[] = [] let ignoreMixins: Set>> = new Set>>() @@ -218,7 +218,7 @@ ignoreKeys = ev.detail.ignoreKeys activityOptions = ev.detail.activityOptions ?? activityOptions ignoreMixins = new Set(ev.detail.ignoreMixins) - allowedCollections = ev.detail.allowedCollections ?? [] + allowedCollections = [...(ev.detail.allowedCollections ?? []), 'collaborators'] collectionArrays = ev.detail.collectionArrays ?? [] title = ev.detail.title mixins = getDocMixins(object, showAllMixins, ignoreMixins, realObjectClass) diff --git a/server-plugins/activity-resources/src/index.ts b/server-plugins/activity-resources/src/index.ts index cb7d656fb9..249581af54 100644 --- a/server-plugins/activity-resources/src/index.ts +++ b/server-plugins/activity-resources/src/index.ts @@ -379,7 +379,7 @@ async function ActivityMessagesHandler (_txes: TxCUD[], control: TriggerCon const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx as TxCreateDoc)) const notificationTxes = await control.ctx.with('createCollaboratorNotifications', {}, (ctx) => - createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map, Doc>) + createCollaboratorNotifications(ctx, tx, control, messages) ) result.push(...txes, ...notificationTxes) diff --git a/server-plugins/activity-resources/src/references.ts b/server-plugins/activity-resources/src/references.ts index 143d7fa4b1..627fca396a 100644 --- a/server-plugins/activity-resources/src/references.ts +++ b/server-plugins/activity-resources/src/references.ts @@ -16,39 +16,42 @@ import activity, { type ActivityMessage, type ActivityReference, type UserMentionInfo } from '@hcengineering/activity' import contact, { type Employee, type Person } from '@hcengineering/contact' import core, { - type PersonId, + type AccountUuid, type Blob, type Class, type Data, type Doc, type Hierarchy, type Markup, + type MeasureContext, + type ModelDb, + type PersonId, type Ref, type Space, type Tx, type TxCreateDoc, type TxCUD, TxFactory, - type TxMixin, TxProcessor, type TxRemoveDoc, type TxUpdateDoc, - type Type, - type MeasureContext, - type AccountUuid + type Type } from '@hcengineering/core' -import notification, { type MentionInboxNotification, type NotificationType } from '@hcengineering/notification' +import notification, { + getClassCollaborators, + type MentionInboxNotification, + type NotificationType +} from '@hcengineering/notification' import { getPerson } from '@hcengineering/server-contact' import { type StorageAdapter, type TriggerControl } from '@hcengineering/server-core' import { + getAllowedProviders, getCommonNotificationTxes, getNotificationProviderControl, - getPushCollaboratorTx, - getAllowedProviders, + getReceiversInfo, type NotificationProviderControl } from '@hcengineering/server-notification-resources' import { areEqualJson, extractReferences, jsonToMarkup, markupToJSON } from '@hcengineering/text-core' -import { getReceiversInfo } from '@hcengineering/server-notification-resources' export function isDocMentioned (doc: Ref, content: string): boolean { const references = [] @@ -107,7 +110,7 @@ export async function getPersonNotificationTxes ( let collaborators: AccountUuid[] = [] if ([contact.mention.Everyone, contact.mention.Here].includes(reference.attachedTo as Ref)) { - collaborators = await getMultipleMentionCollaborators(reference, control, doc) + collaborators = await getMultipleMentionCollaborators(ctx, reference, control, doc) } else { const employee = ( await control.findAll(ctx, contact.mixin.Employee, { _id: reference.attachedTo as Ref }) @@ -196,16 +199,22 @@ function getUpdateMentionInfoTx ( } async function getMultipleMentionCollaborators ( + ctx: MeasureContext, reference: Data, control: TriggerControl, doc: Doc ): Promise { - const { hierarchy } = control + const { hierarchy, modelDb } = control const personRef = reference.attachedTo as Ref - const mixin = hierarchy.classHierarchyMixin(doc._class, notification.mixin.ClassCollaborators) - if (mixin === undefined) return [] - const collaborators = hierarchy.as(doc, notification.mixin.Collaborators).collaborators + const classCollaborators = getClassCollaborators(modelDb, hierarchy, doc._class) + + if (classCollaborators === undefined) return [] + const collaborators = ( + await control.findAll(ctx, core.class.Collaborator, { + attachedTo: doc._id + }) + ).map((it) => it.collaborator) if (collaborators.length === 0) return [] const statuses = Array.from(control.userStatusMap.values()) @@ -229,16 +238,18 @@ function checkSpace (account: AccountUuid, space: Space, control: TriggerControl return true } -function getCollaboratorsTxes (control: TriggerControl, receiver: AccountUuid, object?: Doc): TxMixin[] { - const res: TxMixin[] = [] +function getCollaboratorsTxes (control: TriggerControl, receiver: AccountUuid, object?: Doc): Tx[] { + const res: Tx[] = [] if (object !== undefined) { - // Add user to collaborators of object where user is mentioned - const objectTx = getPushCollaboratorTx(control, receiver, object) + const tx = control.txFactory.createTxCreateDoc(core.class.Collaborator, object.space, { + attachedTo: object._id, + collaborator: receiver, + attachedToClass: object._class, + collection: 'collaborators' + }) - if (objectTx !== undefined) { - res.push(objectTx) - } + res.push(tx) } return res @@ -553,6 +564,7 @@ async function getRemoveActivityReferenceTxes ( } function guessReferenceObj ( + modelDb: ModelDb, hierarchy: Hierarchy, tx: TxCUD ): { @@ -568,7 +580,7 @@ function guessReferenceObj ( } } - const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) + const mixin = getClassCollaborators(modelDb, hierarchy, tx.objectClass) return mixin !== undefined ? { objectId: tx.objectId, @@ -595,7 +607,7 @@ async function ActivityReferenceCreate (tx: TxCUD, control: TriggerControl) const txFactory = new TxFactory(control.txFactory.account) const doc = TxProcessor.createDoc2Doc(ctx) - const target = guessReferenceObj(control.hierarchy, tx) + const target = guessReferenceObj(control.modelDb, control.hierarchy, tx) const txes: Tx[] = await getCreateReferencesTxes( control.ctx, @@ -643,7 +655,7 @@ async function ActivityReferenceUpdate (tx: TxCUD, control: TriggerControl) const txFactory = new TxFactory(control.txFactory.account) const doc = TxProcessor.updateDoc2Doc(rawDoc, ctx) - const target = guessReferenceObj(control.hierarchy, tx) + const target = guessReferenceObj(control.modelDb, control.hierarchy, tx) const txes: Tx[] = await getUpdateReferencesTxes( control.ctx, diff --git a/server-plugins/activity-resources/src/utils.ts b/server-plugins/activity-resources/src/utils.ts index d5fad00f49..dddce73272 100644 --- a/server-plugins/activity-resources/src/utils.ts +++ b/server-plugins/activity-resources/src/utils.ts @@ -2,10 +2,12 @@ import { type ActivityMessageControl, type DocAttributeUpdates, type DocUpdateAc import cardPlugin, { type Card, type Tag } from '@hcengineering/card' import { type ActivityUpdate, ActivityUpdateType } from '@hcengineering/communication-types' import core, { + type ArrOf, type AttachedDoc, type Attribute, type Class, type Collection, + combineAttributes, type Doc, type Hierarchy, type MeasureContext, @@ -16,16 +18,11 @@ import core, { type TxCUD, type TxMixin, TxProcessor, - type TxUpdateDoc, - combineAttributes, - type ArrOf, - type AccountUuid + type TxUpdateDoc } from '@hcengineering/core' -import notification from '@hcengineering/notification' import { translate } from '@hcengineering/platform' import { type ActivityControl, type DocObjectCache, getAllObjectTransactions } from '@hcengineering/server-activity' import { type TriggerControl } from '@hcengineering/server-core' -import { getDocCollaborators } from '@hcengineering/server-notification-resources' function getAvailableAttributesKeys (tx: TxCUD, hierarchy: Hierarchy): string[] { if (hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { @@ -148,41 +145,11 @@ interface AttributeDiff { removed: DocAttributeUpdates['removed'] } -async function getCollaboratorsDiff ( - control: ActivityControl, - doc: Doc, - prevDoc: Doc | undefined -): Promise { - const { hierarchy } = control - const value = hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [] - - let prevValue: AccountUuid[] = [] - - if (prevDoc !== undefined && hierarchy.hasMixin(prevDoc, notification.mixin.Collaborators)) { - prevValue = hierarchy.as(prevDoc, notification.mixin.Collaborators).collaborators ?? [] - } else if (prevDoc !== undefined) { - const mixin = hierarchy.classHierarchyMixin(prevDoc._class, notification.mixin.ClassCollaborators) - prevValue = - mixin !== undefined - ? await getDocCollaborators((control as TriggerControl).ctx, prevDoc, mixin, control as TriggerControl) - : [] - } - - const added = value.filter((item) => !prevValue.includes(item)) as DocAttributeUpdates['added'] - const removed = prevValue.filter((item) => !value.includes(item)) as DocAttributeUpdates['removed'] - - return { - added, - removed - } -} - export async function getAttributeDiff ( control: ActivityControl, doc: Doc, prevDoc: Doc | undefined, attrKey: string, - attrClass: Ref>, mixin?: Ref> ): Promise { const { hierarchy } = control @@ -190,10 +157,6 @@ export async function getAttributeDiff ( let actualDoc: Doc | undefined = doc let actualPrevDoc: Doc | undefined = prevDoc - if (mixin != null && hierarchy.isDerived(attrClass, notification.mixin.Collaborators)) { - return await getCollaboratorsDiff(control, doc, prevDoc) - } - if (mixin != null) { actualDoc = hierarchy.as(doc, mixin) actualPrevDoc = prevDoc === undefined ? undefined : hierarchy.as(prevDoc, mixin) @@ -293,14 +256,14 @@ export async function getTxAttributesUpdates ( continue } - if (hierarchy.isDerived(attrClass, core.class.TypeMarkup) || mixin === notification.mixin.Collaborators) { + if (hierarchy.isDerived(attrClass, core.class.TypeMarkup)) { if (docDiff === undefined) { docDiff = await getDocDiff(control, updateObject._class, updateObject._id, tx._id, mixin, objectCache) } } if (Array.isArray(attrValue) && docDiff?.doc !== undefined) { - const diff = await getAttributeDiff(control, docDiff.doc, docDiff.prevDoc, key, attrClass, mixin) + const diff = await getAttributeDiff(control, docDiff.doc, docDiff.prevDoc, key, mixin) added.push(...diff.added) removed.push(...diff.removed) attrValue = [] @@ -483,7 +446,7 @@ export async function getNewActivityUpdates ( } if (Array.isArray(attrValue)) { - const diff = await getAttributeDiff(control, card, undefined, key, attrClass, mixin) + const diff = await getAttributeDiff(control, card, undefined, key, mixin) added.push(...diff.added) removed.push(...diff.removed) } diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index fff606e21e..14478bd2de 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -16,16 +16,18 @@ import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity' import chunter, { Channel, ChatMessage, chunterId, ChunterSpace, ThreadMessage } from '@hcengineering/chunter' import contact, { Employee, Person } from '@hcengineering/contact' -import { getAccountBySocialId, getPerson } from '@hcengineering/server-contact' import core, { - PersonId, + AccountUuid, Class, + combineAttributes, concatLink, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, + notEmpty, + PersonId, Ref, Timestamp, Tx, @@ -34,21 +36,19 @@ import core, { TxProcessor, TxUpdateDoc, UserStatus, - type MeasureContext, - combineAttributes, - AccountUuid, - notEmpty + type MeasureContext } from '@hcengineering/core' -import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification' +import notification, { DocNotifyContext, getClassCollaborators, NotificationContent } from '@hcengineering/notification' import { getMetadata, IntlString, translate } from '@hcengineering/platform' +import { getAccountBySocialId, getPerson } from '@hcengineering/server-contact' import serverCore, { TriggerControl } from '@hcengineering/server-core' import { createCollaboratorNotifications, - getDocCollaborators, - getMixinTx + getAddCollaboratTxes, + getDocCollaborators } from '@hcengineering/server-notification-resources' -import { extractReferences, markupToText, stripTags } from '@hcengineering/text-core' import { jsonToHTML, markupToJSON } from '@hcengineering/text' +import { extractReferences, markupToText, stripTags } from '@hcengineering/text-core' import { workbenchId } from '@hcengineering/workbench' import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification' @@ -169,7 +169,7 @@ async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD, contro const message = TxProcessor.createDoc2Doc(actualTx) if (message.modifiedBy === core.account.System) return [] - const mixin = hierarchy.classHierarchyMixin(message.attachedToClass, notification.mixin.ClassCollaborators) + const mixin = getClassCollaborators(control.modelDb, hierarchy, message.attachedToClass) if (mixin === undefined) { return [] @@ -192,34 +192,34 @@ async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD, contro ? await control.findAll(ctx, contact.mixin.Employee, { _id: { $in: mentionedPersons as Ref[] } }) : [] const collaboratorsFromMessage = [...employees.map((it) => it.personUuid), account].filter(notEmpty) + let currentCollaborators = ( + await control.findAll(ctx, core.class.Collaborator, { + attachedTo: targetDoc._id + }) + ).map((it) => it.collaborator) - if (hierarchy.hasMixin(targetDoc, notification.mixin.Collaborators)) { - const collaboratorsMixin = hierarchy.as(targetDoc, notification.mixin.Collaborators) - const newCollabs = collaboratorsFromMessage.filter((it) => !collaboratorsMixin.collaborators.includes(it)) - if (newCollabs.length > 0) { - res.push( - control.txFactory.createTxMixin( - targetDoc._id, - targetDoc._class, - targetDoc.space, - notification.mixin.Collaborators, - { - $push: { collaborators: { $each: newCollabs, $position: 0 } } - } - ) - ) + if (currentCollaborators.length === 0) { + const mixin = getClassCollaborators(control.modelDb, control.hierarchy, targetDoc._class) + if (mixin !== undefined) { + const collaborators = await getDocCollaborators(ctx, targetDoc, mixin, control) + currentCollaborators = collaborators + res.push(...getAddCollaboratTxes(tx.objectId, tx.objectClass, tx.objectSpace, control, collaborators)) } - } else { - const collaborators = await getDocCollaborators(ctx, targetDoc, mixin, control) - res.push(getMixinTx(tx, control, Array.from(new Set(collaborators.concat(collaboratorsFromMessage))))) } - if (collaboratorsFromMessage.length > 0) { - control.txFactory.createTxMixin(message._id, message._class, message.space, notification.mixin.Collaborators, { - $push: { - $push: { collaborators: { $each: collaboratorsFromMessage, $position: 0 } } - } + for (const collab of collaboratorsFromMessage) { + if (currentCollaborators.includes(collab)) { + continue + } + + const tx = control.txFactory.createTxCreateDoc(core.class.Collaborator, targetDoc.space, { + attachedTo: targetDoc._id, + attachedToClass: targetDoc._class, + collaborator: collab, + collection: 'collaborators' }) + + res.push(tx) } if (account != null && isChannel && !(targetDoc as Channel).members.includes(account)) { diff --git a/server-plugins/contact-resources/src/index.ts b/server-plugins/contact-resources/src/index.ts index 812009f25e..22b3f2cfbf 100644 --- a/server-plugins/contact-resources/src/index.ts +++ b/server-plugins/contact-resources/src/index.ts @@ -50,7 +50,6 @@ import core, { TxUpdateDoc, TypedSpace } from '@hcengineering/core' -import notification, { Collaborators } from '@hcengineering/notification' import { getMetadata } from '@hcengineering/platform' import { makeRank } from '@hcengineering/rank' import { getAccountBySocialId, getCurrentPerson } from '@hcengineering/server-contact' @@ -253,29 +252,13 @@ export async function OnChannelUpdate (txes: Tx[], control: TriggerControl): Pro 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) { - if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { - const collab = control.hierarchy.as(doc, notification.mixin.Collaborators) as Doc as Collaborators - if (collab.collaborators.includes(account)) { - result.push( - control.txFactory.createTxMixin(doc._id, doc._class, doc.space, notification.mixin.Collaborators, { - $push: { - collaborators: account - } - }) - ) - } - } else { - const res = control.txFactory.createTxMixin( - doc._id, - doc._class, - doc.space, - notification.mixin.Collaborators, - { - collaborators: [account] - } - ) - result.push(res) - } + const tx = control.txFactory.createTxCreateDoc(core.class.Collaborator, doc.space, { + attachedTo: doc._id, + attachedToClass: doc._class, + collection: 'collaborators', + collaborator: account + }) + result.push(tx) } } } diff --git a/server-plugins/love-resources/src/index.ts b/server-plugins/love-resources/src/index.ts index 244e21d7f7..ebb4832d9e 100644 --- a/server-plugins/love-resources/src/index.ts +++ b/server-plugins/love-resources/src/index.ts @@ -13,10 +13,13 @@ // limitations under the License. // -import contact, { Employee, Person, formatName, getName } from '@hcengineering/contact' +import contact, { Employee, formatName, getName, Person } from '@hcengineering/contact' import core, { + type AccountUuid, + combineAttributes, concatLink, Doc, + generateId, Ref, Timestamp, Tx, @@ -25,10 +28,7 @@ import core, { TxMixin, TxProcessor, TxUpdateDoc, - UserStatus, - combineAttributes, - type PersonUuid, - type AccountUuid + UserStatus } from '@hcengineering/core' import love, { Invite, @@ -46,15 +46,15 @@ import love, { } from '@hcengineering/love' import notification from '@hcengineering/notification' import { getMetadata, translate } from '@hcengineering/platform' -import serverCore, { TriggerControl } from '@hcengineering/server-core' import { getSocialStrings } from '@hcengineering/server-contact' +import serverCore, { TriggerControl } from '@hcengineering/server-core' import { createPushNotification, getNotificationProviderControl, isAllowed } from '@hcengineering/server-notification-resources' -import { workbenchId } from '@hcengineering/workbench' import view from '@hcengineering/view' +import { workbenchId } from '@hcengineering/workbench' export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise { const result: Tx[] = [] @@ -102,7 +102,7 @@ export async function OnEmployee (txes: Tx[], control: TriggerControl): Promise< return result } -async function createUserInfo (user: PersonUuid, control: TriggerControl): Promise { +async function createUserInfo (user: AccountUuid, control: TriggerControl): Promise { const person = (await control.findAll(control.ctx, contact.class.Person, { personUuid: user }))[0] if (person === undefined) return [] @@ -117,6 +117,7 @@ async function createUserInfo (user: PersonUuid, control: TriggerControl): Promi room: room?._id ?? love.ids.Reception, x: 0, y: 0, + account: user, sessionId: null }) const ptx = control.txFactory.createTxApplyIf( @@ -180,25 +181,95 @@ export async function OnUserStatus (txes: Tx[], control: TriggerControl): Promis } async function roomJoinHandler (info: ParticipantInfo, control: TriggerControl): Promise { + const res: Tx[] = [] const roomInfos = await control.queryFind(control.ctx, love.class.RoomInfo, {}) const roomInfo = roomInfos.find((ri) => ri.room === info.room) if (roomInfo !== undefined && !roomInfo.persons.includes(info.person)) { - return [ + res.push( control.txFactory.createTxUpdateDoc(love.class.RoomInfo, core.space.Workspace, roomInfo._id, { $push: { persons: info.person } }) - ] + ) } else { const room = (await control.findAll(control.ctx, love.class.Room, { _id: info.room }))[0] if (room === undefined) return [] - return [ + res.push( control.txFactory.createTxCreateDoc(love.class.RoomInfo, core.space.Workspace, { persons: [info.person], room: info.room, isOffice: isOffice(room) }) - ] + ) } + if (info.account != null) { + const meetingMinutes = ( + await control.findAll(control.ctx, love.class.MeetingMinutes, { + attachedTo: info.room, + status: MeetingStatus.Active + }) + )[0] + if (meetingMinutes !== undefined) { + const colab = await control.findAll(control.ctx, core.class.Collaborator, { + attachedTo: meetingMinutes._id, + collaborator: info.account + }) + if (colab.length === 0) { + res.push( + control.txFactory.createTxCreateDoc(core.class.Collaborator, core.space.Workspace, { + attachedTo: meetingMinutes._id, + attachedToClass: meetingMinutes._class, + collection: 'collaborators', + collaborator: info.account + }) + ) + } + } else { + const _id = generateId() + const date = new Date() + .toLocaleDateString('en-GB', { + day: 'numeric', + month: 'long', + year: 'numeric' + }) + .replace(',', ' at') + res.push( + control.txFactory.createTxCreateDoc( + love.class.MeetingMinutes, + core.space.Workspace, + { + description: null, + attachedTo: info.room, + status: MeetingStatus.Active, + title: `${await getRoomName(control, info.room)} ${date}`, + attachedToClass: love.class.Room, + collection: 'meetings' + }, + _id + ) + ) + res.push( + control.txFactory.createTxCreateDoc(core.class.Collaborator, core.space.Workspace, { + attachedTo: _id, + attachedToClass: love.class.MeetingMinutes, + collection: 'collaborators', + collaborator: info.account + }) + ) + } + } + return res +} + +async function getRoomName (control: TriggerControl, roomId: Ref): Promise { + const room = (await control.findAll(control.ctx, love.class.Room, { _id: roomId }))[0] + if (room === undefined) return '' + if (isOffice(room) && room.person !== null && room.name === '') { + const employee = (await control.findAll(control.ctx, contact.class.Person, { _id: room.person }))[0] + if (employee != null) { + return getName(control.hierarchy, employee) + } + } + return room.name } async function rejectJoinRequests (info: ParticipantInfo, control: TriggerControl): Promise { diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index 21d4ff494b..4115ae70eb 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -15,6 +15,7 @@ // import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' +import { Analytics } from '@hcengineering/analytics' import chunter, { ChatMessage } from '@hcengineering/chunter' import contact, { Employee, type Person } from '@hcengineering/contact' import core, { @@ -23,6 +24,8 @@ import core, { ArrOf, AttachedDoc, Class, + ClassCollaborators, + Collaborator, Collection, combineAttributes, Data, @@ -47,21 +50,20 @@ import core, { } from '@hcengineering/core' import notification, { ActivityInboxNotification, - ClassCollaborators, - Collaborators, CommonInboxNotification, DocNotifyContext, + getClassCollaborators, InboxNotification, MentionInboxNotification, NotificationType } from '@hcengineering/notification' import { getResource, translate } from '@hcengineering/platform' +import { getAccountBySocialId, getEmployeesBySocialIds } from '@hcengineering/server-contact' import { type TriggerControl } from '@hcengineering/server-core' import { NOTIFICATION_BODY_SIZE, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification' import { markupToText, stripTags } from '@hcengineering/text-core' -import { Analytics } from '@hcengineering/analytics' -import { getAccountBySocialId, getEmployeesBySocialIds } from '@hcengineering/server-contact' +import { PushNotificationsHandler } from './push' import { AvailableProvidersCache, AvailableProvidersCacheKey, @@ -72,8 +74,6 @@ import { NotifyResult } from './types' import { - createPullCollaboratorsTx, - createPushCollaboratorsTx, getHTMLPresenter, getNotificationContent, getNotificationLink, @@ -82,7 +82,6 @@ import { getReceiversInfo, getSenderInfo, getTextPresenter, - isAllowed, isMixinTx, isShouldNotifyTx, isUserEmployeeInFieldValueTypeMatch, @@ -92,25 +91,6 @@ import { replaceAll, updateNotifyContextsSpace } from './utils' -import { PushNotificationsHandler } from './push' - -export function getPushCollaboratorTx ( - control: TriggerControl, - account: AccountUuid, - doc: Doc -): TxMixin | undefined { - const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators) - - if (mixin.collaborators === undefined || !mixin.collaborators.includes(account)) { - return control.txFactory.createTxMixin(doc._id, doc._class, doc.space, notification.mixin.Collaborators, { - $push: { - collaborators: account - } - }) - } - - return undefined -} export async function getCommonNotificationTxes ( ctx: MeasureContext, @@ -322,7 +302,7 @@ async function getKeyCollaborators ( export async function getDocCollaborators ( ctx: MeasureContext, doc: Doc, - mixin: ClassCollaborators, + mixin: ClassCollaborators, control: TriggerControl ): Promise { const collaborators = new Set() @@ -720,7 +700,7 @@ export async function createCollabDocInfo ( activityMessages: ActivityMessage[], params: NotifyParams, unsubscribe: AccountUuid[] = [], - cache = new Map, Doc>() + cache: Map, Doc> = new Map, Doc>() ): Promise { let res: Tx[] = [] @@ -823,26 +803,31 @@ export async function createCollabDocInfo ( /** * @public */ -export function getMixinTx ( - actualTx: TxCUD, +export function getAddCollaboratTxes ( + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, control: TriggerControl, collaborators: AccountUuid[] -): TxMixin { - return control.txFactory.createTxMixin( - actualTx.objectId, - actualTx.objectClass, - actualTx.objectSpace, - notification.mixin.Collaborators, - { - collaborators - } - ) +): TxCreateDoc[] { + const res: TxCreateDoc[] = [] + for (const collaborator of collaborators) { + const tx = control.txFactory.createTxCreateDoc(core.class.Collaborator, objectSpace, { + attachedTo: objectId, + attachedToClass: objectClass, + collaborator, + collection: 'collaborators' + }) + res.push(tx) + } + return res } async function getTxCollabs ( ctx: MeasureContext, tx: TxCUD, control: TriggerControl, + cache: Map, Collaborator[]>, doc: Doc ): Promise<{ added: AccountUuid[] @@ -850,10 +835,7 @@ async function getTxCollabs ( result: AccountUuid[] }> { const { hierarchy } = control - const mixin = hierarchy.classHierarchyMixin( - doc._class, - notification.mixin.ClassCollaborators - ) + const mixin = getClassCollaborators(control.modelDb, hierarchy, doc._class) if (mixin === undefined) return { added: [], removed: [], result: [] } if (tx._class === core.class.TxCreateDoc) { @@ -862,17 +844,20 @@ async function getTxCollabs ( } if (tx._class === core.class.TxRemoveDoc) { - if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { - return { added: [], removed: [], result: hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [] } - } - return { added: [], removed: [], result: [] } } if ([core.class.TxUpdateDoc, core.class.TxMixin].includes(tx._class)) { - const collabs = new Set(hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? []) + const collaborators = + cache.get(doc._id) ?? + (await control.findAll(ctx, core.class.Collaborator, { + attachedTo: doc._id + })) + const collabs = collaborators.map((c) => c.collaborator) const ops = isMixinTx(tx) ? tx.attributes : (tx as TxUpdateDoc).operations - const newCollaborators = (await getNewCollaborators(ops, mixin, doc._class, control)).filter((p) => !collabs.has(p)) + const newCollaborators = (await getNewCollaborators(ops, mixin, doc._class, control)).filter( + (p) => !collabs.includes(p) + ) const isSpace = control.hierarchy.isDerived(doc._class, core.class.Space) const removedCollabs = isSpace ? await getRemovedMembers(ops, mixin, (doc as Space)._class, control) : [] const result = [...collabs, ...newCollaborators].filter((p) => !removedCollabs.includes(p)) @@ -889,37 +874,39 @@ async function getSpaceCollabTxes ( doc: Doc, tx: TxCUD, activityMessages: ActivityMessage[], - cache: Map, Doc> + cache: Map, Collaborator[]>, + docCache: Map, Doc> ): Promise { if (doc.space === core.space.Space) { return [] } - const space = await getObjectSpace(control, doc, cache) + const space = await getObjectSpace(control, doc, docCache) if (space === undefined) return [] - cache.set(space._id, space) + docCache.set(space._id, space) - const mixin = control.hierarchy.classHierarchyMixin( - space._class, - notification.mixin.ClassCollaborators - ) + const mixin = getClassCollaborators(control.modelDb, control.hierarchy, space._class) if (mixin !== undefined) { - const collabs = control.hierarchy.as(space, notification.mixin.Collaborators) - if (collabs.collaborators !== undefined) { - return await createCollabDocInfo( - ctx, - [], - collabs.collaborators, - control, - tx, - doc, - activityMessages, - { isSpace: true, isOwn: false, shouldUpdateTimestamp: true }, - [], - cache - ) - } + const collaborators = + cache.get(space._id) ?? + (await control.findAll(ctx, core.class.Collaborator, { + attachedTo: space._id + })) + cache.set(space._id, collaborators) + const collabs = collaborators.map((c) => c.collaborator) + return await createCollabDocInfo( + ctx, + [], + collabs, + control, + tx, + doc, + activityMessages, + { isSpace: true, isOwn: false, shouldUpdateTimestamp: true }, + [], + docCache + ) } return [] } @@ -953,29 +940,29 @@ async function createCollaboratorDoc ( tx: TxCreateDoc, control: TriggerControl, activityMessage: ActivityMessage[], - cache: Map, Doc> + cache: Map, Collaborator[]>, + docCache: Map, Doc> ): Promise { const res: Tx[] = [] const hierarchy = control.hierarchy - const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) - + const mixin = getClassCollaborators(control.modelDb, hierarchy, tx.objectClass) if (mixin === undefined) { return res } const doc = TxProcessor.createDoc2Doc(tx) const collaborators = await ctx.with('get-collaborators', {}, (ctx) => getDocCollaborators(ctx, doc, mixin, control)) - const mixinTx = getMixinTx(tx, control, collaborators) + const mixinTx = getAddCollaboratTxes(tx.objectId, tx.objectClass, tx.objectSpace, control, collaborators) - res.push(mixinTx) + res.push(...mixinTx) res.push( ...(await ctx.with('get-space-collabtxes', {}, (ctx) => - getSpaceCollabTxes(ctx, control, doc, tx, activityMessage, cache) + getSpaceCollabTxes(ctx, control, doc, tx, activityMessage, cache, docCache) )) ) - res.push(...(await pushCollaboratorsToPublicSpace(control, doc, collaborators, cache))) + res.push(...(await pushCollaboratorsToPublicSpace(control, doc, collaborators, docCache))) const notificationTxes = await ctx.with('create-collabdocinfo', {}, (ctx) => createCollabDocInfo( @@ -992,7 +979,7 @@ async function createCollaboratorDoc ( shouldUpdateTimestamp: true }, [], - cache + docCache ) ) @@ -1001,128 +988,25 @@ async function createCollaboratorDoc ( return res } -async function updateCollaboratorsMixin ( - ctx: MeasureContext, - tx: TxMixin, - control: TriggerControl, - activityMessages: ActivityMessage[], - originTx: TxCUD, - cache: Map, Doc> -): Promise { - const { hierarchy } = control - - if (tx._class !== core.class.TxMixin) return [] - if (originTx.space === core.space.DerivedTx) return [] - if (!hierarchy.isDerived(tx.mixin, notification.mixin.Collaborators)) return [] - - const res: Tx[] = [] - - const notificationControl = await getNotificationProviderControl(ctx, control) - - if (tx.attributes.collaborators !== undefined) { - const createTx = ( - await control.findAll(ctx, core.class.TxCreateDoc, { - objectId: tx.objectId - }) - )[0] - const mixinTxes = await control.findAll(ctx, core.class.TxMixin, { - objectId: tx.objectId - }) - const prevDoc = TxProcessor.buildDoc2Doc([createTx, ...mixinTxes].filter((t) => t._id !== tx._id)) as Doc - const newCollabs: AccountUuid[] = [] - - let prevCollabs: Set - - if (hierarchy.hasMixin(prevDoc, notification.mixin.Collaborators)) { - const prevDocMixin = control.hierarchy.as(prevDoc, notification.mixin.Collaborators) - prevCollabs = new Set(prevDocMixin.collaborators ?? []) - } else { - const mixin = hierarchy.classHierarchyMixin(prevDoc._class, notification.mixin.ClassCollaborators) - prevCollabs = mixin !== undefined ? new Set(await getDocCollaborators(ctx, prevDoc, mixin, control)) : new Set() - } - - const type = await control.modelDb.findOne(notification.class.NotificationType, { - _id: notification.ids.CollaboratoAddNotification - }) - - if (type === undefined) { - return res - } - - const providers = await control.modelDb.findAll(notification.class.NotificationProvider, {}) - const sender: SenderInfo = await getSenderInfo(ctx, tx.modifiedBy, control) - const receivers = await getReceiversInfo(ctx, tx.attributes.collaborators, control) - - for (const collab of tx.attributes.collaborators) { - const info = receivers.find((it) => it.account === collab) - if (info === undefined) continue - if (!prevCollabs.has(collab) && sender.person?.personUuid !== collab) { - for (const provider of providers) { - if (isAllowed(control, info.socialIds, type, provider, notificationControl)) { - newCollabs.push(collab) - break - } - } - } - } - - if (newCollabs.length > 0) { - const object = cache.get(tx.objectId) ?? (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }))[0] - if (object === undefined) return res - const space = await getObjectSpace(control, object, cache) - - cache.set(object._id, object) - cache.set(space._id, space) - - const docNotifyContexts = await control.findAll(ctx, notification.class.DocNotifyContext, { - user: { $in: newCollabs }, - objectId: tx.objectId - }) - - const infos = receivers.filter((it) => newCollabs.includes(it.account)) - - for (const target of infos) { - const isMember = space.members.includes(target.account) - if (space.private && !isMember) continue - - if (!hierarchy.isDerived(space._class, core.class.SystemSpace) && !isMember) { - res.push( - control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { - $push: { members: target.account } - }) - ) - } - - for (const message of activityMessages) { - await pushActivityInboxNotifications( - ctx, - originTx, - control, - res, - target, - sender, - prevDoc, - docNotifyContexts, - message, - [], - true - ) - } - } - } - } - return res -} - async function collectionCollabDoc ( ctx: MeasureContext, tx: TxCUD, control: TriggerControl, activityMessages: ActivityMessage[], - cache: Map, Doc>, + cache: Map, Collaborator[]>, + docCache: Map, Doc>, ignoreCollection: boolean = false ): Promise { - let res = await createCollaboratorNotifications(ctx, tx, control, activityMessages, tx, cache, ignoreCollection) + let res = await createCollaboratorNotifications( + ctx, + tx, + control, + activityMessages, + tx, + cache, + docCache, + ignoreCollection + ) if (![core.class.TxCreateDoc, core.class.TxRemoveDoc, core.class.TxUpdateDoc].includes(tx._class)) { return res @@ -1134,7 +1018,7 @@ async function collectionCollabDoc ( return res } - const mixin = control.hierarchy.classHierarchyMixin(attachedToClass, notification.mixin.ClassCollaborators) + const mixin = getClassCollaborators(control.modelDb, control.hierarchy, attachedToClass) if (mixin === undefined) { return res @@ -1144,14 +1028,14 @@ async function collectionCollabDoc ( 'get-doc', {}, async (ctx) => - cache.get(attachedTo) ?? (await control.findAll(ctx, attachedToClass, { _id: attachedTo }, { limit: 1 }))[0] + docCache.get(attachedTo) ?? (await control.findAll(ctx, attachedToClass, { _id: attachedTo }, { limit: 1 }))[0] ) if (doc === undefined) { return res } - cache.set(doc._id, doc) + docCache.set(doc._id, doc) const collaborators = await ctx.with('get-collaborators', {}, (ctx) => getCollaborators(ctx, doc, control, tx, res)) @@ -1171,7 +1055,7 @@ async function collectionCollabDoc ( shouldUpdateTimestamp: true }, [], - cache + docCache ) ) ) @@ -1203,8 +1087,7 @@ async function removeContextNotifications ( ) } async function removeCollaboratorDoc (tx: TxRemoveDoc, control: TriggerControl): Promise { - const hierarchy = control.hierarchy - const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) + const mixin = getClassCollaborators(control.modelDb, control.hierarchy, tx.objectClass) if (mixin === undefined) { return [] @@ -1241,14 +1124,14 @@ async function removeCollaboratorDoc (tx: TxRemoveDoc, control: TriggerCont async function getNewCollaborators ( ops: DocumentUpdate | MixinUpdate, - mixin: ClassCollaborators, + mixin: ClassCollaborators, docClass: Ref>, control: TriggerControl ): Promise { const newCollaborators = new Set() if (ops.$push !== undefined) { for (const key in ops.$push) { - if (mixin.fields.includes(key)) { + if (mixin.fields.includes(key as any)) { let value = (ops.$push as any)[key] if (typeof value !== 'string') { value = value.$each @@ -1264,7 +1147,7 @@ async function getNewCollaborators ( } for (const key in ops) { if (key.startsWith('$')) continue - if (mixin.fields.includes(key)) { + if (mixin.fields.includes(key as any)) { const value = (ops as any)[key] const newCollabs = await getKeyCollaborators(docClass, value, key, control) if (newCollabs !== undefined) { @@ -1280,14 +1163,14 @@ async function getNewCollaborators ( async function getRemovedMembers ( ops: DocumentUpdate | MixinUpdate, - mixin: ClassCollaborators, + mixin: ClassCollaborators, docClass: Ref>, control: TriggerControl ): Promise { const removedCollaborators: AccountUuid[] = [] if (ops.$pull !== undefined && 'members' in ops.$pull) { const key = 'members' - if (mixin.fields.includes(key)) { + if (mixin.fields.includes(key as any)) { let value = (ops.$pull as any)[key] if (typeof value !== 'string') { value = value.$in @@ -1302,68 +1185,112 @@ async function getRemovedMembers ( return Array.from(new Set(removedCollaborators)) } +async function createSyncCollaboratorsTxes ( + ctx: MeasureContext, + control: TriggerControl, + cache: Map, Collaborator[]>, + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + added: AccountUuid[], + removed: AccountUuid[] +): Promise<{ txes: Tx[], toAdd: AccountUuid[] }> { + const res: Tx[] = [] + let currentCollaborators = + cache.get(objectId) ?? + (await control.findAll(ctx, core.class.Collaborator, { + attachedTo: objectId + })) + + const toAdd = added.filter((p) => currentCollaborators.find((c) => c.collaborator === p) === undefined) + + if (toAdd.length === 0 && removed.length === 0) return { txes: res, toAdd: [] } + + if (toAdd.length > 0) { + const txes = getAddCollaboratTxes(objectId, objectClass, objectSpace, control, toAdd) + res.push(...txes) + txes.forEach((tx) => { + const collab = TxProcessor.createDoc2Doc(tx) + currentCollaborators.push(collab) + }) + } + + if (removed.length > 0) { + const toRemove: Collaborator[] = [] + const collabs: Collaborator[] = [] + for (const collab of currentCollaborators) { + if (removed.includes(collab.collaborator)) { + toRemove.push(collab) + } else { + collabs.push(collab) + } + } + for (const removedCollab of toRemove) { + res.push(control.txFactory.createTxRemoveDoc(core.class.Collaborator, removedCollab.space, removedCollab._id)) + } + currentCollaborators = collabs + } + + cache.set(objectId, currentCollaborators) + + return { txes: res, toAdd } +} + async function updateCollaboratorDoc ( ctx: MeasureContext, tx: TxUpdateDoc | TxMixin, control: TriggerControl, activityMessages: ActivityMessage[], - cache: Map, Doc> + cache: Map, Collaborator[]>, + docCache: Map, Doc> ): Promise { const hierarchy = control.hierarchy let res: Tx[] = [] - const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) + const mixin = getClassCollaborators(control.modelDb, hierarchy, tx.objectClass) if (mixin === undefined) return [] const doc = await ctx.with( 'find-doc', { _class: tx.objectClass }, async (ctx) => - cache.get(tx.objectId) ?? (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] + docCache.get(tx.objectId) ?? (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] ) if (doc === undefined) return [] - cache.set(doc._id, doc) + docCache.set(doc._id, doc) const params: NotifyParams = { isOwn: true, isSpace: false, shouldUpdateTimestamp: true } - if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { - // we should handle change field and subscribe new collaborators - const collabsInfo = await ctx.with('get-tx-collaborators', {}, (ctx) => getTxCollabs(ctx, tx, control, doc)) + // we should handle change field and subscribe new collaborators + const collabsInfo = await ctx.with('get-tx-collaborators', {}, (ctx) => getTxCollabs(ctx, tx, control, cache, doc)) + const sync = await createSyncCollaboratorsTxes( + ctx, + control, + cache, + doc._id, + doc._class, + doc.space, + collabsInfo.added, + collabsInfo.removed + ) + res.push(...sync.txes) - if (collabsInfo.added.length > 0) { - res.push(createPushCollaboratorsTx(control, tx.objectId, tx.objectClass, tx.objectSpace, collabsInfo.added)) - // res.push(...(await pushCollaboratorsToPublicSpace(control, doc, collabsInfo.added, cache))) - } - - if (collabsInfo.removed.length > 0) { - res.push(createPullCollaboratorsTx(control, tx.objectId, tx.objectClass, tx.objectSpace, collabsInfo.removed)) - } - - res = res.concat( - await ctx.with('create-collab-docinfo', {}, (ctx) => - createCollabDocInfo( - ctx, - res, - collabsInfo.result, - control, - tx, - doc, - activityMessages, - params, - collabsInfo.removed, - cache - ) + res = res.concat( + await ctx.with('create-collab-docinfo', {}, (ctx) => + createCollabDocInfo( + ctx, + res, + collabsInfo.result, + control, + tx, + doc, + activityMessages, + params, + collabsInfo.removed, + docCache ) ) - } else { - const collaborators = await ctx.with('get-doc-collaborators', {}, (ctx) => - getDocCollaborators(ctx, doc, mixin, control) - ) - res.push(getMixinTx(tx, control, collaborators)) - res = res.concat( - await createCollabDocInfo(ctx, res, collaborators, control, tx, doc, activityMessages, params, [], cache) - ) - } + ) res = res.concat( await ctx.with('get-space-collabtxes', {}, (ctx) => - getSpaceCollabTxes(ctx, control, doc, tx, activityMessages, cache) + getSpaceCollabTxes(ctx, control, doc, tx, activityMessages, cache, docCache) ) ) res = res.concat( @@ -1446,62 +1373,49 @@ async function updateCollaborators ( ctx: MeasureContext, control: TriggerControl, tx: TxCUD, - cache: Map, Doc> = new Map, Doc>() + cache: Map, Collaborator[]> ): Promise { - if (tx._class !== core.class.TxUpdateDoc && tx._class !== core.class.TxMixin) return [] + if (tx._class !== core.class.TxCreateDoc && tx._class !== core.class.TxRemoveDoc) return [] + if (tx.objectClass !== core.class.Collaborator) return [] const hierarchy = control.hierarchy - const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) - if (mixin === undefined) return [] + const mixin = getClassCollaborators(control.modelDb, hierarchy, tx.objectClass) + if (mixin === undefined || tx.attachedToClass === undefined || tx.attachedTo === undefined) return [] - const { objectClass, objectId, objectSpace } = tx - const ops = isMixinTx(tx) ? tx.attributes : (tx as TxUpdateDoc).operations - const addedCollaborators = await getNewCollaborators(ops, mixin, objectClass, control) - const isSpace = control.hierarchy.isDerived(objectClass, core.class.Space) - const removedCollaborators = isSpace - ? await getRemovedMembers(ops, mixin, objectClass as Ref>, control) - : [] - - if (removedCollaborators.length === 0 && addedCollaborators.length === 0) return [] - - const doc = - cache.get(objectId) ?? (await control.findAll(control.ctx, objectClass, { _id: objectId }, { limit: 1 }))[0] - cache.set(objectId, doc) - if (doc === undefined) return [] + if (hierarchy.classHierarchyMixin(tx.attachedToClass, activity.mixin.ActivityDoc) === undefined) return [] + const contexts = await control.findAll(control.ctx, notification.class.DocNotifyContext, { objectId: tx.attachedTo }) const res: Tx[] = [] - const currentCollaborators = new Set(hierarchy.as(doc, notification.mixin.Collaborators).collaborators) - const toAdd = addedCollaborators.filter((p) => !currentCollaborators.has(p)) + if (tx._class === core.class.TxCreateDoc) { + const collab = TxProcessor.createDoc2Doc(tx as TxCreateDoc) + const addedInfo = await getReceiversInfo(ctx, [collab.collaborator], control) - if (toAdd.length === 0 && removedCollaborators.length === 0) return [] - - if (toAdd.length > 0) { - res.push(createPushCollaboratorsTx(control, objectId, objectClass, objectSpace, toAdd)) - // res.push(...(await pushCollaboratorsToPublicSpace(control, doc, toAdd, cache))) - } - - if (removedCollaborators.length > 0) { - res.push(createPullCollaboratorsTx(control, objectId, objectClass, objectSpace, removedCollaborators)) - } - - if (hierarchy.classHierarchyMixin(objectClass, activity.mixin.ActivityDoc) === undefined) return res - - const contexts = await control.findAll(control.ctx, notification.class.DocNotifyContext, { objectId }) - const addedInfo = await getReceiversInfo(ctx, toAdd, control) - - for (const info of addedInfo.values()) { - const context = getDocNotifyContext(control, contexts, objectId, info.account) - if (context !== undefined) { - if (context.hidden) { - res.push(control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { hidden: false })) + for (const info of addedInfo.values()) { + const context = getDocNotifyContext(control, contexts, tx.attachedTo, info.account) + if (context !== undefined) { + if (context.hidden) { + res.push(control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { hidden: false })) + } } + await createNotifyContext( + ctx, + control, + tx.attachedTo, + tx.attachedToClass, + tx.objectSpace, + info, + tx.modifiedBy, + undefined, + tx + ) } - await createNotifyContext(ctx, control, objectId, objectClass, objectSpace, info, tx.modifiedBy, undefined, tx) + } else { + const removed = control.removedMap.get(tx.objectId) as Collaborator + if (removed === undefined) return [] + await removeContexts(ctx, contexts, [removed.collaborator], control) } - await removeContexts(ctx, contexts, removedCollaborators, control) - return res } @@ -1511,7 +1425,8 @@ export async function createCollaboratorNotifications ( control: TriggerControl, activityMessages: ActivityMessage[], originTx?: TxCUD, - cache: Map, Doc> = new Map, Doc>(), + cache: Map, Collaborator[]> = new Map, Collaborator[]>(), + docCache: Map, Doc> = new Map, Doc>(), ignoreCollection: boolean = false ): Promise { if (tx.space === core.space.DerivedTx) { @@ -1525,32 +1440,20 @@ export async function createCollaboratorNotifications ( if (tx.attachedTo !== undefined && !ignoreCollection) { return await ctx.with('collectionCollabDoc', {}, (ctx) => - collectionCollabDoc(ctx, tx as TxCUD, control, activityMessages, cache, true) + collectionCollabDoc(ctx, tx as TxCUD, control, activityMessages, cache, docCache, true) ) } switch (tx._class) { case core.class.TxCreateDoc: { return await ctx.with('createCollaboratorDoc', {}, (ctx) => - createCollaboratorDoc(ctx, tx as TxCreateDoc, control, activityMessages, cache) + createCollaboratorDoc(ctx, tx as TxCreateDoc, control, activityMessages, cache, docCache) ) } case core.class.TxUpdateDoc: case core.class.TxMixin: { - let res = await ctx.with('updateCollaboratorDoc', {}, (ctx) => - updateCollaboratorDoc(ctx, tx as TxUpdateDoc, control, activityMessages, cache) - ) - res = res.concat( - await ctx.with('updateCollaboratorMixin', {}, (ctx) => - updateCollaboratorsMixin( - ctx, - tx as TxMixin, - control, - activityMessages, - originTx ?? tx, - cache - ) - ) + const res = await ctx.with('updateCollaboratorDoc', {}, (ctx) => + updateCollaboratorDoc(ctx, tx as TxUpdateDoc, control, activityMessages, cache, docCache) ) return res } @@ -1583,18 +1486,22 @@ export async function getCollaborators ( tx: TxCUD, res: Tx[] ): Promise { - const mixin = control.hierarchy.classHierarchyMixin(doc._class, notification.mixin.ClassCollaborators) + const mixin = getClassCollaborators(control.modelDb, control.hierarchy, doc._class) if (mixin === undefined) { return [] } - if (control.hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { - return control.hierarchy.as(doc, notification.mixin.Collaborators).collaborators + const collaborators = await control.findAll(ctx, core.class.Collaborator, { + attachedTo: doc._id + }) + + if (collaborators.length > 0) { + return collaborators.map((p) => p.collaborator) } else { const collaborators = await getDocCollaborators(ctx, doc, mixin, control) - res.push(getMixinTx(tx, control, collaborators)) + res.push(...getAddCollaboratTxes(tx.objectId, tx.objectClass, tx.objectSpace, control, collaborators)) return collaborators } } @@ -1712,8 +1619,8 @@ async function OnDocRemove (txes: TxCUD[], control: TriggerControl): Promis return res } -export * from './types' export * from './push' +export * from './types' export * from './utils' // eslint-disable-next-line @typescript-eslint/explicit-function-return-type diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts index 4fd4d01c6a..fcdbdb69e1 100644 --- a/server-plugins/notification-resources/src/utils.ts +++ b/server-plugins/notification-resources/src/utils.ts @@ -47,7 +47,6 @@ import core, { TxUpdateDoc } from '@hcengineering/core' import notification, { - Collaborators, NotificationContent, notificationId, NotificationProvider, @@ -56,6 +55,7 @@ import notification, { type NotificationTypeSetting } from '@hcengineering/notification' import { getMetadata, getResource, IntlString, translate } from '@hcengineering/platform' +import { getPersonSpaces } from '@hcengineering/server-contact' import serverCore, { TriggerControl } from '@hcengineering/server-core' import serverNotification, { HTMLPresenter, @@ -65,10 +65,9 @@ import serverNotification, { TextPresenter } from '@hcengineering/server-notification' import serverView from '@hcengineering/server-view' +import { extractReferences, markupToJSON, Reference } from '@hcengineering/text-core' import { encodeObjectURI } from '@hcengineering/view' import { workbenchId } from '@hcengineering/workbench' -import { extractReferences, markupToJSON, Reference } from '@hcengineering/text-core' -import { getPersonSpaces } from '@hcengineering/server-contact' import { NotifyResult } from './types' @@ -567,35 +566,6 @@ export async function getSenderInfo ( } } -export function createPushCollaboratorsTx ( - control: TriggerControl, - objectId: Ref, - objectClass: Ref>, - space: Ref, - collaborators: AccountUuid[] -): TxMixin { - return control.txFactory.createTxMixin(objectId, objectClass, space, notification.mixin.Collaborators, { - $push: { - collaborators: { - $each: collaborators, - $position: 0 - } - } - }) -} - -export function createPullCollaboratorsTx ( - control: TriggerControl, - objectId: Ref, - objectClass: Ref>, - space: Ref, - collaborators: AccountUuid[] -): TxMixin { - return control.txFactory.createTxMixin(objectId, objectClass, space, notification.mixin.Collaborators, { - $pull: { collaborators: { $in: collaborators } } - }) -} - export async function getNotificationLink ( control: TriggerControl, doc: Doc, diff --git a/server-plugins/time-resources/src/index.ts b/server-plugins/time-resources/src/index.ts index a7dd25ba1c..8821ba4502 100644 --- a/server-plugins/time-resources/src/index.ts +++ b/server-plugins/time-resources/src/index.ts @@ -241,15 +241,6 @@ export async function OnToDoCreate (txes: TxCUD[], control: TriggerControl) continue } - const mixin = hierarchy.classHierarchyMixin( - createTx.objectClass as Ref>, - notification.mixin.ClassCollaborators - ) - - if (mixin === undefined) { - continue - } - const todo = TxProcessor.createDoc2Doc(createTx) const object = (await control.findAll(control.ctx, todo.attachedToClass, { _id: todo.attachedTo }))[0] if (object === undefined) { diff --git a/server/postgres/src/schemas.ts b/server/postgres/src/schemas.ts index e25d708220..a96cc98473 100644 --- a/server/postgres/src/schemas.ts +++ b/server/postgres/src/schemas.ts @@ -1,4 +1,4 @@ -import { DOMAIN_MODEL_TX, DOMAIN_RELATION, DOMAIN_SPACE, DOMAIN_TX } from '@hcengineering/core' +import { DOMAIN_COLLABORATOR, DOMAIN_MODEL_TX, DOMAIN_RELATION, DOMAIN_SPACE, DOMAIN_TX } from '@hcengineering/core' export type DataType = 'bigint' | 'bool' | 'text' | 'text[]' @@ -70,6 +70,25 @@ const defaultSchema: Schema = { } } +const collaboratorSchema: Schema = { + ...baseSchema, + attachedTo: { + type: 'text', + notNull: true, + index: true + }, + attachedToClass: { + type: 'text', + notNull: true, + index: true + }, + collaborator: { + type: 'text', + notNull: true, + index: true + } +} + const spaceSchema: Schema = { ...baseSchema, private: { @@ -297,6 +316,7 @@ export const domainSchemas: Record = { [translateDomain('github_sync')]: docSyncInfo, [translateDomain('github_user')]: githubLogin, [DOMAIN_RELATION]: relationSchema, + [DOMAIN_COLLABORATOR]: collaboratorSchema, kanban: defaultSchema } diff --git a/server/postgres/src/storage.ts b/server/postgres/src/storage.ts index d869c322f4..d0dc09cc0e 100644 --- a/server/postgres/src/storage.ts +++ b/server/postgres/src/storage.ts @@ -22,6 +22,7 @@ import core, { type DocumentQuery, type DocumentUpdate, type Domain, + DOMAIN_COLLABORATOR, DOMAIN_MODEL, DOMAIN_MODEL_TX, DOMAIN_RELATION, @@ -673,7 +674,7 @@ abstract class PostgresAdapterBase implements DbAdapter { const select = `SELECT ${this.getProjection(vars, domain, projection, joins, options?.associations)} FROM ${domain}` const showArchived = shouldShowArchived(query, options) - const secJoin = this.addSecurity(vars, query, showArchived, domain, ctx.contextData) + const secJoin = this.addSecurity(_class, vars, query, showArchived, domain, ctx.contextData) if (secJoin !== undefined) { sqlChunks.push(secJoin) } @@ -694,7 +695,7 @@ abstract class PostgresAdapterBase implements DbAdapter { if (options?.total === true) { const pvars = new ValuesVariables() const showArchived = shouldShowArchived(query, options) - const secJoin = this.addSecurity(pvars, query, showArchived, domain, ctx.contextData) + const secJoin = this.addSecurity(_class, pvars, query, showArchived, domain, ctx.contextData) const totalChunks: string[] = [] if (secJoin !== undefined) { totalChunks.push(secJoin) @@ -784,6 +785,7 @@ abstract class PostgresAdapterBase implements DbAdapter { } addSecurity( + _class: Ref>, vars: ValuesVariables, query: DocumentQuery, showArchived: boolean, @@ -801,8 +803,15 @@ abstract class PostgresAdapterBase implements DbAdapter { const key = domain === DOMAIN_SPACE ? '_id' : domain === DOMAIN_TX ? '"objectSpace"' : 'space' const privateCheck = domain === DOMAIN_SPACE ? ' OR sec.private = false' : '' const archivedCheck = showArchived ? '' : ' AND sec.archived = false' - const q = `(sec.members @> '{"${acc.uuid}"}' OR sec."_class" = '${core.class.SystemSpace}'${privateCheck})${archivedCheck}` - return `INNER JOIN ${translateDomain(DOMAIN_SPACE)} AS sec ON sec._id = ${domain}.${key} AND sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND ${q}` + const q = `(sec._id = '${core.space.Space}' OR sec."_class" = '${core.class.SystemSpace}' OR sec.members @> '{"${acc.uuid}"}'${privateCheck})${archivedCheck}` + const res = `INNER JOIN ${translateDomain(DOMAIN_SPACE)} AS sec ON sec._id = ${domain}.${key} AND sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND ${q}` + + const collabSec = this.modelDb.findAllSync(core.class.ClassCollaborators, { attachedTo: _class })[0] + if (collabSec === undefined || collabSec.provideSecurity !== true || acc.role !== AccountRole.Guest) { + return res + } + const collab = ` INNER JOIN ${translateDomain(DOMAIN_COLLABORATOR)} AS collab_sec ON collab_sec.collaborator = '${acc.uuid}' AND collab_sec."attachedTo" = ${domain}._id AND collab_sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND ${q}` + return res + collab } } } diff --git a/services/ai-bot/pod-ai-bot/src/workspace/love.ts b/services/ai-bot/pod-ai-bot/src/workspace/love.ts index aaf7071c3d..6d40db5ba2 100644 --- a/services/ai-bot/pod-ai-bot/src/workspace/love.ts +++ b/services/ai-bot/pod-ai-bot/src/workspace/love.ts @@ -28,7 +28,8 @@ import core, { TxProcessor, TxUpdateDoc, WorkspaceUuid, - pickPrimarySocialId + pickPrimarySocialId, + AccountUuid } from '@hcengineering/core' import love, { getFreeRoomPlace, @@ -273,6 +274,7 @@ export class LoveController { room: room._id, person: this.currentPerson._id, name: this.currentPerson.name, + account: (this.currentPerson.personUuid as AccountUuid) ?? null, sessionId: null }) } diff --git a/services/github/model-github/src/index.ts b/services/github/model-github/src/index.ts index 1ad1aa1fbb..29dd341b13 100644 --- a/services/github/model-github/src/index.ts +++ b/services/github/model-github/src/index.ts @@ -27,30 +27,22 @@ import core, { TAttachedDoc, TDoc } from '@hcengineering/model-core' import github from './plugin' import { + AccountRole, DateRangeMode, IndexKind, - type PersonId, type Class, + type ClassCollaborators, type Data, type Doc, type Domain, type Hyperlink, type Markup, + type PersonId, type Ref, - type Timestamp, - AccountRole + type Timestamp } from '@hcengineering/core' import { type Person } from '@hcengineering/contact' -import contact, { TPerson } from '@hcengineering/model-contact' -import presentation from '@hcengineering/model-presentation' -import tracker, { TComponent, TIssue, TProject, issuesOptions } from '@hcengineering/model-tracker' -import view, { classPresenter } from '@hcengineering/model-view' -import workbench from '@hcengineering/model-workbench' -import { getEmbeddedLabel } from '@hcengineering/platform' -import setting from '@hcengineering/setting' -import tags from '@hcengineering/tags' -import task from '@hcengineering/task' import { type DocSyncInfo, type GithubAuthentication, @@ -77,6 +69,15 @@ import { type MinimizeReason, type PullRequestMergeable } from '@hcengineering/github' +import contact, { TPerson } from '@hcengineering/model-contact' +import presentation from '@hcengineering/model-presentation' +import tracker, { TComponent, TIssue, TProject, issuesOptions } from '@hcengineering/model-tracker' +import view, { classPresenter } from '@hcengineering/model-view' +import workbench from '@hcengineering/model-workbench' +import { getEmbeddedLabel } from '@hcengineering/platform' +import setting from '@hcengineering/setting' +import tags from '@hcengineering/tags' +import task from '@hcengineering/task' import { generateClassNotificationTypes } from '@hcengineering/model-notification' @@ -617,7 +618,8 @@ export function createModel (builder: Builder): void { editor: github.component.EditPullRequest }) - builder.mixin(github.class.GithubPullRequest, core.class.Class, notification.mixin.ClassCollaborators, { + builder.createDoc>(core.class.ClassCollaborators, core.space.Model, { + attachedTo: github.class.GithubPullRequest, fields: ['createdBy', 'assignee', 'reviewers'] })