mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-13 05:07:43 +02:00
UBERF-5811: rework backlinks (#4887)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
@@ -32,7 +32,8 @@ import {
|
||||
type TxViewlet,
|
||||
type ActivityMessageControl,
|
||||
type SavedMessage,
|
||||
type IgnoreActivity
|
||||
type IgnoreActivity,
|
||||
type ActivityReference
|
||||
} from '@hcengineering/activity'
|
||||
import core, {
|
||||
DOMAIN_MODEL,
|
||||
@@ -60,7 +61,8 @@ import {
|
||||
TypeIntlString,
|
||||
ArrOf,
|
||||
TypeTimestamp,
|
||||
UX
|
||||
UX,
|
||||
TypeMarkup
|
||||
} from '@hcengineering/model'
|
||||
import { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
|
||||
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
@@ -146,6 +148,26 @@ export class TDocUpdateMessage extends TActivityMessage implements DocUpdateMess
|
||||
attributeUpdates?: DocAttributeUpdates
|
||||
}
|
||||
|
||||
@Model(activity.class.ActivityReference, activity.class.ActivityMessage)
|
||||
export class TActivityReference extends TActivityMessage implements ActivityReference {
|
||||
// Source document we have reference from, it should be parent document for Comment/Message.
|
||||
@Prop(TypeRef(core.class.Doc), core.string.Object)
|
||||
@Index(IndexKind.Indexed)
|
||||
srcDocId!: Ref<Doc>
|
||||
|
||||
@Prop(TypeRef(core.class.Class), core.string.Class)
|
||||
@Index(IndexKind.Indexed)
|
||||
srcDocClass!: Ref<Class<Doc>>
|
||||
|
||||
// Reference to comment/message in source doc
|
||||
attachedDocId?: Ref<Doc>
|
||||
attachedDocClass?: Ref<Class<Doc>>
|
||||
|
||||
@Prop(TypeMarkup(), activity.string.Message)
|
||||
@Index(IndexKind.FullText)
|
||||
message!: string
|
||||
}
|
||||
|
||||
@Model(activity.class.ActivityInfoMessage, activity.class.ActivityMessage)
|
||||
export class TActivityInfoMessage extends TActivityMessage implements ActivityInfoMessage {
|
||||
@Prop(TypeIntlString(), activity.string.Update)
|
||||
@@ -251,7 +273,8 @@ export function createModel (builder: Builder): void {
|
||||
TActivityInfoMessage,
|
||||
TActivityMessageControl,
|
||||
TSavedMessage,
|
||||
TIgnoreActivity
|
||||
TIgnoreActivity,
|
||||
TActivityReference
|
||||
)
|
||||
|
||||
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
@@ -262,6 +285,10 @@ export function createModel (builder: Builder): void {
|
||||
presenter: activity.component.ActivityInfoMessagePresenter
|
||||
})
|
||||
|
||||
builder.mixin(activity.class.ActivityReference, core.class.Class, view.mixin.ObjectPresenter, {
|
||||
presenter: activity.component.ActivityReferencePresenter
|
||||
})
|
||||
|
||||
builder.mixin(activity.class.DocUpdateMessage, core.class.Class, view.mixin.LinkProvider, {
|
||||
encode: activity.function.GetFragment
|
||||
})
|
||||
@@ -289,6 +316,12 @@ export function createModel (builder: Builder): void {
|
||||
filter: activity.filter.PinnedFilter
|
||||
})
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
|
||||
label: activity.string.Mentions,
|
||||
position: 60,
|
||||
filter: activity.filter.ReferencesFilter
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.DocUpdateMessageViewlet,
|
||||
core.space.Model,
|
||||
@@ -297,23 +330,11 @@ export function createModel (builder: Builder): void {
|
||||
action: 'create',
|
||||
component: activity.component.ReactionPresenter,
|
||||
label: activity.string.Reacted,
|
||||
onlyWithParent: true,
|
||||
hideIfRemoved: true
|
||||
onlyWithParent: true
|
||||
},
|
||||
activity.ids.ReactionAddedActivityViewlet
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.DocUpdateMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: activity.class.Reaction,
|
||||
action: 'remove',
|
||||
hideIfRemoved: true
|
||||
},
|
||||
activity.ids.ReactionRemovedActivityViewlet
|
||||
)
|
||||
|
||||
builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.ClassCollaborators, {
|
||||
fields: ['createdBy', 'repliedPersons']
|
||||
})
|
||||
@@ -330,6 +351,14 @@ export function createModel (builder: Builder): void {
|
||||
labelPresenter: activity.component.ActivityMessageNotificationLabel
|
||||
})
|
||||
|
||||
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
|
||||
messageMatch: {
|
||||
_class: activity.class.DocUpdateMessage,
|
||||
objectClass: activity.class.Reaction
|
||||
},
|
||||
presenter: activity.component.ReactionNotificationPresenter
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.NotificationType,
|
||||
core.space.Model,
|
||||
@@ -346,14 +375,6 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
activity.ids.AddReactionNotification
|
||||
)
|
||||
|
||||
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
|
||||
messageMatch: {
|
||||
_class: activity.class.DocUpdateMessage,
|
||||
objectClass: activity.class.Reaction
|
||||
},
|
||||
presenter: activity.component.ReactionNotificationPresenter
|
||||
})
|
||||
}
|
||||
|
||||
export default activity
|
||||
|
||||
@@ -30,11 +30,11 @@ export default mergeIds(activityId, activity, {
|
||||
filter: {
|
||||
AttributesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
|
||||
PinnedFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
|
||||
AllFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
||||
AllFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
|
||||
ReferencesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
||||
},
|
||||
ids: {
|
||||
ReactionAddedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
|
||||
ReactionRemovedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
|
||||
ActivityNotificationGroup: '' as Ref<NotificationGroup>,
|
||||
AddReactionNotification: '' as Ref<NotificationType>
|
||||
},
|
||||
|
||||
+17
-97
@@ -15,12 +15,10 @@
|
||||
|
||||
import activity, { type ActivityMessage } from '@hcengineering/activity'
|
||||
import {
|
||||
type Backlink,
|
||||
type Channel,
|
||||
chunterId,
|
||||
type ChunterMessage,
|
||||
type ChunterMessageExtension,
|
||||
type Comment,
|
||||
type DirectMessage,
|
||||
type Message,
|
||||
type DirectMessageInput,
|
||||
@@ -63,17 +61,16 @@ import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-c
|
||||
import notification from '@hcengineering/model-notification'
|
||||
import view, { createAction, actionTemplates as viewTemplates } from '@hcengineering/model-view'
|
||||
import workbench from '@hcengineering/model-workbench'
|
||||
import chunter from './plugin'
|
||||
import { type AnyComponent } from '@hcengineering/ui/src/types'
|
||||
import { TypeBoolean } from '@hcengineering/model'
|
||||
import type { IntlString, Resource } from '@hcengineering/platform'
|
||||
import { TActivityMessage } from '@hcengineering/model-activity'
|
||||
|
||||
import chunter from './plugin'
|
||||
|
||||
export { chunterId } from '@hcengineering/chunter'
|
||||
export { chunterOperation } from './migration'
|
||||
|
||||
export const DOMAIN_CHUNTER = 'chunter' as Domain
|
||||
export const DOMAIN_COMMENT = 'comment' as Domain
|
||||
|
||||
@Model(chunter.class.ChunterSpace, core.class.Space)
|
||||
export class TChunterSpace extends TSpace implements ChunterSpace {
|
||||
@@ -135,30 +132,6 @@ export class TMessage extends TChunterMessage implements Message {
|
||||
lastReply?: Timestamp
|
||||
}
|
||||
|
||||
@Model(chunter.class.Comment, core.class.AttachedDoc, DOMAIN_COMMENT)
|
||||
@UX(chunter.string.Comment, undefined, 'COM')
|
||||
export class TComment extends TAttachedDoc implements Comment {
|
||||
@Prop(TypeMarkup(), chunter.string.Message)
|
||||
@Index(IndexKind.FullText)
|
||||
message!: string
|
||||
|
||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||
attachments?: number
|
||||
|
||||
@Prop(Collection(activity.class.Reaction), activity.string.Reactions)
|
||||
reactions?: number
|
||||
|
||||
@Prop(TypeBoolean(), chunter.string.PinMessage)
|
||||
pinned?: boolean
|
||||
}
|
||||
|
||||
@Model(chunter.class.Backlink, chunter.class.Comment)
|
||||
@UX(chunter.string.Reference, chunter.icon.Chunter)
|
||||
export class TBacklink extends TComment implements Backlink {
|
||||
backlinkId!: Ref<Doc>
|
||||
backlinkClass!: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
@Mixin(chunter.mixin.DirectMessageInput, core.class.Class)
|
||||
export class TDirectMessageInput extends TClass implements DirectMessageInput {
|
||||
component!: AnyComponent
|
||||
@@ -227,8 +200,6 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
TMessage,
|
||||
TChunterMessage,
|
||||
TChunterMessageExtension,
|
||||
TComment,
|
||||
TBacklink,
|
||||
TDirectMessage,
|
||||
TDirectMessageInput,
|
||||
TChatMessage,
|
||||
@@ -442,12 +413,6 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
chunter.action.CopyChatMessageLink
|
||||
)
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessagesFilter, core.space.Model, {
|
||||
label: chunter.string.FilterBacklinks,
|
||||
position: 60,
|
||||
filter: chunter.filter.BacklinksFilter
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.ChunterMessage, core.class.Class, view.mixin.ClassFilters, {
|
||||
filters: ['space', '_class']
|
||||
})
|
||||
@@ -466,29 +431,6 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
chunter.ids.ChunterNotificationGroup
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.NotificationType,
|
||||
core.space.Model,
|
||||
{
|
||||
label: chunter.string.MentionNotification,
|
||||
generated: false,
|
||||
hidden: false,
|
||||
txClasses: [core.class.TxCreateDoc],
|
||||
objectClass: chunter.class.Backlink,
|
||||
group: chunter.ids.ChunterNotificationGroup,
|
||||
providers: {
|
||||
[notification.providers.EmailNotification]: true,
|
||||
[notification.providers.PlatformNotification]: true
|
||||
},
|
||||
templates: {
|
||||
textTemplate: '{sender} mentioned you in {doc} {data}',
|
||||
htmlTemplate: '<p>{sender}</b> mentioned you in {doc}</p> {data}',
|
||||
subjectTemplate: 'You were mentioned in {doc}'
|
||||
}
|
||||
},
|
||||
chunter.ids.MentionNotification
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.NotificationType,
|
||||
core.space.Model,
|
||||
@@ -546,43 +488,6 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
chunter.ids.ThreadNotification
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.DocUpdateMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: chunter.class.Backlink,
|
||||
action: 'create',
|
||||
component: chunter.component.BacklinkContent,
|
||||
labelComponent: chunter.activity.BacklinkCreatedLabel,
|
||||
hideIfRemoved: true
|
||||
},
|
||||
chunter.ids.BacklinkCreatedActivityViewlet
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.DocUpdateMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: chunter.class.Backlink,
|
||||
action: 'update',
|
||||
component: chunter.component.BacklinkContent,
|
||||
labelComponent: chunter.activity.BacklinkCreatedLabel,
|
||||
hideIfRemoved: true
|
||||
},
|
||||
chunter.ids.BacklinkUpdateActivityViewlet
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
activity.class.DocUpdateMessageViewlet,
|
||||
core.space.Model,
|
||||
{
|
||||
objectClass: chunter.class.Backlink,
|
||||
action: 'remove',
|
||||
hideIfRemoved: true
|
||||
},
|
||||
chunter.ids.BacklinkRemovedActivityViewlet
|
||||
)
|
||||
|
||||
createAction(builder, {
|
||||
...viewTemplates.open,
|
||||
target: chunter.class.Channel,
|
||||
@@ -679,6 +584,11 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
components: { input: chunter.component.ChatMessageInput }
|
||||
})
|
||||
|
||||
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
|
||||
ofClass: activity.class.ActivityReference,
|
||||
components: { input: chunter.component.ChatMessageInput }
|
||||
})
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
|
||||
ofMessage: chunter.class.ChatMessage,
|
||||
components: [{ kind: 'footer', component: chunter.component.Replies }]
|
||||
@@ -694,6 +604,11 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
components: [{ kind: 'footer', component: chunter.component.Replies }]
|
||||
})
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
|
||||
ofMessage: activity.class.ActivityReference,
|
||||
components: [{ kind: 'footer', component: chunter.component.Replies }]
|
||||
})
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
|
||||
ofMessage: chunter.class.ChatMessage,
|
||||
components: [{ kind: 'action', component: chunter.component.ReplyToThreadAction }]
|
||||
@@ -709,6 +624,11 @@ export function createModel (builder: Builder, options = { addApplication: true
|
||||
components: [{ kind: 'action', component: chunter.component.ReplyToThreadAction }]
|
||||
})
|
||||
|
||||
builder.createDoc(activity.class.ActivityMessageExtension, core.space.Model, {
|
||||
ofMessage: activity.class.ActivityReference,
|
||||
components: [{ kind: 'action', component: chunter.component.ReplyToThreadAction }]
|
||||
})
|
||||
|
||||
builder.mixin(chunter.class.Channel, core.class.Class, chunter.mixin.ObjectChatPanel, {
|
||||
ignoreKeys: ['archived', 'collaborators', 'lastMessage', 'pinned', 'topic', 'description']
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { type Class, type Doc, type Ref, TxOperations } from '@hcengineering/core'
|
||||
import core, { type Class, type Doc, type Domain, type Ref, TxOperations } from '@hcengineering/core'
|
||||
import {
|
||||
type MigrateOperation,
|
||||
type MigrationClient,
|
||||
@@ -21,12 +21,13 @@ import {
|
||||
tryMigrate
|
||||
} from '@hcengineering/model'
|
||||
import { chunterId } from '@hcengineering/chunter'
|
||||
import { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
|
||||
import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
|
||||
import notification from '@hcengineering/notification'
|
||||
|
||||
import { DOMAIN_COMMENT } from './index'
|
||||
import chunter from './plugin'
|
||||
|
||||
const DOMAIN_COMMENT = 'comment' as Domain
|
||||
|
||||
export async function createDocNotifyContexts (
|
||||
client: MigrationUpgradeClient,
|
||||
tx: TxOperations,
|
||||
@@ -103,29 +104,21 @@ export async function createRandom (client: MigrationUpgradeClient, tx: TxOperat
|
||||
await createDocNotifyContexts(client, tx, chunter.space.Random, chunter.class.Channel)
|
||||
}
|
||||
|
||||
async function createBacklink (tx: TxOperations): Promise<void> {
|
||||
const current = await tx.findOne(core.class.Space, {
|
||||
_id: chunter.space.Backlinks
|
||||
})
|
||||
if (current === undefined) {
|
||||
await tx.createDoc(
|
||||
core.class.Space,
|
||||
core.space.Space,
|
||||
{
|
||||
name: 'Backlinks',
|
||||
description: 'Backlinks',
|
||||
private: false,
|
||||
archived: false,
|
||||
members: []
|
||||
},
|
||||
chunter.space.Backlinks
|
||||
)
|
||||
}
|
||||
async function convertCommentsToChatMessages (client: MigrationClient): Promise<void> {
|
||||
await client.update(
|
||||
DOMAIN_COMMENT,
|
||||
{ _class: 'chunter:class:Comment' as Ref<Class<Doc>> },
|
||||
{ _class: chunter.class.ChatMessage }
|
||||
)
|
||||
await client.move(DOMAIN_COMMENT, { _class: chunter.class.ChatMessage }, DOMAIN_ACTIVITY)
|
||||
}
|
||||
|
||||
async function convertCommentsToChatMessages (client: MigrationClient): Promise<void> {
|
||||
await client.update(DOMAIN_COMMENT, { _class: chunter.class.Comment }, { _class: chunter.class.ChatMessage })
|
||||
await client.move(DOMAIN_COMMENT, { _class: chunter.class.ChatMessage }, DOMAIN_ACTIVITY)
|
||||
async function removeBacklinks (client: MigrationClient): Promise<void> {
|
||||
await client.deleteMany(DOMAIN_COMMENT, { _class: 'chunter:class:Backlink' as Ref<Class<Doc>> })
|
||||
await client.deleteMany(DOMAIN_ACTIVITY, {
|
||||
_class: activity.class.DocUpdateMessage,
|
||||
objectClass: 'chunter:class:Backlink' as Ref<Class<Doc>>
|
||||
})
|
||||
}
|
||||
|
||||
export const chunterOperation: MigrateOperation = {
|
||||
@@ -136,11 +129,16 @@ export const chunterOperation: MigrateOperation = {
|
||||
func: convertCommentsToChatMessages
|
||||
}
|
||||
])
|
||||
await tryMigrate(client, chunterId, [
|
||||
{
|
||||
state: 'remove-backlinks',
|
||||
func: removeBacklinks
|
||||
}
|
||||
])
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
const tx = new TxOperations(client, core.account.System)
|
||||
await createGeneral(client, tx)
|
||||
await createRandom(client, tx)
|
||||
await createBacklink(tx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { ActivityMessage, DocUpdateMessageViewlet, TxViewlet } from '@hcengineering/activity'
|
||||
import type { ActivityMessage, TxViewlet } from '@hcengineering/activity'
|
||||
import { chunterId, type Channel } from '@hcengineering/chunter'
|
||||
import chunter from '@hcengineering/chunter-resources/src/plugin'
|
||||
import { type Client, type Doc, type Ref } from '@hcengineering/core'
|
||||
@@ -29,8 +29,6 @@ export default mergeIds(chunterId, chunter, {
|
||||
DirectMessagePresenter: '' as AnyComponent,
|
||||
MessagePresenter: '' as AnyComponent,
|
||||
DmPresenter: '' as AnyComponent,
|
||||
BacklinkContent: '' as AnyComponent,
|
||||
BacklinkReference: '' as AnyComponent,
|
||||
ChannelsPanel: '' as AnyComponent,
|
||||
Chat: '' as AnyComponent,
|
||||
ChatMessageNotificationLabel: '' as AnyComponent,
|
||||
@@ -68,7 +66,6 @@ export default mergeIds(chunterId, chunter, {
|
||||
PinnedMessages: '' as IntlString,
|
||||
SavedMessages: '' as IntlString,
|
||||
Emoji: '' as IntlString,
|
||||
FilterBacklinks: '' as IntlString,
|
||||
DM: '' as IntlString,
|
||||
DMNotification: '' as IntlString,
|
||||
ConfigLabel: '' as IntlString,
|
||||
@@ -81,21 +78,15 @@ export default mergeIds(chunterId, chunter, {
|
||||
},
|
||||
ids: {
|
||||
TxCommentCreate: '' as Ref<TxViewlet>,
|
||||
TxBacklinkCreate: '' as Ref<TxViewlet>,
|
||||
TxCommentRemove: '' as Ref<TxViewlet>,
|
||||
TxBacklinkRemove: '' as Ref<TxViewlet>,
|
||||
TxMessageCreate: '' as Ref<TxViewlet>,
|
||||
TxChatMessageCreate: '' as Ref<TxViewlet>,
|
||||
TxChatMessageRemove: '' as Ref<TxViewlet>,
|
||||
ChunterNotificationGroup: '' as Ref<NotificationGroup>,
|
||||
BacklinkCreatedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
|
||||
BacklinkUpdateActivityViewlet: '' as Ref<DocUpdateMessageViewlet>,
|
||||
BacklinkRemovedActivityViewlet: '' as Ref<DocUpdateMessageViewlet>
|
||||
ChunterNotificationGroup: '' as Ref<NotificationGroup>
|
||||
},
|
||||
activity: {
|
||||
TxCommentCreate: '' as AnyComponent,
|
||||
TxMessageCreate: '' as AnyComponent,
|
||||
BacklinkCreatedLabel: '' as AnyComponent
|
||||
TxMessageCreate: '' as AnyComponent
|
||||
},
|
||||
space: {
|
||||
General: '' as Ref<Channel>,
|
||||
@@ -111,7 +102,6 @@ export default mergeIds(chunterId, chunter, {
|
||||
GetThreadLink: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
|
||||
},
|
||||
filter: {
|
||||
BacklinksFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>,
|
||||
ChatMessagesFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@hcengineering/contact-resources": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/model": "^0.6.7",
|
||||
"@hcengineering/model-activity": "^0.6.0",
|
||||
"@hcengineering/model-attachment": "^0.6.0",
|
||||
"@hcengineering/model-chunter": "^0.6.0",
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
type ModelLogger,
|
||||
tryMigrate
|
||||
} from '@hcengineering/model'
|
||||
import { DOMAIN_COMMENT } from '@hcengineering/model-chunter'
|
||||
import core from '@hcengineering/model-core'
|
||||
import { DOMAIN_VIEW } from '@hcengineering/model-view'
|
||||
import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity'
|
||||
|
||||
import contact, { DOMAIN_CONTACT, contactId } from './index'
|
||||
|
||||
async function createSpace (tx: TxOperations): Promise<void> {
|
||||
@@ -111,20 +112,20 @@ export const contactOperation: MigrateOperation = {
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
'tx.attributes.backlinkClass': 'contact:class:Employee'
|
||||
'tx.attributes.srcDocClass': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'tx.attributes.backlinkClass': contact.mixin.Employee }
|
||||
$set: { 'tx.attributes.srcDocClass': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
|
||||
await client.update(
|
||||
DOMAIN_TX,
|
||||
{
|
||||
'tx.attributes.backlinkClass': 'contact:class:Employee'
|
||||
'tx.attributes.srcDocClass': 'contact:class:Employee'
|
||||
},
|
||||
{
|
||||
$set: { 'tx.attributes.backlinkClass': contact.mixin.Employee }
|
||||
$set: { 'tx.attributes.srcDocClass': contact.mixin.Employee }
|
||||
}
|
||||
)
|
||||
|
||||
@@ -167,9 +168,12 @@ export const contactOperation: MigrateOperation = {
|
||||
)
|
||||
}
|
||||
await client.update(
|
||||
DOMAIN_COMMENT,
|
||||
{ backlinkClass: 'contact:class:Employee' },
|
||||
{ $set: { backlinkClass: contact.mixin.Employee } }
|
||||
DOMAIN_ACTIVITY,
|
||||
{
|
||||
_class: activity.class.ActivityReference,
|
||||
srcDocClass: 'contact:class:Employee'
|
||||
},
|
||||
{ $set: { srcDocClass: contact.mixin.Employee } }
|
||||
)
|
||||
await client.update(
|
||||
'tags' as Domain,
|
||||
|
||||
@@ -99,15 +99,15 @@ export class TDocument extends TAttachedDoc implements Document {
|
||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||
attachments?: number
|
||||
|
||||
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
|
||||
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
|
||||
comments?: number
|
||||
|
||||
@Prop(Collection(tags.class.TagReference), document.string.Labels)
|
||||
labels?: number
|
||||
|
||||
@Prop(Collection(chunter.class.Backlink), document.string.Backlinks)
|
||||
@Prop(Collection(activity.class.ActivityReference), document.string.Backlinks)
|
||||
@Hidden()
|
||||
backlinks!: number
|
||||
references!: number
|
||||
|
||||
@Prop(TypeString(), document.string.Icon)
|
||||
@Hidden()
|
||||
|
||||
@@ -71,7 +71,9 @@ import {
|
||||
type ActivityInboxNotification,
|
||||
type CommonInboxNotification,
|
||||
type NotificationContextPresenter,
|
||||
type ActivityNotificationViewlet
|
||||
type ActivityNotificationViewlet,
|
||||
type BaseNotificationType,
|
||||
type CommonNotificationType
|
||||
} from '@hcengineering/notification'
|
||||
import { type Asset, type IntlString } from '@hcengineering/platform'
|
||||
import setting from '@hcengineering/setting'
|
||||
@@ -99,19 +101,26 @@ export class TNotification extends TAttachedDoc implements Notification {
|
||||
type!: Ref<NotificationType>
|
||||
}
|
||||
|
||||
@Model(notification.class.NotificationType, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TNotificationType extends TDoc implements NotificationType {
|
||||
@Model(notification.class.BaseNotificationType, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TBaseNotificationType extends TDoc implements BaseNotificationType {
|
||||
generated!: boolean
|
||||
label!: IntlString
|
||||
group!: Ref<NotificationGroup>
|
||||
txClasses!: Ref<Class<Tx>>[]
|
||||
providers!: Record<Ref<NotificationProvider>, boolean>
|
||||
objectClass!: Ref<Class<Doc>>
|
||||
hidden!: boolean
|
||||
templates?: NotificationTemplate
|
||||
}
|
||||
|
||||
@Model(notification.class.NotificationType, notification.class.BaseNotificationType)
|
||||
export class TNotificationType extends TBaseNotificationType implements NotificationType {
|
||||
txClasses!: Ref<Class<Tx>>[]
|
||||
objectClass!: Ref<Class<Doc>>
|
||||
onlyOwn?: boolean
|
||||
}
|
||||
|
||||
@Model(notification.class.CommonNotificationType, notification.class.BaseNotificationType)
|
||||
export class TCommonNotificationType extends TBaseNotificationType implements CommonNotificationType {}
|
||||
|
||||
@Model(notification.class.NotificationGroup, core.class.Doc, DOMAIN_MODEL)
|
||||
export class TNotificationGroup extends TDoc implements NotificationGroup {
|
||||
label!: IntlString
|
||||
@@ -136,7 +145,7 @@ export class TNotificationProvider extends TDoc implements NotificationProvider
|
||||
@Model(notification.class.NotificationSetting, preference.class.Preference)
|
||||
export class TNotificationSetting extends TPreference implements NotificationSetting {
|
||||
declare attachedTo: Ref<TNotificationProvider>
|
||||
type!: Ref<TNotificationType>
|
||||
type!: Ref<BaseNotificationType>
|
||||
enabled!: boolean
|
||||
}
|
||||
|
||||
@@ -245,13 +254,18 @@ export class TActivityInboxNotification extends TInboxNotification implements Ac
|
||||
|
||||
@Model(notification.class.CommonInboxNotification, notification.class.InboxNotification)
|
||||
export class TCommonInboxNotification extends TInboxNotification implements CommonInboxNotification {
|
||||
header?: IntlString
|
||||
@Prop(TypeIntlString(), notification.string.Message)
|
||||
message!: IntlString
|
||||
@Prop(TypeIntlString(), core.string.String)
|
||||
header?: IntlString
|
||||
|
||||
props!: Record<string, any>
|
||||
icon!: Asset
|
||||
iconProps!: Record<string, any>
|
||||
@Prop(TypeIntlString(), notification.string.Message)
|
||||
message?: IntlString
|
||||
|
||||
@Prop(TypeString(), notification.string.Message)
|
||||
messageHtml?: string
|
||||
|
||||
props?: Record<string, any>
|
||||
icon?: Asset
|
||||
iconProps?: Record<string, any>
|
||||
}
|
||||
|
||||
@Model(notification.class.ActivityNotificationViewlet, core.class.Doc, DOMAIN_MODEL)
|
||||
@@ -279,7 +293,9 @@ export function createModel (builder: Builder): void {
|
||||
TActivityInboxNotification,
|
||||
TCommonInboxNotification,
|
||||
TNotificationContextPresenter,
|
||||
TActivityNotificationViewlet
|
||||
TActivityNotificationViewlet,
|
||||
TBaseNotificationType,
|
||||
TCommonNotificationType
|
||||
)
|
||||
|
||||
// Temporarily disabled, we should think about it
|
||||
@@ -599,6 +615,27 @@ export function createModel (builder: Builder): void {
|
||||
})
|
||||
|
||||
defineViewlets(builder)
|
||||
|
||||
builder.createDoc(
|
||||
notification.class.CommonNotificationType,
|
||||
core.space.Model,
|
||||
{
|
||||
label: activity.string.Mentions,
|
||||
generated: false,
|
||||
hidden: false,
|
||||
group: notification.ids.NotificationGroup,
|
||||
providers: {
|
||||
[notification.providers.EmailNotification]: true,
|
||||
[notification.providers.PlatformNotification]: true
|
||||
},
|
||||
templates: {
|
||||
textTemplate: '{sender} mentioned you in {doc} {data}',
|
||||
htmlTemplate: '<p>{sender}</b> mentioned you in {doc}</p> {data}',
|
||||
subjectTemplate: 'You were mentioned in {doc}'
|
||||
}
|
||||
},
|
||||
notification.ids.MentionCommonNotificationType
|
||||
)
|
||||
}
|
||||
|
||||
export function generateClassNotificationTypes (
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
TypeString,
|
||||
UX
|
||||
} from '@hcengineering/model'
|
||||
import chunter, { TComment } from '@hcengineering/model-chunter'
|
||||
import chunter, { TChatMessage } from '@hcengineering/model-chunter'
|
||||
import core, { TAttachedDoc, TClass } from '@hcengineering/model-core'
|
||||
import { generateClassNotificationTypes } from '@hcengineering/model-notification'
|
||||
import view from '@hcengineering/model-view'
|
||||
@@ -77,7 +77,7 @@ export class TRequest extends TAttachedDoc implements Request {
|
||||
}
|
||||
|
||||
@Mixin(request.mixin.RequestDecisionComment, chunter.class.ChatMessage)
|
||||
export class TRequestDecisionComment extends TComment implements RequestDecisionComment {}
|
||||
export class TRequestDecisionComment extends TChatMessage implements RequestDecisionComment {}
|
||||
|
||||
@Mixin(request.mixin.RequestPresenter, core.class.Class)
|
||||
export class TRequestPresenter extends TClass implements RequestPresenter {
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/server-activity": "^0.6.0",
|
||||
"@hcengineering/server-activity-resources": "^0.6.0",
|
||||
"@hcengineering/server-core": "^0.6.1"
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
"@hcengineering/server-notification": "^0.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +37,8 @@ export function createModel (builder: Builder): void {
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverActivity.trigger.OnDocRemoved
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverActivity.trigger.ReferenceTrigger
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/chunter": "^0.6.12",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
"@hcengineering/server-notification": "^0.6.1"
|
||||
"@hcengineering/server-notification": "^0.6.1",
|
||||
"@hcengineering/activity": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,6 @@ export function createModel (builder: Builder): void {
|
||||
presenter: serverChunter.function.ChunterNotificationContentProvider
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverChunter.trigger.BacklinkTrigger
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverChunter.trigger.ChunterTrigger
|
||||
})
|
||||
@@ -86,15 +82,6 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
})
|
||||
|
||||
builder.mixin(
|
||||
chunter.ids.MentionNotification,
|
||||
notification.class.NotificationType,
|
||||
serverNotification.mixin.TypeMatch,
|
||||
{
|
||||
func: serverChunter.function.IsMeMentioned
|
||||
}
|
||||
)
|
||||
|
||||
builder.mixin(chunter.ids.DMNotification, notification.class.NotificationType, serverNotification.mixin.TypeMatch, {
|
||||
func: serverChunter.function.IsDirectMessage
|
||||
})
|
||||
|
||||
@@ -76,14 +76,6 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.OnBacklinkCreate,
|
||||
txMatch: {
|
||||
_class: core.class.TxCollectionCUD,
|
||||
'tx._class': core.class.TxCreateDoc
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.OnAttributeCreate,
|
||||
txMatch: {
|
||||
@@ -99,4 +91,8 @@ export function createModel (builder: Builder): void {
|
||||
_class: core.class.TxUpdateDoc
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.OnDocRemove
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
"Update": "Update",
|
||||
"Updated": "Updated",
|
||||
"UpdatedCollection": "Updated",
|
||||
"Message": "Message"
|
||||
"Message": "Message",
|
||||
"Mentioned": "Mentioned",
|
||||
"You": "You",
|
||||
"Mentions": "Mentions",
|
||||
"MentionedYouIn": "Mentioned you in"
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,10 @@
|
||||
"Update": "Обновить",
|
||||
"Updated": "Обновил(а)",
|
||||
"UpdatedCollection": "Обновленные",
|
||||
"Message": "Сообщение"
|
||||
"Message": "Сообщение",
|
||||
"Mentioned": "Упомянул(а)",
|
||||
"You": "Вы",
|
||||
"Mentions": "Упоминания",
|
||||
"MentionedYouIn": "Упомянул(а) вас в"
|
||||
}
|
||||
}
|
||||
@@ -75,3 +75,5 @@ export function loadSavedMessages (): void {
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
||||
loadSavedMessages()
|
||||
|
||||
@@ -227,9 +227,7 @@ export async function combineActivityMessages (
|
||||
const client = getClient()
|
||||
const uncombinedMessages = messages.filter((message) => message._class !== activity.class.DocUpdateMessage)
|
||||
|
||||
const docUpdateMessages = combineByCreateThreshold(
|
||||
messages.filter((message): message is DocUpdateMessage => message._class === activity.class.DocUpdateMessage)
|
||||
)
|
||||
const docUpdateMessages = combineByCreateThreshold(messages.filter(isDocUpdateMessage))
|
||||
|
||||
if (docUpdateMessages.length === 0) {
|
||||
return sortActivityMessages(uncombinedMessages, sortingOrder)
|
||||
@@ -472,6 +470,10 @@ function getAttributeUpdatesKey (message: DocUpdateMessage): string {
|
||||
return [attrKey, attrClass, isMixin].join('-')
|
||||
}
|
||||
|
||||
export function referencesFilter (message: ActivityMessage, _class?: Ref<Doc>): boolean {
|
||||
return message._class === activity.class.ActivityReference
|
||||
}
|
||||
|
||||
export function attributesFilter (message: ActivityMessage, _class?: Ref<Doc>): boolean {
|
||||
if (message._class === activity.class.DocUpdateMessage) {
|
||||
return (message as DocUpdateMessage).objectClass === _class
|
||||
@@ -548,14 +550,22 @@ export async function getMessageFragment (doc: Doc): Promise<string> {
|
||||
return `${label}-${doc._id}`
|
||||
}
|
||||
|
||||
export function isReactionMessage (message?: ActivityMessage): message is DocUpdateMessage {
|
||||
function isDocUpdateMessage (message?: ActivityMessage): message is DocUpdateMessage {
|
||||
if (message === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (message._class !== activity.class.DocUpdateMessage) {
|
||||
return message._class === activity.class.DocUpdateMessage
|
||||
}
|
||||
|
||||
export function isReactionMessage (message?: ActivityMessage): boolean {
|
||||
if (message === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (message as DocUpdateMessage).objectClass === activity.class.Reaction
|
||||
if (!isDocUpdateMessage(message)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return message.objectClass === activity.class.Reaction
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import ActivityExtensionComponent from './ActivityExtension.svelte'
|
||||
import ActivityFilter from './ActivityFilter.svelte'
|
||||
import { combineActivityMessages } from '../activityMessagesUtils'
|
||||
import { loadSavedMessages } from '../activity'
|
||||
|
||||
export let object: Doc
|
||||
export let showCommenInput: boolean = true
|
||||
@@ -50,9 +49,9 @@
|
||||
|
||||
const res = activityMessagesQuery.query(
|
||||
activity.class.ActivityMessage,
|
||||
{ attachedTo: objectId },
|
||||
{ attachedTo: objectId, hidden: { $ne: true } },
|
||||
(result: ActivityMessage[]) => {
|
||||
combineActivityMessages(result, order).then((messages) => {
|
||||
void combineActivityMessages(result, order).then((messages) => {
|
||||
activityMessages = messages
|
||||
isLoading = false
|
||||
})
|
||||
@@ -69,10 +68,6 @@
|
||||
}
|
||||
|
||||
$: void updateActivityMessages(object._id, isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending)
|
||||
|
||||
onMount(() => {
|
||||
loadSavedMessages()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="antiSection-header high mt-9" class:invisible={transparent}>
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import activity, { ActivityReference } from '@hcengineering/activity'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Action, Label } from '@hcengineering/ui'
|
||||
import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Account, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
|
||||
import { Person, type PersonAccount } from '@hcengineering/contact'
|
||||
import view, { ObjectPanel } from '@hcengineering/view'
|
||||
import { DocNavLink, getDocLinkTitle } from '@hcengineering/view-resources'
|
||||
|
||||
import ReferenceContent from './ReferenceContent.svelte'
|
||||
import ReferenceSrcPresenter from './ReferenceSrcPresenter.svelte'
|
||||
|
||||
import ActivityMessageTemplate from '../activity-message/ActivityMessageTemplate.svelte'
|
||||
|
||||
export let value: ActivityReference
|
||||
export let showNotify: boolean = false
|
||||
export let isHighlighted: boolean = false
|
||||
export let isSelected: boolean = false
|
||||
export let shouldScroll: boolean = false
|
||||
export let embedded: boolean = false
|
||||
export let withActions: boolean = true
|
||||
export let showEmbedded = false
|
||||
export let hideFooter = false
|
||||
export let actions: Action[] = []
|
||||
export let skipLabel = false
|
||||
export let withFlatActions: boolean = true
|
||||
export let excludedActions: string[] = []
|
||||
export let hoverable = true
|
||||
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const currentAccount = getCurrentAccount() as PersonAccount
|
||||
|
||||
const srcDocQuery = createQuery()
|
||||
const targetDocQuery = createQuery()
|
||||
|
||||
let srcDoc: Doc | undefined = undefined
|
||||
let targetDoc: Doc | undefined = undefined
|
||||
|
||||
let srcDocPanel: ObjectPanel | undefined
|
||||
let targetPanel: ObjectPanel | undefined
|
||||
|
||||
let targetTitle: string | undefined = undefined
|
||||
|
||||
let person: Person | undefined = undefined
|
||||
|
||||
$: person = getPerson(value.createdBy ?? value.modifiedBy, $personAccountByIdStore, $personByIdStore)
|
||||
|
||||
$: srcDocQuery.query(value.srcDocClass, { _id: value.srcDocId }, (result) => {
|
||||
srcDoc = result.shift()
|
||||
})
|
||||
|
||||
$: targetDocQuery.query(value.attachedToClass, { _id: value.attachedTo }, (r) => {
|
||||
targetDoc = r.shift()
|
||||
})
|
||||
|
||||
$: targetPanel = hierarchy.classHierarchyMixin(value.attachedToClass, view.mixin.ObjectPanel)
|
||||
$: srcDocPanel = hierarchy.classHierarchyMixin(value.srcDocClass, view.mixin.ObjectPanel)
|
||||
|
||||
$: targetDoc !== undefined &&
|
||||
getDocLinkTitle(client, targetDoc._id, targetDoc._class, targetDoc).then((res) => {
|
||||
targetTitle = res
|
||||
})
|
||||
|
||||
function getPerson (
|
||||
_id: Ref<Account>,
|
||||
accountById: Map<Ref<PersonAccount>, PersonAccount>,
|
||||
personById: Map<Ref<Person>, Person>
|
||||
): Person | undefined {
|
||||
const personAccount = accountById.get(_id as Ref<PersonAccount>)
|
||||
|
||||
if (personAccount === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return personById.get(personAccount.person)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActivityMessageTemplate
|
||||
message={value}
|
||||
{person}
|
||||
{showNotify}
|
||||
{isHighlighted}
|
||||
{isSelected}
|
||||
{shouldScroll}
|
||||
{embedded}
|
||||
{excludedActions}
|
||||
{withActions}
|
||||
{showEmbedded}
|
||||
{hideFooter}
|
||||
{actions}
|
||||
{skipLabel}
|
||||
{withFlatActions}
|
||||
{hoverable}
|
||||
{hoverStyles}
|
||||
{onClick}
|
||||
{onReply}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<span class="header">
|
||||
<span class="text-sm lower ml-1">
|
||||
<Label label={activity.string.Mentioned} />
|
||||
</span>
|
||||
{#if targetDoc}
|
||||
<DocNavLink object={targetDoc} component={targetPanel?.component ?? view.component.EditDoc} shrink={0}>
|
||||
<span class="text-sm">
|
||||
{#if currentAccount.person === targetDoc._id}
|
||||
<Label label={activity.string.You} />
|
||||
{:else}
|
||||
{targetTitle}
|
||||
{/if}
|
||||
</span>
|
||||
</DocNavLink>
|
||||
{/if}
|
||||
{#if srcDoc}
|
||||
<span class="text-sm lower"><Label label={activity.string.In} /></span>
|
||||
<DocNavLink object={srcDoc} component={srcDocPanel?.component ?? view.component.EditDoc} shrink={0}>
|
||||
<span class="text-sm">
|
||||
<ReferenceSrcPresenter
|
||||
{value}
|
||||
inline={hierarchy.isDerived(srcDoc._class, activity.class.ActivityMessage)}
|
||||
/>
|
||||
</span>
|
||||
</DocNavLink>
|
||||
{/if}
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<ReferenceContent {value} />
|
||||
</svelte:fragment>
|
||||
</ActivityMessageTemplate>
|
||||
|
||||
<style lang="scss">
|
||||
.header {
|
||||
gap: var(--global-spacing-1);
|
||||
}
|
||||
</style>
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2024 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
|
||||
@@ -13,22 +13,22 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Backlink } from '@hcengineering/chunter'
|
||||
import { createQuery, MessageViewer } from '@hcengineering/presentation'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import activity, { ActivityReference } from '@hcengineering/activity'
|
||||
|
||||
import chunter from '../plugin'
|
||||
|
||||
export let _id: Ref<Backlink> | undefined = undefined
|
||||
export let value: Backlink | undefined = undefined
|
||||
export let _id: Ref<ActivityReference> | undefined = undefined
|
||||
export let value: ActivityReference | undefined = undefined
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
$: value === undefined &&
|
||||
_id &&
|
||||
query.query(chunter.class.Backlink, { _id }, (res) => {
|
||||
value = res[0]
|
||||
$: if (value === undefined && _id !== undefined) {
|
||||
query.query(activity.class.ActivityReference, { _id }, (res) => {
|
||||
value = res.shift()
|
||||
})
|
||||
} else {
|
||||
query.unsubscribe()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
+13
-14
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
// Copyright © 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2024 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
|
||||
@@ -13,35 +13,34 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Backlink } from '@hcengineering/chunter'
|
||||
import type { Doc } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import { getObjectPresenter } from '@hcengineering/view-resources'
|
||||
import { ActivityReference } from '@hcengineering/activity'
|
||||
|
||||
export let value: Backlink
|
||||
export let value: ActivityReference
|
||||
export let inline = true
|
||||
|
||||
const client = getClient()
|
||||
const srcDocQuery = createQuery()
|
||||
|
||||
let srcDoc: Doc | undefined
|
||||
let presenter: AttributeModel | undefined
|
||||
|
||||
const docQuery = createQuery()
|
||||
let doc: Doc | undefined
|
||||
$: srcDocQuery.query(value.srcDocClass, { _id: value.srcDocId }, (r) => {
|
||||
srcDoc = r.shift()
|
||||
})
|
||||
|
||||
$: value.backlinkClass != null &&
|
||||
docQuery.query(value.backlinkClass, { _id: value.backlinkId }, (r) => {
|
||||
doc = r.shift()
|
||||
})
|
||||
|
||||
$: if (doc !== undefined) {
|
||||
getObjectPresenter(client, doc._class, { key: '' }).then((p) => {
|
||||
presenter = p
|
||||
$: if (srcDoc !== undefined) {
|
||||
void getObjectPresenter(client, srcDoc._class, { key: '' }).then((result) => {
|
||||
presenter = result
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if presenter}
|
||||
<span class="labels-row">
|
||||
<svelte:component this={presenter.presenter} value={doc} {inline} embedded shouldShowAvatar={false} />
|
||||
<svelte:component this={presenter.presenter} value={srcDoc} {inline} embedded shouldShowAvatar={false} />
|
||||
</span>
|
||||
{/if}
|
||||
@@ -22,12 +22,21 @@ import ActivityInfoMessagePresenter from './components/activity-message/Activity
|
||||
import ReactionPresenter from './components/reactions/ReactionPresenter.svelte'
|
||||
import ReactionNotificationPresenter from './components/reactions/ReactionNotificationPresenter.svelte'
|
||||
import ActivityMessageNotificationLabel from './components/activity-message/ActivityMessageNotificationLabel.svelte'
|
||||
import ActivityReferencePresenter from './components/activity-reference/ActivityReferencePresenter.svelte'
|
||||
|
||||
import { getMessageFragment, attributesFilter, pinnedFilter, allFilter } from './activityMessagesUtils'
|
||||
import {
|
||||
getMessageFragment,
|
||||
attributesFilter,
|
||||
pinnedFilter,
|
||||
allFilter,
|
||||
referencesFilter
|
||||
} from './activityMessagesUtils'
|
||||
import { updateReferences } from './references'
|
||||
|
||||
export * from './activity'
|
||||
export * from './utils'
|
||||
export * from './activityMessagesUtils'
|
||||
export * from './references'
|
||||
|
||||
export { default as Reactions } from './components/reactions/Reactions.svelte'
|
||||
export { default as ActivityMessageTemplate } from './components/activity-message/ActivityMessageTemplate.svelte'
|
||||
@@ -40,6 +49,7 @@ export { default as ActivityMessageHeader } from './components/activity-message/
|
||||
export { default as AddReactionAction } from './components/reactions/AddReactionAction.svelte'
|
||||
export { default as ActivityMessageAction } from './components/ActivityMessageAction.svelte'
|
||||
export { default as ActivityMessagesFilterPopup } from './components/FilterPopup.svelte'
|
||||
export { default as ActivityReferencePresenter } from './components/activity-reference/ActivityReferencePresenter.svelte'
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
@@ -49,14 +59,19 @@ export default async (): Promise<Resources> => ({
|
||||
ReactionPresenter,
|
||||
ActivityInfoMessagePresenter,
|
||||
ReactionNotificationPresenter,
|
||||
ActivityMessageNotificationLabel
|
||||
ActivityMessageNotificationLabel,
|
||||
ActivityReferencePresenter
|
||||
},
|
||||
filter: {
|
||||
AttributesFilter: attributesFilter,
|
||||
PinnedFilter: pinnedFilter,
|
||||
AllFilter: allFilter
|
||||
AllFilter: allFilter,
|
||||
ReferencesFilter: referencesFilter
|
||||
},
|
||||
function: {
|
||||
GetFragment: getMessageFragment
|
||||
},
|
||||
backreference: {
|
||||
Update: updateReferences
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import core, {
|
||||
type Class,
|
||||
type Data,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type Ref,
|
||||
type RelatedDocument,
|
||||
type Space,
|
||||
type TxOperations
|
||||
} from '@hcengineering/core'
|
||||
import activity, { type ActivityReference } from '@hcengineering/activity'
|
||||
import { type IntlString, translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
async function updateReferencesList (
|
||||
client: TxOperations,
|
||||
q: DocumentQuery<ActivityReference>,
|
||||
references: Array<Data<ActivityReference>>,
|
||||
space: Ref<Space>
|
||||
): Promise<void> {
|
||||
const currentRefs: ActivityReference[] = await client.findAll(activity.class.ActivityReference, q)
|
||||
|
||||
// We need to find ones we need to remove, and ones we need to update.
|
||||
for (const c of currentRefs) {
|
||||
// Find existing and check if we need to update message.
|
||||
const pos = references.findIndex(
|
||||
(ref) => ref.srcDocId === c.srcDocId && ref.srcDocClass === c.srcDocClass && ref.attachedTo === c.attachedTo
|
||||
)
|
||||
if (pos !== -1) {
|
||||
// We need to check and update if required.
|
||||
const data = references[pos]
|
||||
if (c.message !== data.message) {
|
||||
await client.updateCollection(c._class, c.space, c._id, c.attachedTo, c.attachedToClass, c.collection, {
|
||||
message: data.message
|
||||
})
|
||||
}
|
||||
references.splice(pos, 1)
|
||||
} else {
|
||||
// We need to remove reference.
|
||||
await client.removeCollection(c._class, c.space, c._id, c.attachedTo, c.attachedToClass, c.collection)
|
||||
}
|
||||
}
|
||||
// Add missing references
|
||||
for (const ref of references) {
|
||||
const { attachedTo, attachedToClass, collection, ...adata } = ref
|
||||
await client.addCollection(activity.class.ActivityReference, space, attachedTo, attachedToClass, collection, adata)
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateReferences (
|
||||
source: Doc,
|
||||
key: string,
|
||||
target: RelatedDocument[],
|
||||
msg: IntlString
|
||||
): Promise<void> {
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const message = await translate(msg, {})
|
||||
const references: Array<Data<ActivityReference>> = target
|
||||
.filter((it) => !hierarchy.isDerived(it._class, contact.class.Person))
|
||||
.map((it) => ({
|
||||
srcDocId: source._id,
|
||||
srcDocClass: source._class,
|
||||
attachedTo: it._id,
|
||||
attachedToClass: it._class,
|
||||
message,
|
||||
collection: key
|
||||
}))
|
||||
|
||||
const query: DocumentQuery<ActivityReference> = { srcDocId: source._id, srcDocClass: source._class, collection: key }
|
||||
const space: Ref<Space> = hierarchy.isDerived(source._class, core.class.Space)
|
||||
? (source._id as Ref<Space>)
|
||||
: source.space
|
||||
|
||||
await updateReferencesList(client, query, references, space)
|
||||
}
|
||||
|
||||
function extractReferences (
|
||||
srcDocId: Ref<Doc>,
|
||||
srcDocClass: Ref<Class<Doc>>,
|
||||
attachedDocId: Ref<Doc> | undefined,
|
||||
attachedDocClass: Ref<Class<Doc>> | undefined,
|
||||
kids: NodeListOf<ChildNode>
|
||||
): Array<Data<ActivityReference>> {
|
||||
const result: Array<Data<ActivityReference>> = []
|
||||
|
||||
const nodes: Array<NodeListOf<ChildNode>> = [kids]
|
||||
while (true) {
|
||||
const nds = nodes.shift()
|
||||
if (nds === undefined) {
|
||||
break
|
||||
}
|
||||
nds.forEach((kid) => {
|
||||
if (
|
||||
kid.nodeType === Node.ELEMENT_NODE &&
|
||||
(kid as HTMLElement).localName === 'span' &&
|
||||
(kid as HTMLElement).getAttribute('data-type') === 'reference'
|
||||
) {
|
||||
const el = kid as HTMLElement
|
||||
const ato = el.getAttribute('data-id') as Ref<Doc>
|
||||
const atoClass = el.getAttribute('data-objectclass') as Ref<Class<Doc>>
|
||||
const e = result.find((e) => e.attachedTo === ato && e.attachedToClass === atoClass)
|
||||
if (e === undefined && ato !== attachedDocId && ato !== srcDocId) {
|
||||
result.push({
|
||||
attachedTo: ato,
|
||||
attachedToClass: atoClass,
|
||||
collection: 'references',
|
||||
srcDocId,
|
||||
srcDocClass,
|
||||
message: el.parentElement?.innerHTML ?? '',
|
||||
attachedDocId,
|
||||
attachedDocClass
|
||||
})
|
||||
}
|
||||
}
|
||||
nodes.push(kid.childNodes)
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getReferences (
|
||||
srcDocId: Ref<Doc>,
|
||||
srcDocClass: Ref<Class<Doc>>,
|
||||
attachedDocId: Ref<Doc> | undefined,
|
||||
attachedDocClass: Ref<Class<Doc>> | undefined,
|
||||
content: string
|
||||
): Array<Data<ActivityReference>> {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(content, 'text/html')
|
||||
|
||||
return extractReferences(
|
||||
srcDocId,
|
||||
srcDocClass,
|
||||
attachedDocId,
|
||||
attachedDocClass,
|
||||
doc.childNodes as NodeListOf<HTMLElement>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function createReferences (
|
||||
client: TxOperations,
|
||||
srcDocId: Ref<Doc>,
|
||||
srcDocClass: Ref<Class<Doc>>,
|
||||
attachedDocId: Ref<Doc> | undefined,
|
||||
attachedDocClass: Ref<Class<Doc>> | undefined,
|
||||
content: string,
|
||||
space: Ref<Space>
|
||||
): Promise<void> {
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
const references = getReferences(srcDocId, srcDocClass, attachedDocId, attachedDocClass, content)
|
||||
for (const ref of references) {
|
||||
if (hierarchy.isDerived(ref.attachedToClass, contact.class.Person)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const { attachedTo, attachedToClass, collection, ...adata } = ref
|
||||
await client.addCollection(activity.class.ActivityReference, space, attachedTo, attachedToClass, collection, adata)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
DocumentQuery,
|
||||
Mixin,
|
||||
Ref,
|
||||
type RelatedDocument,
|
||||
Timestamp,
|
||||
Tx,
|
||||
TxCreateDoc,
|
||||
@@ -180,6 +181,22 @@ export interface DocUpdateMessage extends ActivityMessage {
|
||||
attributeUpdates?: DocAttributeUpdates
|
||||
}
|
||||
|
||||
export interface ActivityReference extends ActivityMessage {
|
||||
// A mentioned document
|
||||
// attachedTo: Ref<Doc>
|
||||
// attachedToClass: Ref<Class<Doc>>
|
||||
|
||||
// Source document we have reference from, it should be parent document for Comment/Message.
|
||||
srcDocId: Ref<Doc>
|
||||
srcDocClass: Ref<Class<Doc>>
|
||||
|
||||
// Reference to comment/message in source doc
|
||||
attachedDocId?: Ref<Doc>
|
||||
attachedDocClass?: Ref<Class<Doc>>
|
||||
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -310,7 +327,8 @@ export default plugin(activityId, {
|
||||
ActivityMessagesFilter: '' as Ref<Class<ActivityMessagesFilter>>,
|
||||
ActivityExtension: '' as Ref<Class<ActivityExtension>>,
|
||||
Reaction: '' as Ref<Class<Reaction>>,
|
||||
SavedMessage: '' as Ref<Class<SavedMessage>>
|
||||
SavedMessage: '' as Ref<Class<SavedMessage>>,
|
||||
ActivityReference: '' as Ref<Class<ActivityReference>>
|
||||
},
|
||||
icon: {
|
||||
Activity: '' as Asset,
|
||||
@@ -342,7 +360,11 @@ export default plugin(activityId, {
|
||||
LastReply: '' as IntlString,
|
||||
RepliesCount: '' as IntlString,
|
||||
Reacted: '' as IntlString,
|
||||
Message: '' as IntlString
|
||||
Message: '' as IntlString,
|
||||
Mentioned: '' as IntlString,
|
||||
You: '' as IntlString,
|
||||
Mentions: '' as IntlString,
|
||||
MentionedYouIn: '' as IntlString
|
||||
},
|
||||
component: {
|
||||
Activity: '' as AnyComponent,
|
||||
@@ -351,9 +373,15 @@ export default plugin(activityId, {
|
||||
ActivityInfoMessagePresenter: '' as AnyComponent,
|
||||
ReactionPresenter: '' as AnyComponent,
|
||||
ReactionNotificationPresenter: '' as AnyComponent,
|
||||
ActivityMessageNotificationLabel: '' as AnyComponent
|
||||
ActivityMessageNotificationLabel: '' as AnyComponent,
|
||||
ActivityReferencePresenter: '' as AnyComponent
|
||||
},
|
||||
ids: {
|
||||
AllFilter: '' as Ref<ActivityMessagesFilter>
|
||||
AllFilter: '' as Ref<ActivityMessagesFilter>,
|
||||
MentionNotification: '' as Ref<Doc>
|
||||
},
|
||||
backreference: {
|
||||
// Update list of back references
|
||||
Update: '' as Resource<(source: Doc, key: string, target: RelatedDocument[], label: IntlString) => Promise<void>>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { type Backlink } from '@hcengineering/chunter'
|
||||
import { type Data, type DocumentQuery, type TxOperations } from '@hcengineering/core'
|
||||
import chunter from './plugin'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function updateBacklinksList (
|
||||
client: TxOperations,
|
||||
q: DocumentQuery<Backlink>,
|
||||
backlinks: Array<Data<Backlink>>
|
||||
): Promise<void> {
|
||||
const current = await client.findAll(chunter.class.Backlink, q)
|
||||
|
||||
// We need to find ones we need to remove, and ones we need to update.
|
||||
for (const c of current) {
|
||||
// Find existing and check if we need to update message.
|
||||
const pos = backlinks.findIndex(
|
||||
(b) => b.backlinkId === c.backlinkId && b.backlinkClass === c.backlinkClass && b.attachedTo === c.attachedTo
|
||||
)
|
||||
if (pos !== -1) {
|
||||
// We need to check and update if required.
|
||||
const data = backlinks[pos]
|
||||
if (c.message !== data.message) {
|
||||
await client.updateCollection(c._class, c.space, c._id, c.attachedTo, c.attachedToClass, c.collection, {
|
||||
message: data.message
|
||||
})
|
||||
}
|
||||
backlinks.splice(pos, 1)
|
||||
} else {
|
||||
// We need to remove backlink.
|
||||
await client.removeCollection(c._class, c.space, c._id, c.attachedTo, c.attachedToClass, c.collection)
|
||||
}
|
||||
}
|
||||
// Add missing backlinks
|
||||
for (const backlink of backlinks) {
|
||||
const { attachedTo, attachedToClass, collection, ...adata } = backlink
|
||||
await client.addCollection(
|
||||
chunter.class.Backlink,
|
||||
chunter.space.Backlinks,
|
||||
attachedTo,
|
||||
attachedToClass,
|
||||
collection,
|
||||
adata
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
|
||||
this.metadataQuery.query(
|
||||
this.msgClass,
|
||||
{ attachedTo: this.chatId },
|
||||
{ attachedTo: this.chatId, hidden: { $ne: true } },
|
||||
(res) => {
|
||||
this.updatesDates(res)
|
||||
this.metadataStore.set(res)
|
||||
@@ -235,6 +235,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
this.msgClass,
|
||||
{
|
||||
attachedTo: this.chatId,
|
||||
hidden: { $ne: true },
|
||||
...(this.tailStart !== undefined ? { createdOn: { $gte: this.tailStart } } : {})
|
||||
},
|
||||
async (res) => {
|
||||
@@ -287,6 +288,7 @@ export class ChannelDataProvider implements IChannelDataProvider {
|
||||
chunter.class.ChatMessage,
|
||||
{
|
||||
attachedTo: this.chatId,
|
||||
hidden: { $ne: true },
|
||||
createdOn: isBackward
|
||||
? loadEqual
|
||||
? { $lte: loadAfter }
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Backlink } from '@hcengineering/chunter'
|
||||
import type { Doc } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import view, { ObjectPanel } from '@hcengineering/view'
|
||||
import { DocNavLink, getDocLinkTitle } from '@hcengineering/view-resources'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import { getCurrentAccount } from '@hcengineering/core'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
|
||||
import chunter from '../../plugin'
|
||||
import BacklinkReference from '../BacklinkReference.svelte'
|
||||
|
||||
export let value: Backlink | undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const currentAccount = getCurrentAccount() as PersonAccount
|
||||
|
||||
let docPanel: ObjectPanel | undefined
|
||||
let targetPanel: ObjectPanel | undefined
|
||||
|
||||
let targetTitle: string | undefined = undefined
|
||||
|
||||
const docQuery = createQuery()
|
||||
const targetQuery = createQuery()
|
||||
|
||||
let doc: Doc | undefined
|
||||
let target: Doc | undefined
|
||||
|
||||
$: value?.attachedToClass &&
|
||||
getDocLinkTitle(client, value.attachedTo, value.attachedToClass, target).then((res) => {
|
||||
targetTitle = res
|
||||
})
|
||||
|
||||
$: value !== undefined &&
|
||||
docQuery.query(value.backlinkClass, { _id: value.backlinkId }, (r) => {
|
||||
doc = r.shift()
|
||||
})
|
||||
|
||||
$: value?.attachedToClass &&
|
||||
targetQuery.query(value.attachedToClass, { _id: value.attachedTo }, (r) => {
|
||||
target = r.shift()
|
||||
})
|
||||
|
||||
$: targetPanel =
|
||||
value?.attachedToClass && hierarchy.classHierarchyMixin(value.attachedToClass, view.mixin.ObjectPanel)
|
||||
$: docPanel = value?.backlinkClass && hierarchy.classHierarchyMixin(value.backlinkClass, view.mixin.ObjectPanel)
|
||||
</script>
|
||||
|
||||
<span class="text-sm lower">
|
||||
<Label label={chunter.string.Mentioned} />
|
||||
</span>
|
||||
{#if target}
|
||||
<DocNavLink object={target} component={targetPanel?.component ?? view.component.EditDoc} shrink={0}>
|
||||
<span class="text-sm">
|
||||
{#if currentAccount.person === target._id}
|
||||
<Label label={chunter.string.You} />
|
||||
{:else}
|
||||
{targetTitle}
|
||||
{/if}
|
||||
</span>
|
||||
</DocNavLink>
|
||||
{/if}
|
||||
{#if doc && value}
|
||||
<span class="text-sm lower"><Label label={chunter.string.In} /></span>
|
||||
<DocNavLink object={doc} component={docPanel?.component ?? view.component.EditDoc} shrink={0}>
|
||||
<span class="text-sm"
|
||||
><BacklinkReference {value} inline={hierarchy.isDerived(doc._class, chunter.class.ChatMessage)} /></span
|
||||
>
|
||||
</DocNavLink>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
margin-left: 0.25rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,6 @@
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import { closeTooltip, Label, Lazy, Spinner, resizeObserver, MiniToggle } from '@hcengineering/ui'
|
||||
import { ObjectPresenter, DocNavLink } from '@hcengineering/view-resources'
|
||||
import { loadSavedMessages } from '@hcengineering/activity-resources'
|
||||
|
||||
import ChatMessageInput from './ChatMessageInput.svelte'
|
||||
import ChatMessagePresenter from './ChatMessagePresenter.svelte'
|
||||
@@ -53,10 +52,6 @@
|
||||
$: if (isTextMode) {
|
||||
dispatch('tooltip', { kind: 'popup' })
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadSavedMessages()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="commentPopup-container">
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
|
||||
import { NavigatorModel, SpecialNavModel } from '@hcengineering/workbench'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { loadSavedMessages } from '@hcengineering/activity-resources'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import ChatNavigator from './navigator/ChatNavigator.svelte'
|
||||
@@ -118,7 +117,6 @@
|
||||
])
|
||||
|
||||
onMount(() => {
|
||||
loadSavedMessages()
|
||||
loadSavedAttachments()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -95,7 +95,7 @@ export const chatNavGroupsModel: ChatNavGroupModel[] = [
|
||||
label: activity.string.Activity,
|
||||
query: {
|
||||
attachedToClass: {
|
||||
$nin: [chunter.class.DirectMessage, chunter.class.Channel, chunter.class.Backlink]
|
||||
$nin: [chunter.class.DirectMessage, chunter.class.Channel]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
import { Breadcrumbs, IconClose, Label, location as locationStore } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import activity, { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
|
||||
import { getMessageFromLoc, loadSavedMessages } from '@hcengineering/activity-resources'
|
||||
import { getMessageFromLoc } from '@hcengineering/activity-resources'
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
import chunter from '../../plugin'
|
||||
@@ -88,10 +88,6 @@
|
||||
{ label: chunter.string.Thread }
|
||||
]
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadSavedMessages()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="popupPanel panel">
|
||||
|
||||
@@ -14,25 +14,12 @@
|
||||
//
|
||||
|
||||
import { get, writable } from 'svelte/store'
|
||||
import chunter, {
|
||||
type Backlink,
|
||||
type Channel,
|
||||
type ChatMessage,
|
||||
chunterId,
|
||||
type DirectMessage
|
||||
} from '@hcengineering/chunter'
|
||||
import {
|
||||
type Data,
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
getCurrentAccount,
|
||||
type Ref,
|
||||
type RelatedDocument
|
||||
} from '@hcengineering/core'
|
||||
import { type IntlString, type Resources, translate } from '@hcengineering/platform'
|
||||
import chunter, { type Channel, type ChatMessage, chunterId, type DirectMessage } from '@hcengineering/chunter'
|
||||
import { getCurrentAccount, type Ref } from '@hcengineering/core'
|
||||
import { type Resources } from '@hcengineering/platform'
|
||||
import { MessageBox, getClient } from '@hcengineering/presentation'
|
||||
import { closePanel, getCurrentLocation, getLocation, navigate, showPopup } from '@hcengineering/ui'
|
||||
import activity, { type ActivityMessage, type DocUpdateMessage } from '@hcengineering/activity'
|
||||
import { type ActivityMessage } from '@hcengineering/activity'
|
||||
import notification, { type DocNotifyContext, inboxId } from '@hcengineering/notification'
|
||||
|
||||
import ChannelPresenter from './components/ChannelPresenter.svelte'
|
||||
@@ -50,9 +37,6 @@ import EditChannel from './components/EditChannel.svelte'
|
||||
import ChannelPreview from './components/ChannelPreview.svelte'
|
||||
import ThreadView from './components/threads/ThreadView.svelte'
|
||||
import ThreadViewPanel from './components/threads/ThreadViewPanel.svelte'
|
||||
import BacklinkContent from './components/BacklinkContent.svelte'
|
||||
import BacklinkReference from './components/BacklinkReference.svelte'
|
||||
import BacklinkCreatedLabel from './components/activity/BacklinkCreatedLabel.svelte'
|
||||
import ChatMessagePresenter from './components/chat-message/ChatMessagePresenter.svelte'
|
||||
import ChatMessageInput from './components/chat-message/ChatMessageInput.svelte'
|
||||
import ChatMessagesPresenter from './components/chat-message/ChatMessagesPresenter.svelte'
|
||||
@@ -70,7 +54,6 @@ import ChatAside from './components/chat/ChatAside.svelte'
|
||||
import Replies from './components/Replies.svelte'
|
||||
import ReplyToThreadAction from './components/ReplyToThreadAction.svelte'
|
||||
|
||||
import { updateBacklinksList } from './backlinks'
|
||||
import {
|
||||
ChannelTitleProvider,
|
||||
DirectTitleProvider,
|
||||
@@ -186,29 +169,6 @@ export async function chunterBrowserVisible (): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
async function update (source: Doc, key: string, target: RelatedDocument[], msg: IntlString): Promise<void> {
|
||||
const message = await translate(msg, {})
|
||||
const backlinks: Array<Data<Backlink>> = target.map((it) => ({
|
||||
backlinkId: source._id,
|
||||
backlinkClass: source._class,
|
||||
attachedTo: it._id,
|
||||
attachedToClass: it._class,
|
||||
message,
|
||||
collection: key
|
||||
}))
|
||||
|
||||
const q: DocumentQuery<Backlink> = { backlinkId: source._id, backlinkClass: source._class, collection: key }
|
||||
|
||||
await updateBacklinksList(getClient(), q, backlinks)
|
||||
}
|
||||
|
||||
export function backlinksFilter (message: ActivityMessage, _class?: Ref<Doc>): boolean {
|
||||
if (message._class === activity.class.DocUpdateMessage) {
|
||||
return (message as DocUpdateMessage).objectClass === chunter.class.Backlink
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function chatMessagesFilter (message: ActivityMessage): boolean {
|
||||
return message._class === chunter.class.ChatMessage
|
||||
}
|
||||
@@ -250,7 +210,6 @@ export async function replyToThread (message: ActivityMessage): Promise<void> {
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
filter: {
|
||||
BacklinksFilter: backlinksFilter,
|
||||
ChatMessagesFilter: chatMessagesFilter
|
||||
},
|
||||
component: {
|
||||
@@ -271,8 +230,6 @@ export default async (): Promise<Resources> => ({
|
||||
EditChannel,
|
||||
ThreadView,
|
||||
SavedMessages,
|
||||
BacklinkContent,
|
||||
BacklinkReference,
|
||||
ChatMessagePresenter,
|
||||
ChatMessageInput,
|
||||
ChatMessagesPresenter,
|
||||
@@ -301,9 +258,6 @@ export default async (): Promise<Resources> => ({
|
||||
GetUnreadThreadsCount: getUnreadThreadsCount,
|
||||
GetThreadLink: getThreadLink
|
||||
},
|
||||
activity: {
|
||||
BacklinkCreatedLabel
|
||||
},
|
||||
actionImpl: {
|
||||
ArchiveChannel,
|
||||
UnarchiveChannel,
|
||||
@@ -311,8 +265,5 @@ export default async (): Promise<Resources> => ({
|
||||
DeleteChatMessage: deleteChatMessage,
|
||||
OpenChannel,
|
||||
UnpinAllChannels
|
||||
},
|
||||
backreference: {
|
||||
Update: update
|
||||
}
|
||||
})
|
||||
|
||||
@@ -15,17 +15,7 @@
|
||||
|
||||
import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity'
|
||||
import type { Person } from '@hcengineering/contact'
|
||||
import type {
|
||||
Account,
|
||||
AttachedDoc,
|
||||
Class,
|
||||
Doc,
|
||||
Mixin,
|
||||
Ref,
|
||||
RelatedDocument,
|
||||
Space,
|
||||
Timestamp
|
||||
} from '@hcengineering/core'
|
||||
import type { Account, AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||
import { NotificationType } from '@hcengineering/notification'
|
||||
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
|
||||
import { IntlString, plugin } from '@hcengineering/platform'
|
||||
@@ -81,35 +71,6 @@ export interface Message extends ChunterMessage {
|
||||
lastReply?: Timestamp
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use ChatMessage instead
|
||||
*/
|
||||
// TODO: remove comment
|
||||
export interface Comment extends AttachedDoc {
|
||||
message: string
|
||||
attachments?: number
|
||||
reactions?: number
|
||||
pinned?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Backlink extends Comment {
|
||||
// A target document
|
||||
// attachedTo <- target document we point to
|
||||
// A target document class
|
||||
// attachedToClass
|
||||
|
||||
// Source document we have reference from, it should be parent document for Comment/Message.
|
||||
backlinkId: Ref<Doc>
|
||||
// Source document class
|
||||
backlinkClass: Ref<Class<Doc>>
|
||||
// Reference to comment documentId
|
||||
attachedDocId?: Ref<Doc>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -186,8 +147,6 @@ export default plugin(chunterId, {
|
||||
Message: '' as Ref<Class<Message>>,
|
||||
ChunterMessage: '' as Ref<Class<ChunterMessage>>,
|
||||
ThreadMessage: '' as Ref<Class<ThreadMessage>>,
|
||||
Backlink: '' as Ref<Class<Backlink>>,
|
||||
Comment: '' as Ref<Class<Comment>>,
|
||||
ChunterSpace: '' as Ref<Class<ChunterSpace>>,
|
||||
Channel: '' as Ref<Class<Channel>>,
|
||||
DirectMessage: '' as Ref<Class<DirectMessage>>,
|
||||
@@ -199,9 +158,6 @@ export default plugin(chunterId, {
|
||||
ChunterMessageExtension: '' as Ref<Mixin<ChunterMessageExtension>>,
|
||||
ObjectChatPanel: '' as Ref<Mixin<ObjectChatPanel>>
|
||||
},
|
||||
space: {
|
||||
Backlinks: '' as Ref<Space>
|
||||
},
|
||||
string: {
|
||||
Reactions: '' as IntlString,
|
||||
EditUpdate: '' as IntlString,
|
||||
@@ -238,7 +194,6 @@ export default plugin(chunterId, {
|
||||
},
|
||||
ids: {
|
||||
DMNotification: '' as Ref<NotificationType>,
|
||||
MentionNotification: '' as Ref<NotificationType>,
|
||||
ThreadNotification: '' as Ref<NotificationType>,
|
||||
ChannelNotification: '' as Ref<NotificationType>,
|
||||
ThreadMessageViewlet: '' as Ref<ChatMessageViewlet>
|
||||
@@ -246,10 +201,6 @@ export default plugin(chunterId, {
|
||||
app: {
|
||||
Chunter: '' as Ref<Doc>
|
||||
},
|
||||
backreference: {
|
||||
// Update list of back references
|
||||
Update: '' as Resource<(source: Doc, key: string, target: RelatedDocument[], label: IntlString) => Promise<void>>
|
||||
},
|
||||
action: {
|
||||
DeleteChatMessage: '' as Ref<Action>,
|
||||
OpenChannel: '' as Ref<Action>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import core, { Class, Data, Doc, Ref, TxOperations } from '@hcengineering/core'
|
||||
import core, { Ref, TxOperations } from '@hcengineering/core'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
|
||||
import chunter, { Backlink, DirectMessage } from '.'
|
||||
import chunter, { DirectMessage } from '.'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -28,84 +28,3 @@ export async function getDirectChannel (
|
||||
members: accIds
|
||||
})
|
||||
}
|
||||
|
||||
function extractBacklinks (
|
||||
backlinkId: Ref<Doc>,
|
||||
backlinkClass: Ref<Class<Doc>>,
|
||||
attachedDocId: Ref<Doc> | undefined,
|
||||
message: string,
|
||||
kids: NodeListOf<ChildNode>
|
||||
): Array<Data<Backlink>> {
|
||||
const result: Array<Data<Backlink>> = []
|
||||
|
||||
const nodes: Array<NodeListOf<ChildNode>> = [kids]
|
||||
while (true) {
|
||||
const nds = nodes.shift()
|
||||
if (nds === undefined) {
|
||||
break
|
||||
}
|
||||
nds.forEach((kid) => {
|
||||
if (
|
||||
kid.nodeType === Node.ELEMENT_NODE &&
|
||||
(kid as HTMLElement).localName === 'span' &&
|
||||
(kid as HTMLElement).getAttribute('data-type') === 'reference'
|
||||
) {
|
||||
const el = kid as HTMLElement
|
||||
const ato = el.getAttribute('data-id') as Ref<Doc>
|
||||
const atoClass = el.getAttribute('data-objectclass') as Ref<Class<Doc>>
|
||||
const e = result.find((e) => e.attachedTo === ato && e.attachedToClass === atoClass)
|
||||
if (e === undefined && ato !== attachedDocId && ato !== backlinkId) {
|
||||
result.push({
|
||||
attachedTo: ato,
|
||||
attachedToClass: atoClass,
|
||||
collection: 'backlinks',
|
||||
backlinkId,
|
||||
backlinkClass,
|
||||
message: el.parentElement?.innerHTML ?? '',
|
||||
attachedDocId
|
||||
})
|
||||
}
|
||||
}
|
||||
nodes.push(kid.childNodes)
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getBacklinks (
|
||||
backlinkId: Ref<Doc>,
|
||||
backlinkClass: Ref<Class<Doc>>,
|
||||
attachedDocId: Ref<Doc> | undefined,
|
||||
content: string
|
||||
): Array<Data<Backlink>> {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(content, 'text/html')
|
||||
return extractBacklinks(backlinkId, backlinkClass, attachedDocId, content, doc.childNodes as NodeListOf<HTMLElement>)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function createBacklinks (
|
||||
client: TxOperations,
|
||||
backlinkId: Ref<Doc>,
|
||||
backlinkClass: Ref<Class<Doc>>,
|
||||
attachedDocId: Ref<Doc> | undefined,
|
||||
content: string
|
||||
): Promise<void> {
|
||||
const backlinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
|
||||
for (const backlink of backlinks) {
|
||||
const { attachedTo, attachedToClass, collection, ...adata } = backlink
|
||||
await client.addCollection(
|
||||
chunter.class.Backlink,
|
||||
chunter.space.Backlinks,
|
||||
attachedTo,
|
||||
attachedToClass,
|
||||
collection,
|
||||
adata
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +300,9 @@ export const contactPlugin = plugin(contactId, {
|
||||
ContactName: '' as Ref<TemplateField>,
|
||||
ContactFirstName: '' as Ref<TemplateField>,
|
||||
ContactLastName: '' as Ref<TemplateField>
|
||||
},
|
||||
ids: {
|
||||
MentionCommonNotificationType: '' as Ref<Doc>
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<path d="M18.5,9.8c0.4,0,0.8-0.3,0.8-0.8V7.5c0-0.4-0.3-0.8-0.8-0.8s-0.8,0.3-0.8,0.8V9C17.8,9.4,18.1,9.8,18.5,9.8z"/>
|
||||
<path d="M13.5,6.8c-0.4,0-0.8,0.3-0.8,0.8V9c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8V7.5C14.2,7.1,13.9,6.8,13.5,6.8z"/>
|
||||
</symbol>
|
||||
<symbol id="backlinks" viewBox="0 0 32 32">
|
||||
<symbol id="references" viewBox="0 0 32 32">
|
||||
<path d="M29.25 6.75997C28.6926 6.20061 28.0302 5.75679 27.3009 5.45396C26.5716 5.15112 25.7897 4.99524 25 4.99524C24.2103 4.99524 23.4284 5.15112 22.6991 5.45396C22.278 5.62881 21.8792 5.85065 21.5101 6.1146C21.0614 6.43545 21.0693 7.07931 21.4594 7.46935C21.8499 7.85988 22.4827 7.84994 22.9575 7.5679C23.1218 7.47029 23.2933 7.38434 23.4707 7.31086C23.9571 7.10938 24.4785 7.00567 25.005 7.00567C25.5315 7.00567 26.0528 7.10938 26.5393 7.31086C27.0257 7.51235 27.4677 7.80767 27.84 8.17997C28.2123 8.55227 28.5076 8.99425 28.7091 9.48068C28.9106 9.96711 29.0143 10.4885 29.0143 11.015C29.0143 11.5415 28.9106 12.0628 28.7091 12.5493C28.5076 13.0357 28.2123 13.4777 27.84 13.85L19.84 21.85C19.0894 22.6019 18.0709 23.0248 17.0085 23.0257C15.9461 23.0267 14.9269 22.6055 14.175 21.855C13.4231 21.1044 13.0001 20.0859 12.9992 19.0235C12.9983 17.9611 13.4194 16.9419 14.17 16.19L14.8803 15.4746C15.2675 15.0846 15.2659 14.4537 14.8787 14.0637C14.4886 13.6709 13.8519 13.6681 13.4604 14.0596L12.75 14.77C12.1906 15.3274 11.7468 15.9897 11.444 16.7191C11.1411 17.4484 10.9852 18.2303 10.9852 19.02C10.9852 19.8097 11.1411 20.5916 11.444 21.3209C11.7468 22.0502 12.1906 22.7125 12.75 23.27C13.8815 24.387 15.41 25.0092 17 25C17.7927 25.0032 18.5782 24.8494 19.3111 24.5473C20.044 24.2452 20.7098 23.8009 21.27 23.24L29.27 15.24C30.3909 14.1123 31.0184 12.5858 31.0147 10.9958C31.0109 9.40582 30.3762 7.88232 29.25 6.75997Z" />
|
||||
<path d="M4.18997 24.82C3.81656 24.4483 3.52026 24.0065 3.31807 23.52C3.11589 23.0335 3.01181 22.5118 3.01181 21.985C3.01181 21.4581 3.11589 20.9365 3.31807 20.4499C3.52026 19.9634 3.81656 19.5216 4.18997 19.15L12.19 11.15C12.5616 10.7766 13.0034 10.4803 13.4899 10.2781C13.9765 10.0759 14.4981 9.97181 15.025 9.97181C15.5518 9.97181 16.0735 10.0759 16.56 10.2781C17.0465 10.4803 17.4883 10.7766 17.86 11.15C18.231 11.5246 18.5231 11.9698 18.7189 12.4594C18.9147 12.9489 19.0103 13.4728 19 14C19.003 14.5288 18.9012 15.0529 18.7004 15.5421C18.4995 16.0313 18.2037 16.4758 17.83 16.85L16.4072 18.2929C16.0213 18.6842 16.0289 19.3189 16.4175 19.7075C16.808 20.098 17.4466 20.1034 17.8371 19.7129L19.25 18.3C20.3785 17.1715 21.0124 15.6409 21.0124 14.045C21.0124 12.449 20.3785 10.9185 19.25 9.78997C18.1215 8.66147 16.5909 8.02749 14.995 8.02749C13.399 8.02749 11.8685 8.66147 10.74 9.78997L2.73997 17.79C2.17911 18.3476 1.73401 19.0106 1.43029 19.7408C1.12657 20.471 0.970215 21.2541 0.970215 22.045C0.970215 22.8358 1.12657 23.6189 1.43029 24.3492C1.73401 25.0794 2.17911 25.7424 2.73997 26.3C3.87879 27.4084 5.41087 28.0198 6.99997 28C8.26594 28.0012 9.49169 27.6069 10.5118 26.885C10.964 26.5651 10.961 25.921 10.5693 25.5293C10.1781 25.1381 9.54699 25.1518 9.0717 25.4348C8.90786 25.5324 8.73689 25.6184 8.56 25.6919C8.07349 25.8941 7.55182 25.9981 7.02497 25.9981C6.49812 25.9981 5.97645 25.8941 5.48994 25.6919C5.00342 25.4897 4.56164 25.1934 4.18997 24.82Z" />
|
||||
</symbol>
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -26,7 +26,7 @@ loadMetadata(document.icon, {
|
||||
Videofile: `${icons}#videofile`,
|
||||
Library: `${icons}#library`,
|
||||
Teamspace: `${icons}#teamspace`,
|
||||
Backlinks: `${icons}#backlinks`,
|
||||
References: `${icons}#references`,
|
||||
History: `${icons}#history`,
|
||||
Star: `${icons}#star`,
|
||||
Starred: `${icons}#starred`
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
"svelte-eslint-parser": "^0.33.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/activity": "^0.6.0",
|
||||
"@hcengineering/activity-resources": "^0.6.1",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"svelte": "^4.2.5",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
@@ -50,7 +52,6 @@
|
||||
"@hcengineering/attachment": "^0.6.9",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
"@hcengineering/login": "^0.6.8",
|
||||
"@hcengineering/chunter": "^0.6.12",
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/contact-resources": "^0.6.0",
|
||||
"@hcengineering/preference": "^0.6.9",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
attachments: 0,
|
||||
labels: 0,
|
||||
comments: 0,
|
||||
backlinks: 0
|
||||
references: 0
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
import DocumentEditor from './DocumentEditor.svelte'
|
||||
import DocumentPresenter from './DocumentPresenter.svelte'
|
||||
import DocumentTitle from './DocumentTitle.svelte'
|
||||
import Backlinks from './sidebar/Backlinks.svelte'
|
||||
import References from './sidebar/References.svelte'
|
||||
import History from './sidebar/History.svelte'
|
||||
|
||||
export let _id: Ref<Document>
|
||||
@@ -182,8 +182,8 @@
|
||||
|
||||
const aside: ButtonItem[] = [
|
||||
{
|
||||
id: 'backlinks',
|
||||
icon: document.icon.Backlinks
|
||||
id: 'references',
|
||||
icon: document.icon.References
|
||||
},
|
||||
{
|
||||
id: 'history',
|
||||
@@ -339,8 +339,8 @@
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
{#if selectedAside === 'backlinks'}
|
||||
<Backlinks doc={doc._id} />
|
||||
{#if selectedAside === 'references'}
|
||||
<References doc={doc._id} />
|
||||
{:else if selectedAside === 'history'}
|
||||
<History value={doc} {readonly} />
|
||||
{/if}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<!--
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Backlink } from '@hcengineering/chunter'
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
import { TimeSince } from '@hcengineering/ui'
|
||||
import { ObjectPresenter } from '@hcengineering/view-resources'
|
||||
|
||||
export let value: Backlink
|
||||
</script>
|
||||
|
||||
<div class="container flex-col flex-gap-2 flex-no-shrink">
|
||||
<div class="flex-between h-8">
|
||||
<div class="fs-bold overflow-label">
|
||||
<ObjectPresenter _class={value.backlinkClass} objectId={value.backlinkId} accent />
|
||||
</div>
|
||||
<div class="time">
|
||||
<TimeSince value={value.createdOn ?? value.modifiedOn} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-no-shrink">
|
||||
<MessageViewer message={value.message} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid var(--theme-divider-color);
|
||||
}
|
||||
.time {
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-trans-color);
|
||||
}
|
||||
</style>
|
||||
+9
-9
@@ -15,28 +15,28 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import chunter, { Backlink } from '@hcengineering/chunter'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Document } from '@hcengineering/document'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Label, Lazy, Scroller } from '@hcengineering/ui'
|
||||
import activity, { ActivityReference } from '@hcengineering/activity'
|
||||
import { ActivityReferencePresenter } from '@hcengineering/activity-resources'
|
||||
|
||||
import document from '../../plugin'
|
||||
import BacklinkView from './BacklinkView.svelte'
|
||||
|
||||
export let doc: Ref<Document>
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
let backlinks: Backlink[] = []
|
||||
let references: ActivityReference[] = []
|
||||
|
||||
$: query.query(
|
||||
chunter.class.Backlink,
|
||||
activity.class.ActivityReference,
|
||||
{
|
||||
attachedTo: doc
|
||||
},
|
||||
(res) => {
|
||||
backlinks = res
|
||||
references = res
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -48,11 +48,11 @@
|
||||
|
||||
<div class="divider" />
|
||||
|
||||
{#if backlinks.length > 0}
|
||||
<Scroller>
|
||||
{#each backlinks as backlink}
|
||||
{#if references.length > 0}
|
||||
<Scroller padding="0.75rem 0.25rem">
|
||||
{#each references as reference}
|
||||
<Lazy>
|
||||
<BacklinkView value={backlink} />
|
||||
<ActivityReferencePresenter value={reference} hoverStyles="filledHover" />
|
||||
</Lazy>
|
||||
{/each}
|
||||
</Scroller>
|
||||
@@ -47,7 +47,7 @@ export async function createEmptyDocument (
|
||||
embeddings: 0,
|
||||
labels: 0,
|
||||
comments: 0,
|
||||
backlinks: 0,
|
||||
references: 0,
|
||||
...data
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface Document extends AttachedDoc<Document, 'children', Teamspace>,
|
||||
comments?: number
|
||||
embeddings?: number
|
||||
labels?: number
|
||||
backlinks?: number
|
||||
references?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +105,7 @@ const documentPlugin = plugin(documentId, {
|
||||
Videofile: '' as Asset,
|
||||
Library: '' as Asset,
|
||||
Teamspace: '' as Asset,
|
||||
Backlinks: '' as Asset,
|
||||
References: '' as Asset,
|
||||
History: '' as Asset,
|
||||
Star: '' as Asset,
|
||||
Starred: '' as Asset
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
NotificationProvider,
|
||||
NotificationSetting,
|
||||
NotificationStatus,
|
||||
NotificationType,
|
||||
Notification as PlatformNotification
|
||||
Notification as PlatformNotification,
|
||||
BaseNotificationType
|
||||
} from '@hcengineering/notification'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { getCurrentLocation, showPanel } from '@hcengineering/ui'
|
||||
@@ -32,7 +32,10 @@
|
||||
const providersQuery = createQuery()
|
||||
|
||||
let settingsReceived = false
|
||||
let settings: Map<Ref<NotificationType>, NotificationSetting> = new Map<Ref<NotificationType>, NotificationSetting>()
|
||||
let settings: Map<Ref<BaseNotificationType>, NotificationSetting> = new Map<
|
||||
Ref<BaseNotificationType>,
|
||||
NotificationSetting
|
||||
>()
|
||||
let provider: NotificationProvider | undefined
|
||||
|
||||
$: enabled = 'Notification' in window && Notification?.permission !== 'denied'
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { IdMap, Ref, toIdMap } from '@hcengineering/core'
|
||||
import type {
|
||||
BaseNotificationType,
|
||||
NotificationGroup,
|
||||
NotificationProvider,
|
||||
NotificationSetting,
|
||||
@@ -26,18 +27,18 @@
|
||||
import notification from '../plugin'
|
||||
|
||||
export let group: Ref<NotificationGroup>
|
||||
export let settings: Map<Ref<NotificationType>, NotificationSetting[]>
|
||||
export let settings: Map<Ref<BaseNotificationType>, NotificationSetting[]>
|
||||
|
||||
const client = getClient()
|
||||
let types: NotificationType[] = []
|
||||
let typesMap: IdMap<NotificationType> = new Map()
|
||||
let types: BaseNotificationType[] = []
|
||||
let typesMap: IdMap<BaseNotificationType> = new Map()
|
||||
let providers: NotificationProvider[] = []
|
||||
let providersMap: IdMap<NotificationProvider> = new Map()
|
||||
|
||||
load()
|
||||
void load()
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(notification.class.NotificationType, { group }, (res) => {
|
||||
$: query.query(notification.class.BaseNotificationType, { group }, (res) => {
|
||||
types = res
|
||||
typesMap = toIdMap(types)
|
||||
})
|
||||
@@ -50,8 +51,8 @@
|
||||
$: column = providers.length + 1
|
||||
|
||||
function getStatus (
|
||||
settings: Map<Ref<NotificationType>, NotificationSetting[]>,
|
||||
type: Ref<NotificationType>,
|
||||
settings: Map<Ref<BaseNotificationType>, NotificationSetting[]>,
|
||||
type: Ref<BaseNotificationType>,
|
||||
provider: Ref<NotificationProvider>
|
||||
): boolean {
|
||||
const setting = getSetting(settings, type, provider)
|
||||
@@ -63,14 +64,14 @@
|
||||
return typeValue?.providers?.[provider] ?? false
|
||||
}
|
||||
|
||||
function createHandler (type: Ref<NotificationType>, provider: Ref<NotificationProvider>): (evt: any) => void {
|
||||
function createHandler (type: Ref<BaseNotificationType>, provider: Ref<NotificationProvider>): (evt: any) => void {
|
||||
return (evt: any) => {
|
||||
void change(type, provider, evt.detail)
|
||||
}
|
||||
}
|
||||
|
||||
async function change (
|
||||
type: Ref<NotificationType>,
|
||||
type: Ref<BaseNotificationType>,
|
||||
provider: Ref<NotificationProvider>,
|
||||
value: boolean
|
||||
): Promise<void> {
|
||||
@@ -89,8 +90,8 @@
|
||||
}
|
||||
|
||||
function getSetting (
|
||||
map: Map<Ref<NotificationType>, NotificationSetting[]>,
|
||||
type: Ref<NotificationType>,
|
||||
map: Map<Ref<BaseNotificationType>, NotificationSetting[]>,
|
||||
type: Ref<BaseNotificationType>,
|
||||
provider: Ref<NotificationProvider>
|
||||
): NotificationSetting | undefined {
|
||||
const typeMap = map.get(type)
|
||||
@@ -98,10 +99,15 @@
|
||||
return typeMap.find((p) => p.attachedTo === provider)
|
||||
}
|
||||
|
||||
function getLabel (type: NotificationType): IntlString {
|
||||
if (type.attachedToClass !== undefined) {
|
||||
const isNotificationType = (type: BaseNotificationType): type is NotificationType => {
|
||||
return type._class === notification.class.NotificationType
|
||||
}
|
||||
|
||||
function getLabel (type: BaseNotificationType): IntlString {
|
||||
if (isNotificationType(type) && type.attachedToClass !== undefined) {
|
||||
return notification.string.AddedRemoved
|
||||
}
|
||||
|
||||
return notification.string.Change
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import type {
|
||||
BaseNotificationType,
|
||||
NotificationGroup,
|
||||
NotificationPreferencesGroup,
|
||||
NotificationSetting,
|
||||
NotificationType
|
||||
NotificationSetting
|
||||
} from '@hcengineering/notification'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
@@ -51,7 +51,7 @@
|
||||
groups = res
|
||||
})
|
||||
|
||||
let settings = new Map<Ref<NotificationType>, NotificationSetting[]>()
|
||||
let settings = new Map<Ref<BaseNotificationType>, NotificationSetting[]>()
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
|
||||
+10
-8
@@ -23,7 +23,7 @@
|
||||
EmployeePresenter
|
||||
} from '@hcengineering/contact-resources'
|
||||
import core, { Doc, getDisplayTime, Ref } from '@hcengineering/core'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import notification, { CommonInboxNotification } from '@hcengineering/notification'
|
||||
import { ActionIcon, IconMoreH, Label, showPopup } from '@hcengineering/ui'
|
||||
@@ -61,13 +61,15 @@
|
||||
object = result[0]
|
||||
})
|
||||
|
||||
$: void translate(value.message, value.props)
|
||||
.then((message) => {
|
||||
content = message
|
||||
})
|
||||
.catch((err) => {
|
||||
content = JSON.stringify(err, null, 2)
|
||||
})
|
||||
$: void updateContent(value.message, value.messageHtml)
|
||||
|
||||
async function updateContent (message?: IntlString, messageHtml?: string): Promise<void> {
|
||||
if (messageHtml !== undefined) {
|
||||
content = messageHtml
|
||||
} else if (message !== undefined) {
|
||||
content = await translate(message, value.props)
|
||||
}
|
||||
}
|
||||
|
||||
function handleActionMenuOpened (): void {
|
||||
isActionMenuOpened = true
|
||||
|
||||
@@ -41,8 +41,7 @@
|
||||
import { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { ViewletSelector } from '@hcengineering/view-resources'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import { isReactionMessage, loadSavedMessages } from '@hcengineering/activity-resources'
|
||||
import { onMount } from 'svelte'
|
||||
import { isReactionMessage } from '@hcengineering/activity-resources'
|
||||
|
||||
import { inboxMessagesStore, InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
||||
import Filter from '../Filter.svelte'
|
||||
@@ -256,10 +255,6 @@
|
||||
{ size: 'auto', minSize: 30, maxSize: 'auto', float: undefined }
|
||||
])
|
||||
|
||||
onMount(() => {
|
||||
loadSavedMessages()
|
||||
})
|
||||
|
||||
function archiveAll (): void {
|
||||
showPopup(
|
||||
MessageBox,
|
||||
|
||||
@@ -96,18 +96,24 @@ export interface NotificationContent {
|
||||
intlParamsNotLocalized?: Record<string, IntlString>
|
||||
}
|
||||
|
||||
export interface BaseNotificationType extends Doc {
|
||||
label: IntlString
|
||||
// Is autogenerated
|
||||
generated: boolean
|
||||
// allowed to change setting (probably we should show it, but disable toggle??)
|
||||
hidden: boolean
|
||||
group: Ref<NotificationGroup>
|
||||
// allowed providers and default value for it
|
||||
providers: Record<Ref<NotificationProvider>, boolean>
|
||||
// templates for email (and browser/push?)
|
||||
templates?: NotificationTemplate
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface NotificationType extends Doc {
|
||||
export interface NotificationType extends BaseNotificationType {
|
||||
// For show/hide with attributes
|
||||
attribute?: Ref<AnyAttribute>
|
||||
// Is autogenerated
|
||||
generated: boolean
|
||||
// allowed to to change setting (probably we should show it, but disable toggle??)
|
||||
hidden: boolean
|
||||
label: IntlString
|
||||
group: Ref<NotificationGroup>
|
||||
txClasses: Ref<Class<Tx>>[]
|
||||
objectClass: Ref<Class<Doc>>
|
||||
// not allowed to parent doc
|
||||
@@ -119,14 +125,12 @@ export interface NotificationType extends Doc {
|
||||
txMatch?: DocumentQuery<Tx>
|
||||
// use for space collaborators, not object
|
||||
spaceSubscribe?: boolean
|
||||
// allowed providers and default value for it
|
||||
providers: Record<Ref<NotificationProvider>, boolean>
|
||||
// templates for email (and browser/push?)
|
||||
templates?: NotificationTemplate
|
||||
// when true notification will be created for user which trigger it (default - false)
|
||||
allowedForAuthor?: boolean
|
||||
}
|
||||
|
||||
export interface CommonNotificationType extends BaseNotificationType {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -139,7 +143,7 @@ export interface NotificationProvider extends Doc {
|
||||
*/
|
||||
export interface NotificationSetting extends Preference {
|
||||
attachedTo: Ref<NotificationProvider>
|
||||
type: Ref<NotificationType>
|
||||
type: Ref<BaseNotificationType>
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
@@ -239,7 +243,8 @@ export interface ActivityInboxNotification extends InboxNotification {
|
||||
|
||||
export interface CommonInboxNotification extends InboxNotification {
|
||||
header?: IntlString
|
||||
message: IntlString
|
||||
message?: IntlString
|
||||
messageHtml?: string
|
||||
props?: Record<string, any>
|
||||
icon?: Asset
|
||||
iconProps?: Record<string, any>
|
||||
@@ -312,7 +317,9 @@ const notification = plugin(notificationId, {
|
||||
},
|
||||
class: {
|
||||
Notification: '' as Ref<Class<Notification>>,
|
||||
BaseNotificationType: '' as Ref<Class<BaseNotificationType>>,
|
||||
NotificationType: '' as Ref<Class<NotificationType>>,
|
||||
CommonNotificationType: '' as Ref<Class<CommonNotificationType>>,
|
||||
NotificationProvider: '' as Ref<Class<NotificationProvider>>,
|
||||
NotificationSetting: '' as Ref<Class<NotificationSetting>>,
|
||||
DocUpdates: '' as Ref<Class<DocUpdates>>,
|
||||
@@ -327,7 +334,8 @@ const notification = plugin(notificationId, {
|
||||
ids: {
|
||||
NotificationSettings: '' as Ref<Doc>,
|
||||
NotificationGroup: '' as Ref<NotificationGroup>,
|
||||
CollaboratoAddNotification: '' as Ref<NotificationType>
|
||||
CollaboratoAddNotification: '' as Ref<NotificationType>,
|
||||
MentionCommonNotificationType: '' as Ref<CommonNotificationType>
|
||||
},
|
||||
providers: {
|
||||
PlatformNotification: '' as Ref<NotificationProvider>,
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
await descriptionBox.createAttachments()
|
||||
|
||||
if (_comment.trim().length > 0) {
|
||||
await client.addCollection(chunter.class.Comment, _space, doc._id, recruit.class.Applicant, 'comments', {
|
||||
await client.addCollection(chunter.class.ChatMessage, _space, doc._id, recruit.class.Applicant, 'comments', {
|
||||
message: _comment
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,13 +14,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||
import chunter, { Comment } from '@hcengineering/chunter'
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import { AttachedData, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Request, RequestStatus } from '@hcengineering/request'
|
||||
import type { RefAction } from '@hcengineering/text-editor'
|
||||
import { Button } from '@hcengineering/ui'
|
||||
|
||||
import request from '../plugin'
|
||||
import Comments from './icons/Comments.svelte'
|
||||
import DocFail from './icons/DocFail.svelte'
|
||||
@@ -55,25 +56,32 @@
|
||||
let message: string = ''
|
||||
let attachments: number | undefined = 0
|
||||
|
||||
async function onUpdate (event: CustomEvent<AttachedData<Comment>>) {
|
||||
async function onUpdate (event: CustomEvent<AttachedData<ChatMessage>>) {
|
||||
message = event.detail.message
|
||||
attachments = event.detail.attachments
|
||||
}
|
||||
|
||||
async function saveComment (): Promise<void> {
|
||||
const _id = await client.addCollection(chunter.class.Comment, value.space, value._id, value._class, 'comments', {
|
||||
message,
|
||||
attachments
|
||||
})
|
||||
const _id = await client.addCollection(
|
||||
chunter.class.ChatMessage,
|
||||
value.space,
|
||||
value._id,
|
||||
value._class,
|
||||
'comments',
|
||||
{
|
||||
message,
|
||||
attachments
|
||||
}
|
||||
)
|
||||
|
||||
await client.createMixin(_id, chunter.class.Comment, value.space, request.mixin.RequestDecisionComment, {})
|
||||
await client.createMixin(_id, chunter.class.ChatMessage, value.space, request.mixin.RequestDecisionComment, {})
|
||||
|
||||
refInput.createAttachments()
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function comment (): Promise<void> {
|
||||
await client.addCollection(chunter.class.Comment, value.space, value._id, value._class, 'comments', {
|
||||
await client.addCollection(chunter.class.ChatMessage, value.space, value._id, value._class, 'comments', {
|
||||
message,
|
||||
attachments
|
||||
})
|
||||
|
||||
@@ -73,6 +73,8 @@
|
||||
import view from '@hcengineering/view'
|
||||
import { ObjectBox } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import activity from '@hcengineering/activity'
|
||||
|
||||
import { activeComponent, activeMilestone, generateIssueShortLink, updateIssueRelation } from '../issues'
|
||||
import tracker from '../plugin'
|
||||
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
|
||||
@@ -485,7 +487,7 @@
|
||||
if (client.getHierarchy().isDerived(relatedTo._class, tracker.class.Issue)) {
|
||||
await updateIssueRelation(operations, relatedTo as Issue, doc, 'relations', '$push')
|
||||
} else {
|
||||
const update = await getResource(chunter.backreference.Update)
|
||||
const update = await getResource(activity.backreference.Update)
|
||||
await update(doc, 'relations', [relatedTo], tracker.string.AddedReference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { Class, Doc, Ref, RelatedDocument } from '@hcengineering/core'
|
||||
import { getResource, IntlString } from '@hcengineering/platform'
|
||||
import { createQuery, getClient, ObjectSearchPopup, ObjectSearchResult } from '@hcengineering/presentation'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { Action, closePopup, Menu, showPopup } from '@hcengineering/ui'
|
||||
import activity from '@hcengineering/activity'
|
||||
|
||||
import { updateIssueRelation } from '../issues'
|
||||
import tracker from '../plugin'
|
||||
|
||||
@@ -40,7 +41,7 @@
|
||||
|
||||
return
|
||||
}
|
||||
const update = await getResource(chunter.backreference.Update)
|
||||
const update = await getResource(activity.backreference.Update)
|
||||
|
||||
let docs: RelatedDocument[] = []
|
||||
let label: IntlString = tracker.string.RemoveRelation
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { Component, Icon, IconClose, navigate } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import activity from '@hcengineering/activity'
|
||||
|
||||
import { issueLinkFragmentProvider, updateIssueRelation } from '../../issues'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
@@ -43,7 +45,7 @@
|
||||
await updateIssueRelation(client, issueDoc, value, prop, '$pull')
|
||||
}
|
||||
|
||||
const update = await getResource(chunter.backreference.Update)
|
||||
const update = await getResource(activity.backreference.Update)
|
||||
|
||||
let docs: RelatedDocument[] = []
|
||||
let label: IntlString = tracker.string.RemoveRelation
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/server-activity": "^0.6.0",
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/server-notification-resources": "^0.6.0"
|
||||
"@hcengineering/server-notification-resources": "^0.6.0",
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"@hcengineering/collaboration": "^0.6.0",
|
||||
"@hcengineering/contact": "^0.6.20"
|
||||
}
|
||||
}
|
||||
|
||||
+24
-20
@@ -14,44 +14,47 @@
|
||||
//
|
||||
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { getBacklinks } from '../backlinks'
|
||||
|
||||
import { getReferencesData } from '../references'
|
||||
|
||||
describe('extractBacklinks', () => {
|
||||
it('should return no backlinks for empty document', () => {
|
||||
it('should return no references for empty document', () => {
|
||||
const content = '<p></p>'
|
||||
|
||||
const backlinks = getBacklinks(
|
||||
'backlinkId' as Ref<Doc>,
|
||||
'backlinkClass' as Ref<Class<Doc>>,
|
||||
const references = getReferencesData(
|
||||
'srcDocId' as Ref<Doc>,
|
||||
'srcDocClass' as Ref<Class<Doc>>,
|
||||
'attachedDocId' as Ref<Doc>,
|
||||
'attachedDocClass' as Ref<Class<Doc>>,
|
||||
content
|
||||
)
|
||||
|
||||
expect(backlinks).toEqual([])
|
||||
expect(references).toEqual([])
|
||||
})
|
||||
|
||||
it('should parse single backlink', () => {
|
||||
const content =
|
||||
'<p>hello <span class="reference" data-type="reference" data-id="id" data-objectclass="contact:class:Person" data-label="Appleseed John">@Appleseed John</span> </p>'
|
||||
|
||||
const backlinks = getBacklinks(
|
||||
'backlinkId' as Ref<Doc>,
|
||||
'backlinkClass' as Ref<Class<Doc>>,
|
||||
const references = getReferencesData(
|
||||
'srcDocId' as Ref<Doc>,
|
||||
'srcDocClass' as Ref<Class<Doc>>,
|
||||
'attachedDocId' as Ref<Doc>,
|
||||
'attachedDocClass' as Ref<Class<Doc>>,
|
||||
content
|
||||
)
|
||||
|
||||
expect(backlinks.length).toBe(1)
|
||||
expect(backlinks).toEqual([
|
||||
expect(references.length).toBe(1)
|
||||
expect(references).toEqual([
|
||||
{
|
||||
attachedTo: 'id',
|
||||
attachedToClass: 'contact:class:Person',
|
||||
collection: 'backlinks',
|
||||
backlinkId: 'backlinkId',
|
||||
backlinkClass: 'backlinkClass',
|
||||
collection: 'references',
|
||||
srcDocId: 'srcDocId',
|
||||
srcDocClass: 'srcDocClass',
|
||||
message:
|
||||
'hello <span data-type="reference" data-id="id" data-objectclass="contact:class:Person" data-label="Appleseed John" class="reference">@Appleseed John</span>',
|
||||
attachedDocId: 'attachedDocId'
|
||||
attachedDocId: 'attachedDocId',
|
||||
attachedDocClass: 'attachedDocClass'
|
||||
}
|
||||
])
|
||||
})
|
||||
@@ -60,13 +63,14 @@ describe('extractBacklinks', () => {
|
||||
const content =
|
||||
'<p><span class="reference" data-type="reference" data-id="id" data-label="Appleseed John" data-objectclass="contact:class:Person">@Appleseed John</span> <span data-type="reference" class="reference" data-id="id" data-label="Appleseed John" data-objectclass="contact:class:Person">@Appleseed John</span> </p>'
|
||||
|
||||
const backlinks = getBacklinks(
|
||||
'backlinkId' as Ref<Doc>,
|
||||
'backlinkClass' as Ref<Class<Doc>>,
|
||||
const references = getReferencesData(
|
||||
'srcDocId' as Ref<Doc>,
|
||||
'srcDocClass' as Ref<Class<Doc>>,
|
||||
'attachedDocId' as Ref<Doc>,
|
||||
'attachedDocClass' as Ref<Class<Doc>>,
|
||||
content
|
||||
)
|
||||
|
||||
expect(backlinks.length).toBe(1)
|
||||
expect(references.length).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
|
||||
import { getDocUpdateAction, getTxAttributesUpdates } from './utils'
|
||||
import { ReferenceTrigger } from './references'
|
||||
|
||||
export async function OnReactionChanged (originTx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const tx = originTx as TxCollectionCUD<ActivityMessage, Reaction>
|
||||
@@ -60,14 +61,26 @@ export async function removeReactionNotifications (
|
||||
control: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
const message = (
|
||||
await control.findAll(activity.class.ActivityMessage, { objectId: tx.tx.objectId }, { projection: { _id: 1 } })
|
||||
await control.findAll(
|
||||
activity.class.ActivityMessage,
|
||||
{ objectId: tx.tx.objectId },
|
||||
{ projection: { _id: 1, _class: 1, space: 1 } }
|
||||
)
|
||||
)[0]
|
||||
|
||||
if (message === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
return await removeDocInboxNotifications(message._id, control)
|
||||
const res: Tx[] = []
|
||||
const txes = await removeDocInboxNotifications(message._id, control)
|
||||
|
||||
const removeTx = control.txFactory.createTxRemoveDoc(message._class, message.space, message._id)
|
||||
|
||||
res.push(removeTx)
|
||||
res.push(...txes)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
export async function createReactionNotifications (
|
||||
@@ -384,6 +397,7 @@ async function OnDocRemoved (originTx: TxCUD<Doc>, control: TriggerControl): Pro
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
ReferenceTrigger,
|
||||
ActivityMessagesHandler,
|
||||
OnDocRemoved,
|
||||
OnReactionChanged
|
||||
|
||||
@@ -0,0 +1,558 @@
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { loadCollaborativeDoc, yDocToBuffer } from '@hcengineering/collaboration'
|
||||
import core, {
|
||||
Account,
|
||||
AttachedDoc,
|
||||
Class,
|
||||
CollaborativeDoc,
|
||||
Data,
|
||||
Doc,
|
||||
Hierarchy,
|
||||
Ref,
|
||||
Space,
|
||||
Tx,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxFactory,
|
||||
TxMixin,
|
||||
TxProcessor,
|
||||
TxRemoveDoc,
|
||||
TxUpdateDoc,
|
||||
Type
|
||||
} from '@hcengineering/core'
|
||||
import notification, { CommonInboxNotification } from '@hcengineering/notification'
|
||||
import { ServerKit, extractReferences, getHTML, parseHTML, yDocContentToNodes } from '@hcengineering/text'
|
||||
import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
|
||||
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
|
||||
import contact, { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import {
|
||||
getPushCollaboratorTx,
|
||||
getCommonNotificationTxes,
|
||||
isMessageAlreadyNotified,
|
||||
shouldNotifyCommon
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
|
||||
const extensions = [ServerKit]
|
||||
|
||||
export async function getPersonNotificationTxes (
|
||||
reference: Data<ActivityReference>,
|
||||
control: TriggerControl,
|
||||
senderId: Ref<Account>,
|
||||
space: Ref<Space>,
|
||||
originTx: TxCUD<Doc>
|
||||
): Promise<Tx[]> {
|
||||
if (reference.attachedTo === senderId) {
|
||||
return []
|
||||
}
|
||||
|
||||
const receiver = (
|
||||
await control.modelDb.findAll(
|
||||
contact.class.PersonAccount,
|
||||
{
|
||||
person: reference.attachedTo as Ref<Person>
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
|
||||
if (receiver === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const isAvailable = await isSpaceAvailable(receiver, space, control)
|
||||
|
||||
if (!isAvailable) {
|
||||
return []
|
||||
}
|
||||
|
||||
const res: Tx[] = []
|
||||
const collaboratorsTx = await getCollaboratorsTxes(reference, control, receiver)
|
||||
|
||||
if (collaboratorsTx !== undefined) {
|
||||
res.push(collaboratorsTx)
|
||||
}
|
||||
|
||||
if (await isReferenceAlreadyNotified(reference, receiver._id, control)) {
|
||||
return res
|
||||
}
|
||||
|
||||
const doc = (await control.findAll(reference.srcDocClass, { _id: reference.srcDocId }))[0]
|
||||
|
||||
if (doc === undefined) {
|
||||
return res
|
||||
}
|
||||
|
||||
const data: Partial<Data<CommonInboxNotification>> = {
|
||||
header: activity.string.MentionedYouIn,
|
||||
messageHtml: reference.message
|
||||
}
|
||||
|
||||
const notifyResult = await shouldNotifyCommon(control, receiver._id, notification.ids.MentionCommonNotificationType)
|
||||
const texes = await getCommonNotificationTxes(
|
||||
control,
|
||||
doc,
|
||||
data,
|
||||
receiver._id,
|
||||
senderId,
|
||||
reference.srcDocId,
|
||||
reference.srcDocClass,
|
||||
space,
|
||||
originTx.modifiedOn,
|
||||
notifyResult
|
||||
)
|
||||
|
||||
res.push(...texes)
|
||||
return res
|
||||
}
|
||||
|
||||
async function isSpaceAvailable (user: PersonAccount, spaceId: Ref<Space>, control: TriggerControl): Promise<boolean> {
|
||||
const space = (await control.findAll<Space>(core.class.Space, { _id: spaceId }))[0]
|
||||
|
||||
if (!space?.private) {
|
||||
return true
|
||||
}
|
||||
|
||||
return space.members.includes(user._id)
|
||||
}
|
||||
|
||||
async function getCollaboratorsTxes (
|
||||
reference: Data<ActivityReference>,
|
||||
control: TriggerControl,
|
||||
receiver: Account
|
||||
): Promise<TxMixin<Doc, Doc> | undefined> {
|
||||
const { hierarchy } = control
|
||||
|
||||
if (reference.attachedDocClass === undefined || reference.attachedDocId === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!hierarchy.isDerived(reference.attachedDocClass, activity.class.ActivityMessage)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const message = (
|
||||
await control.findAll<ActivityMessage>(
|
||||
reference.attachedDocClass,
|
||||
{
|
||||
_id: reference.attachedDocId as Ref<ActivityMessage>
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
|
||||
if (message === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return getPushCollaboratorTx(control, receiver._id, message)
|
||||
}
|
||||
|
||||
async function isReferenceAlreadyNotified (
|
||||
reference: Data<ActivityReference>,
|
||||
receiver: Ref<Account>,
|
||||
control: TriggerControl
|
||||
): Promise<boolean> {
|
||||
const { hierarchy } = control
|
||||
|
||||
if (reference.attachedDocClass === undefined || reference.attachedDocId === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!hierarchy.isDerived(reference.attachedDocClass, activity.class.ActivityMessage)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return await isMessageAlreadyNotified(reference.attachedDocId as Ref<ActivityMessage>, receiver, control)
|
||||
}
|
||||
|
||||
function isMarkupType (type: Ref<Class<Type<any>>>): boolean {
|
||||
return type === core.class.TypeMarkup || type === core.class.TypeCollaborativeMarkup
|
||||
}
|
||||
|
||||
function isCollaborativeType (type: Ref<Class<Type<any>>>): boolean {
|
||||
return type === core.class.TypeCollaborativeDoc
|
||||
}
|
||||
|
||||
async function getCreateReferencesTxes (
|
||||
control: TriggerControl,
|
||||
storage: StorageAdapter,
|
||||
txFactory: TxFactory,
|
||||
createdDoc: Doc,
|
||||
srcDocId: Ref<Doc>,
|
||||
srcDocClass: Ref<Class<Doc>>,
|
||||
srcDocSpace: Ref<Space>,
|
||||
originTx: TxCUD<Doc>
|
||||
): Promise<Tx[]> {
|
||||
const attachedDocId = createdDoc._id
|
||||
const attachedDocClass = createdDoc._class
|
||||
|
||||
const refs: Data<ActivityReference>[] = []
|
||||
const attributes = control.hierarchy.getAllAttributes(createdDoc._class)
|
||||
|
||||
for (const attr of attributes.values()) {
|
||||
if (isMarkupType(attr.type._class)) {
|
||||
const content = (createdDoc as any)[attr.name]?.toString() ?? ''
|
||||
const attrReferences = getReferencesData(srcDocId, srcDocClass, attachedDocId, attachedDocClass, content)
|
||||
|
||||
refs.push(...attrReferences)
|
||||
} else if (attr.type._class === core.class.TypeCollaborativeDoc) {
|
||||
const collaborativeDoc = (createdDoc as any)[attr.name] as CollaborativeDoc
|
||||
try {
|
||||
const ydoc = await loadCollaborativeDoc(storage, control.workspace, collaborativeDoc, control.ctx)
|
||||
if (ydoc !== undefined) {
|
||||
const attrReferences = getReferencesData(
|
||||
srcDocId,
|
||||
srcDocClass,
|
||||
attachedDocId,
|
||||
attachedDocClass,
|
||||
yDocToBuffer(ydoc)
|
||||
)
|
||||
refs.push(...attrReferences)
|
||||
}
|
||||
} catch {
|
||||
// do nothing, the collaborative doc does not sem to exist yet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const refSpace: Ref<Space> = control.hierarchy.isDerived(srcDocClass, core.class.Space)
|
||||
? (srcDocId as Ref<Space>)
|
||||
: srcDocSpace
|
||||
|
||||
return await getReferencesTxes(control, txFactory, refs, refSpace, [], originTx)
|
||||
}
|
||||
|
||||
async function getUpdateReferencesTxes (
|
||||
control: TriggerControl,
|
||||
storage: StorageAdapter,
|
||||
txFactory: TxFactory,
|
||||
updatedDoc: Doc,
|
||||
srcDocId: Ref<Doc>,
|
||||
srcDocClass: Ref<Class<Doc>>,
|
||||
srcDocSpace: Ref<Space>,
|
||||
originTx: TxCUD<Doc>
|
||||
): Promise<Tx[]> {
|
||||
const attachedDocId = updatedDoc._id
|
||||
const attachedDocClass = updatedDoc._class
|
||||
|
||||
// collect attribute references
|
||||
let hasReferenceAttrs = false
|
||||
const references: Data<ActivityReference>[] = []
|
||||
const attributes = control.hierarchy.getAllAttributes(updatedDoc._class)
|
||||
for (const attr of attributes.values()) {
|
||||
if (isMarkupType(attr.type._class)) {
|
||||
hasReferenceAttrs = true
|
||||
const content = (updatedDoc as any)[attr.name]?.toString() ?? ''
|
||||
const attrReferences = getReferencesData(srcDocId, srcDocClass, attachedDocId, attachedDocClass, content)
|
||||
references.push(...attrReferences)
|
||||
} else if (attr.type._class === core.class.TypeCollaborativeDoc) {
|
||||
hasReferenceAttrs = true
|
||||
try {
|
||||
const collaborativeDoc = (updatedDoc as any)[attr.name] as CollaborativeDoc
|
||||
const ydoc = await loadCollaborativeDoc(storage, control.workspace, collaborativeDoc, control.ctx)
|
||||
if (ydoc !== undefined) {
|
||||
const attrReferences = getReferencesData(
|
||||
srcDocId,
|
||||
srcDocClass,
|
||||
attachedDocId,
|
||||
attachedDocClass,
|
||||
yDocToBuffer(ydoc)
|
||||
)
|
||||
references.push(...attrReferences)
|
||||
}
|
||||
} catch {
|
||||
// do nothing, the collaborative doc does not sem to exist yet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There is a chance that references are managed manually
|
||||
// do not update references if there are no reference sources in the doc
|
||||
if (hasReferenceAttrs) {
|
||||
const current = await control.findAll(activity.class.ActivityReference, {
|
||||
srcDocId,
|
||||
srcDocClass,
|
||||
attachedDocId,
|
||||
collection: 'references'
|
||||
})
|
||||
|
||||
const refSpace: Ref<Space> = control.hierarchy.isDerived(srcDocClass, core.class.Space)
|
||||
? (srcDocId as Ref<Space>)
|
||||
: srcDocSpace
|
||||
|
||||
return await getReferencesTxes(control, txFactory, references, refSpace, current, originTx)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export function getReferencesData (
|
||||
srcDocId: Ref<Doc>,
|
||||
srcDocClass: Ref<Class<Doc>>,
|
||||
attachedDocId: Ref<Doc> | undefined,
|
||||
attachedDocClass: Ref<Class<Doc>> | undefined,
|
||||
content: string | Buffer
|
||||
): Array<Data<ActivityReference>> {
|
||||
const result: Array<Data<ActivityReference>> = []
|
||||
const references = []
|
||||
|
||||
if (content instanceof Buffer) {
|
||||
const nodes = yDocContentToNodes(extensions, content)
|
||||
for (const node of nodes) {
|
||||
references.push(...extractReferences(node))
|
||||
}
|
||||
} else {
|
||||
const doc = parseHTML(content, extensions)
|
||||
references.push(...extractReferences(doc))
|
||||
}
|
||||
|
||||
for (const ref of references) {
|
||||
if (ref.objectId !== attachedDocId && ref.objectId !== srcDocId) {
|
||||
result.push({
|
||||
attachedTo: ref.objectId,
|
||||
attachedToClass: ref.objectClass,
|
||||
collection: 'references',
|
||||
srcDocId,
|
||||
srcDocClass,
|
||||
message: ref.parentNode !== null ? getHTML(ref.parentNode, extensions) : '',
|
||||
attachedDocId,
|
||||
attachedDocClass
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function createReferenceTxes (
|
||||
control: TriggerControl,
|
||||
txFactory: TxFactory,
|
||||
ref: Data<ActivityReference>,
|
||||
space: Ref<Space>,
|
||||
originTx: TxCUD<Doc>
|
||||
): Promise<Tx[]> {
|
||||
if (control.hierarchy.isDerived(ref.attachedToClass, contact.class.Person)) {
|
||||
return await getPersonNotificationTxes(ref, control, txFactory.account, space, originTx)
|
||||
}
|
||||
|
||||
const refTx = control.txFactory.createTxCreateDoc(activity.class.ActivityReference, space, ref)
|
||||
const tx = control.txFactory.createTxCollectionCUD(ref.attachedToClass, ref.attachedTo, space, ref.collection, refTx)
|
||||
|
||||
return [tx]
|
||||
}
|
||||
|
||||
async function getReferencesTxes (
|
||||
control: TriggerControl,
|
||||
txFactory: TxFactory,
|
||||
references: Data<ActivityReference>[],
|
||||
space: Ref<Space>,
|
||||
current: ActivityReference[],
|
||||
originTx: TxCUD<Doc>
|
||||
): Promise<Tx[]> {
|
||||
const txes: Tx[] = []
|
||||
|
||||
for (const c of current) {
|
||||
// Find existing and check if we need to update message
|
||||
const pos = references.findIndex(
|
||||
(b) => b.srcDocId === c.srcDocId && b.srcDocClass === c.srcDocClass && b.attachedTo === c.attachedTo
|
||||
)
|
||||
if (pos !== -1) {
|
||||
// Update existing references when message changed
|
||||
const data = references[pos]
|
||||
if (c.message !== data.message) {
|
||||
const innerTx = txFactory.createTxUpdateDoc(c._class, c.space, c._id, {
|
||||
message: data.message
|
||||
})
|
||||
txes.push(txFactory.createTxCollectionCUD(c.attachedToClass, c.attachedTo, c.space, c.collection, innerTx))
|
||||
}
|
||||
references.splice(pos, 1)
|
||||
} else {
|
||||
// Remove not found references
|
||||
const innerTx = txFactory.createTxRemoveDoc(c._class, c.space, c._id)
|
||||
txes.push(txFactory.createTxCollectionCUD(c.attachedToClass, c.attachedTo, c.space, c.collection, innerTx))
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing references
|
||||
for (const ref of references) {
|
||||
txes.push(...(await createReferenceTxes(control, txFactory, ref, space, originTx)))
|
||||
}
|
||||
|
||||
return txes
|
||||
}
|
||||
|
||||
async function getRemoveActivityReferenceTxes (
|
||||
control: TriggerControl,
|
||||
txFactory: TxFactory,
|
||||
removedDocId: Ref<Doc>
|
||||
): Promise<Tx[]> {
|
||||
const txes: Tx[] = []
|
||||
const refs = await control.findAll(activity.class.ActivityReference, {
|
||||
attachedDocId: removedDocId,
|
||||
collection: 'references'
|
||||
})
|
||||
|
||||
for (const ref of refs) {
|
||||
const removeTx = txFactory.createTxRemoveDoc(ref._class, ref.space, ref._id)
|
||||
txes.push(txFactory.createTxCollectionCUD(ref.attachedToClass, ref.attachedTo, ref.space, ref.collection, removeTx))
|
||||
}
|
||||
|
||||
return txes
|
||||
}
|
||||
|
||||
function guessReferenceTx (hierarchy: Hierarchy, tx: TxCUD<Doc>): TxCUD<Doc> {
|
||||
// Try to guess reference target Tx for TxCollectionCUD txes based on collaborators availability
|
||||
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
|
||||
const cltx = tx as TxCollectionCUD<Doc, AttachedDoc>
|
||||
tx = TxProcessor.extractTx(cltx) as TxCUD<Doc>
|
||||
|
||||
if (hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) {
|
||||
return cltx
|
||||
}
|
||||
|
||||
const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators)
|
||||
return mixin !== undefined ? tx : cltx
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
async function ActivityReferenceCreate (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
|
||||
|
||||
if (ctx._class !== core.class.TxCreateDoc) return []
|
||||
if (control.hierarchy.isDerived(ctx.objectClass, activity.class.ActivityReference)) return []
|
||||
|
||||
control.storageFx(async (adapter) => {
|
||||
const txFactory = new TxFactory(control.txFactory.account)
|
||||
|
||||
const doc = TxProcessor.createDoc2Doc(ctx)
|
||||
const targetTx = guessReferenceTx(control.hierarchy, tx)
|
||||
|
||||
const txes: Tx[] = await getCreateReferencesTxes(
|
||||
control,
|
||||
adapter,
|
||||
txFactory,
|
||||
doc,
|
||||
targetTx.objectId,
|
||||
targetTx.objectClass,
|
||||
targetTx.objectSpace,
|
||||
tx
|
||||
)
|
||||
|
||||
if (txes.length !== 0) {
|
||||
await control.apply(txes, true)
|
||||
}
|
||||
})
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async function ActivityReferenceUpdate (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc<Doc>
|
||||
const attributes = control.hierarchy.getAllAttributes(ctx.objectClass)
|
||||
|
||||
let hasUpdates = false
|
||||
|
||||
for (const attr of attributes.values()) {
|
||||
if (isMarkupType(attr.type._class) || isCollaborativeType(attr.type._class)) {
|
||||
if (TxProcessor.txHasUpdate(ctx, attr.name)) {
|
||||
hasUpdates = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasUpdates) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rawDoc = (await control.findAll(ctx.objectClass, { _id: ctx.objectId }))[0]
|
||||
|
||||
if (rawDoc === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
control.storageFx(async (adapter) => {
|
||||
const txFactory = new TxFactory(control.txFactory.account)
|
||||
const doc = TxProcessor.updateDoc2Doc(rawDoc, ctx)
|
||||
const targetTx = guessReferenceTx(control.hierarchy, tx)
|
||||
|
||||
const txes: Tx[] = await getUpdateReferencesTxes(
|
||||
control,
|
||||
adapter,
|
||||
txFactory,
|
||||
doc,
|
||||
targetTx.objectId,
|
||||
targetTx.objectClass,
|
||||
targetTx.objectSpace,
|
||||
tx
|
||||
)
|
||||
|
||||
if (txes.length !== 0) {
|
||||
await control.apply(txes, true)
|
||||
}
|
||||
})
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async function ActivityReferenceRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc<Doc>
|
||||
const attributes = control.hierarchy.getAllAttributes(ctx.objectClass)
|
||||
|
||||
let hasMarkdown = false
|
||||
|
||||
for (const attr of attributes.values()) {
|
||||
if (isMarkupType(attr.type._class)) {
|
||||
hasMarkdown = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMarkdown) {
|
||||
const txFactory = new TxFactory(control.txFactory.account)
|
||||
|
||||
const txes: Tx[] = await getRemoveActivityReferenceTxes(control, txFactory, ctx.objectId)
|
||||
if (txes.length !== 0) {
|
||||
await control.apply(txes, true)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function ReferenceTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
|
||||
const etx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
|
||||
if (control.hierarchy.isDerived(etx.objectClass, activity.class.ActivityReference)) return []
|
||||
|
||||
if (etx._class === core.class.TxCreateDoc) {
|
||||
result.push(...(await ActivityReferenceCreate(tx, control)))
|
||||
}
|
||||
if (etx._class === core.class.TxUpdateDoc) {
|
||||
result.push(...(await ActivityReferenceUpdate(tx, control)))
|
||||
}
|
||||
if (etx._class === core.class.TxRemoveDoc) {
|
||||
result.push(...(await ActivityReferenceRemove(tx, control)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export default plugin(serverActivityId, {
|
||||
trigger: {
|
||||
ActivityMessagesHandler: '' as Resource<TriggerFunc>,
|
||||
OnDocRemoved: '' as Resource<TriggerFunc>,
|
||||
OnReactionChanged: '' as Resource<TriggerFunc>
|
||||
OnReactionChanged: '' as Resource<TriggerFunc>,
|
||||
ReferenceTrigger: '' as Resource<TriggerFunc>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
//
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import chunter, { Backlink } from '@hcengineering/chunter'
|
||||
import { loadCollaborativeDoc, yDocToBuffer } from '@hcengineering/collaboration'
|
||||
import core, {
|
||||
AttachedDoc,
|
||||
Class,
|
||||
CollaborativeDoc,
|
||||
Data,
|
||||
Doc,
|
||||
Hierarchy,
|
||||
Ref,
|
||||
Tx,
|
||||
TxCollectionCUD,
|
||||
TxCUD,
|
||||
TxFactory,
|
||||
TxProcessor,
|
||||
Type
|
||||
} from '@hcengineering/core'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { ServerKit, extractReferences, getHTML, parseHTML, yDocContentToNodes } from '@hcengineering/text'
|
||||
import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
|
||||
|
||||
const extensions = [ServerKit]
|
||||
|
||||
export function isMarkupType (type: Ref<Class<Type<any>>>): boolean {
|
||||
return type === core.class.TypeMarkup || type === core.class.TypeCollaborativeMarkup
|
||||
}
|
||||
|
||||
export function isCollaborativeType (type: Ref<Class<Type<any>>>): boolean {
|
||||
return type === core.class.TypeCollaborativeDoc
|
||||
}
|
||||
|
||||
export async function getCreateBacklinksTxes (
|
||||
control: TriggerControl,
|
||||
storage: StorageAdapter,
|
||||
txFactory: TxFactory,
|
||||
doc: Doc,
|
||||
backlinkId: Ref<Doc>,
|
||||
backlinkClass: Ref<Class<Doc>>
|
||||
): Promise<Tx[]> {
|
||||
const attachedDocId = doc._id
|
||||
|
||||
const backlinks: Data<Backlink>[] = []
|
||||
const attributes = control.hierarchy.getAllAttributes(doc._class)
|
||||
for (const attr of attributes.values()) {
|
||||
if (isMarkupType(attr.type._class)) {
|
||||
const content = (doc as any)[attr.name]?.toString() ?? ''
|
||||
const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
|
||||
backlinks.push(...attrBacklinks)
|
||||
} else if (attr.type._class === core.class.TypeCollaborativeDoc) {
|
||||
const collaborativeDoc = (doc as any)[attr.name] as CollaborativeDoc
|
||||
try {
|
||||
const ydoc = await loadCollaborativeDoc(storage, control.workspace, collaborativeDoc, control.ctx)
|
||||
if (ydoc !== undefined) {
|
||||
const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, yDocToBuffer(ydoc))
|
||||
backlinks.push(...attrBacklinks)
|
||||
}
|
||||
} catch {
|
||||
// do nothing, the collaborative doc does not sem to exist yet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getBacklinksTxes(txFactory, backlinks, [])
|
||||
}
|
||||
|
||||
export async function getUpdateBacklinksTxes (
|
||||
control: TriggerControl,
|
||||
storage: StorageAdapter,
|
||||
txFactory: TxFactory,
|
||||
doc: Doc,
|
||||
backlinkId: Ref<Doc>,
|
||||
backlinkClass: Ref<Class<Doc>>
|
||||
): Promise<Tx[]> {
|
||||
const attachedDocId = doc._id
|
||||
|
||||
// collect attribute backlinks
|
||||
let hasBacklinkAttrs = false
|
||||
const backlinks: Data<Backlink>[] = []
|
||||
const attributes = control.hierarchy.getAllAttributes(doc._class)
|
||||
for (const attr of attributes.values()) {
|
||||
if (isMarkupType(attr.type._class)) {
|
||||
hasBacklinkAttrs = true
|
||||
const content = (doc as any)[attr.name]?.toString() ?? ''
|
||||
const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, content)
|
||||
backlinks.push(...attrBacklinks)
|
||||
} else if (attr.type._class === core.class.TypeCollaborativeDoc) {
|
||||
hasBacklinkAttrs = true
|
||||
try {
|
||||
const collaborativeDoc = (doc as any)[attr.name] as CollaborativeDoc
|
||||
const ydoc = await loadCollaborativeDoc(storage, control.workspace, collaborativeDoc, control.ctx)
|
||||
if (ydoc !== undefined) {
|
||||
const attrBacklinks = getBacklinks(backlinkId, backlinkClass, attachedDocId, yDocToBuffer(ydoc))
|
||||
backlinks.push(...attrBacklinks)
|
||||
}
|
||||
} catch {
|
||||
// do nothing, the collaborative doc does not sem to exist yet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There is a chance that backlinks are managed manually
|
||||
// do not update backlinks if there are no backlink sources in the doc
|
||||
if (hasBacklinkAttrs) {
|
||||
const current = await control.findAll(chunter.class.Backlink, {
|
||||
backlinkId,
|
||||
backlinkClass,
|
||||
attachedDocId,
|
||||
collection: 'backlinks'
|
||||
})
|
||||
|
||||
return getBacklinksTxes(txFactory, backlinks, current)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export function getBacklinks (
|
||||
backlinkId: Ref<Doc>,
|
||||
backlinkClass: Ref<Class<Doc>>,
|
||||
attachedDocId: Ref<Doc> | undefined,
|
||||
content: string | Buffer
|
||||
): Array<Data<Backlink>> {
|
||||
const result: Array<Data<Backlink>> = []
|
||||
|
||||
const references = []
|
||||
|
||||
if (content instanceof Buffer) {
|
||||
const nodes = yDocContentToNodes(extensions, content)
|
||||
for (const node of nodes) {
|
||||
references.push(...extractReferences(node))
|
||||
}
|
||||
} else {
|
||||
const doc = parseHTML(content, extensions)
|
||||
references.push(...extractReferences(doc))
|
||||
}
|
||||
for (const ref of references) {
|
||||
if (ref.objectId !== attachedDocId && ref.objectId !== backlinkId) {
|
||||
result.push({
|
||||
attachedTo: ref.objectId,
|
||||
attachedToClass: ref.objectClass,
|
||||
collection: 'backlinks',
|
||||
backlinkId,
|
||||
backlinkClass,
|
||||
message: ref.parentNode !== null ? getHTML(ref.parentNode, extensions) : '',
|
||||
attachedDocId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getBacklinksTxes (txFactory: TxFactory, backlinks: Data<Backlink>[], current: Backlink[]): Tx[] {
|
||||
const txes: Tx[] = []
|
||||
|
||||
for (const c of current) {
|
||||
// Find existing and check if we need to update message
|
||||
const pos = backlinks.findIndex(
|
||||
(b) => b.backlinkId === c.backlinkId && b.backlinkClass === c.backlinkClass && b.attachedTo === c.attachedTo
|
||||
)
|
||||
if (pos !== -1) {
|
||||
// Update existing backlinks when message changed
|
||||
const data = backlinks[pos]
|
||||
if (c.message !== data.message) {
|
||||
const innerTx = txFactory.createTxUpdateDoc(c._class, c.space, c._id, {
|
||||
message: data.message
|
||||
})
|
||||
txes.push(
|
||||
txFactory.createTxCollectionCUD(
|
||||
c.attachedToClass,
|
||||
c.attachedTo,
|
||||
chunter.space.Backlinks,
|
||||
c.collection,
|
||||
innerTx
|
||||
)
|
||||
)
|
||||
}
|
||||
backlinks.splice(pos, 1)
|
||||
} else {
|
||||
// Remove not found backlinks
|
||||
const innerTx = txFactory.createTxRemoveDoc(c._class, c.space, c._id)
|
||||
txes.push(
|
||||
txFactory.createTxCollectionCUD(c.attachedToClass, c.attachedTo, chunter.space.Backlinks, c.collection, innerTx)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing backlinks
|
||||
for (const backlink of backlinks) {
|
||||
const backlinkTx = txFactory.createTxCreateDoc(chunter.class.Backlink, chunter.space.Backlinks, backlink)
|
||||
txes.push(
|
||||
txFactory.createTxCollectionCUD(
|
||||
backlink.attachedToClass,
|
||||
backlink.attachedTo,
|
||||
chunter.space.Backlinks,
|
||||
backlink.collection,
|
||||
backlinkTx
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return txes
|
||||
}
|
||||
|
||||
export async function getRemoveBacklinksTxes (
|
||||
control: TriggerControl,
|
||||
txFactory: TxFactory,
|
||||
doc: Ref<Doc>
|
||||
): Promise<Tx[]> {
|
||||
const txes: Tx[] = []
|
||||
|
||||
const backlinks = await control.findAll(chunter.class.Backlink, { attachedDocId: doc, collection: 'backlinks' })
|
||||
for (const b of backlinks) {
|
||||
const innerTx = txFactory.createTxRemoveDoc(b._class, b.space, b._id)
|
||||
txes.push(
|
||||
txFactory.createTxCollectionCUD(b.attachedToClass, b.attachedTo, chunter.space.Backlinks, b.collection, innerTx)
|
||||
)
|
||||
}
|
||||
|
||||
return txes
|
||||
}
|
||||
|
||||
export function guessBacklinkTx (hierarchy: Hierarchy, tx: TxCUD<Doc>): TxCUD<Doc> {
|
||||
// Try to guess backlink target Tx for TxCollectionCUD txes based on collaborators availability
|
||||
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
|
||||
const cltx = tx as TxCollectionCUD<Doc, AttachedDoc>
|
||||
tx = TxProcessor.extractTx(cltx) as TxCUD<Doc>
|
||||
|
||||
const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators)
|
||||
return mixin !== undefined ? tx : cltx
|
||||
}
|
||||
return tx
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
//
|
||||
|
||||
import chunter, {
|
||||
Backlink,
|
||||
Channel,
|
||||
ChatMessage,
|
||||
chunterId,
|
||||
@@ -37,7 +36,6 @@ import core, {
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxFactory,
|
||||
TxProcessor,
|
||||
TxRemoveDoc,
|
||||
TxUpdateDoc
|
||||
@@ -53,17 +51,9 @@ import {
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
import { stripTags } from '@hcengineering/text'
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
|
||||
|
||||
import {
|
||||
getCreateBacklinksTxes,
|
||||
getRemoveBacklinksTxes,
|
||||
getUpdateBacklinksTxes,
|
||||
guessBacklinkTx,
|
||||
isMarkupType,
|
||||
isCollaborativeType
|
||||
} from './backlinks'
|
||||
import { IsChannelMessage, IsDirectMessage, IsMeMentioned, IsThreadMessage } from './utils'
|
||||
import { IsChannelMessage, IsDirectMessage, IsThreadMessage } from './utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -101,9 +91,10 @@ export async function CommentRemove (
|
||||
}
|
||||
|
||||
const chatMessage = doc as ChatMessage
|
||||
return await findAll(chunter.class.Backlink, {
|
||||
backlinkId: chatMessage.attachedTo,
|
||||
backlinkClass: chatMessage.attachedToClass,
|
||||
|
||||
return await findAll(activity.class.ActivityReference, {
|
||||
srcDocId: chatMessage.attachedTo,
|
||||
srcDocClass: chatMessage.attachedToClass,
|
||||
attachedDocId: chatMessage._id
|
||||
})
|
||||
}
|
||||
@@ -254,99 +245,6 @@ async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise
|
||||
return [updateTx]
|
||||
}
|
||||
|
||||
async function BacklinksCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
|
||||
|
||||
if (ctx._class !== core.class.TxCreateDoc) return []
|
||||
if (control.hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return []
|
||||
|
||||
control.storageFx(async (adapter) => {
|
||||
const txFactory = new TxFactory(control.txFactory.account)
|
||||
|
||||
const doc = TxProcessor.createDoc2Doc(ctx)
|
||||
const targetTx = guessBacklinkTx(control.hierarchy, tx as TxCUD<Doc>)
|
||||
const txes: Tx[] = await getCreateBacklinksTxes(
|
||||
control,
|
||||
adapter,
|
||||
txFactory,
|
||||
doc,
|
||||
targetTx.objectId,
|
||||
targetTx.objectClass
|
||||
)
|
||||
|
||||
if (txes.length !== 0) {
|
||||
await control.apply(txes, true)
|
||||
}
|
||||
})
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async function BacklinksUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const ctx = TxProcessor.extractTx(tx) as TxUpdateDoc<Doc>
|
||||
|
||||
let hasUpdates = false
|
||||
const attributes = control.hierarchy.getAllAttributes(ctx.objectClass)
|
||||
for (const attr of attributes.values()) {
|
||||
if (isMarkupType(attr.type._class) || isCollaborativeType(attr.type._class)) {
|
||||
if (TxProcessor.txHasUpdate(ctx, attr.name)) {
|
||||
hasUpdates = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasUpdates) {
|
||||
const rawDoc = (await control.findAll(ctx.objectClass, { _id: ctx.objectId }))[0]
|
||||
|
||||
if (rawDoc !== undefined) {
|
||||
control.storageFx(async (adapter) => {
|
||||
const txFactory = new TxFactory(control.txFactory.account)
|
||||
const doc = TxProcessor.updateDoc2Doc(rawDoc, ctx)
|
||||
const targetTx = guessBacklinkTx(control.hierarchy, tx as TxCUD<Doc>)
|
||||
const txes: Tx[] = await getUpdateBacklinksTxes(
|
||||
control,
|
||||
adapter,
|
||||
txFactory,
|
||||
doc,
|
||||
targetTx.objectId,
|
||||
targetTx.objectClass
|
||||
)
|
||||
|
||||
if (txes.length !== 0) {
|
||||
await control.apply(txes, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
async function BacklinksRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const ctx = TxProcessor.extractTx(tx) as TxRemoveDoc<Doc>
|
||||
|
||||
let hasMarkdown = false
|
||||
const attributes = control.hierarchy.getAllAttributes(ctx.objectClass)
|
||||
for (const attr of attributes.values()) {
|
||||
if (isMarkupType(attr.type._class)) {
|
||||
hasMarkdown = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMarkdown) {
|
||||
const txFactory = new TxFactory(control.txFactory.account)
|
||||
|
||||
const txes: Tx[] = await getRemoveBacklinksTxes(control, txFactory, ctx.objectId)
|
||||
if (txes.length !== 0) {
|
||||
await control.apply(txes, true)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -429,27 +327,6 @@ export async function OnDirectMessageSent (originTx: Tx, control: TriggerControl
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function BacklinkTrigger (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
|
||||
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
|
||||
if (control.hierarchy.isDerived(ctx.objectClass, chunter.class.Backlink)) return []
|
||||
|
||||
if (ctx._class === core.class.TxCreateDoc) {
|
||||
result.push(...(await BacklinksCreate(tx, control)))
|
||||
}
|
||||
if (ctx._class === core.class.TxUpdateDoc) {
|
||||
result.push(...(await BacklinksUpdate(tx, control)))
|
||||
}
|
||||
if (ctx._class === core.class.TxRemoveDoc) {
|
||||
result.push(...(await BacklinksRemove(tx, control)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const NOTIFICATION_BODY_SIZE = 50
|
||||
|
||||
/**
|
||||
@@ -468,8 +345,8 @@ export async function getChunterNotificationContent (_: Doc, tx: TxCUD<Doc>): Pr
|
||||
if (ptx.tx.objectClass === chunter.class.ChatMessage) {
|
||||
const createTx = ptx.tx as TxCreateDoc<ChatMessage>
|
||||
message = createTx.attributes.message
|
||||
} else if (ptx.tx.objectClass === chunter.class.Backlink) {
|
||||
const createTx = ptx.tx as TxCreateDoc<Backlink>
|
||||
} else if (ptx.tx.objectClass === activity.class.ActivityReference) {
|
||||
const createTx = ptx.tx as TxCreateDoc<ActivityReference>
|
||||
message = createTx.attributes.message
|
||||
}
|
||||
}
|
||||
@@ -580,7 +457,6 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc<Channel>, control: Trigg
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export default async () => ({
|
||||
trigger: {
|
||||
BacklinkTrigger,
|
||||
ChunterTrigger,
|
||||
OnDirectMessageSent,
|
||||
OnChatMessageRemoved,
|
||||
@@ -593,7 +469,6 @@ export default async () => ({
|
||||
ChunterNotificationContentProvider: getChunterNotificationContent,
|
||||
IsDirectMessage,
|
||||
IsThreadMessage,
|
||||
IsMeMentioned,
|
||||
IsChannelMessage
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,12 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { Account, Doc, Hierarchy, Ref, Tx, TxCreateDoc, TxCUD, TxProcessor } from '@hcengineering/core'
|
||||
import { Account, Doc, Ref, Tx, TxCreateDoc } from '@hcengineering/core'
|
||||
import { NotificationType } from '@hcengineering/notification'
|
||||
import { TriggerControl } from '@hcengineering/server-core'
|
||||
import chunter, { Backlink, DirectMessage } from '@hcengineering/chunter'
|
||||
import contact, { Person } from '@hcengineering/contact'
|
||||
import core from '@hcengineering/core/src/component'
|
||||
import chunter, { DirectMessage } from '@hcengineering/chunter'
|
||||
import activity from '@hcengineering/activity'
|
||||
|
||||
/**
|
||||
@@ -40,31 +38,6 @@ export async function IsChannelMessage (
|
||||
return hierarchy.isDerived(doc._class, chunter.class.Channel) || hierarchy.hasMixin(doc, activity.mixin.ActivityDoc)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function IsMeMentioned (
|
||||
originTx: Tx,
|
||||
doc: Doc,
|
||||
user: Ref<Account>,
|
||||
type: NotificationType,
|
||||
control: TriggerControl
|
||||
): Promise<boolean> {
|
||||
const tx = TxProcessor.extractTx(originTx) as TxCUD<Backlink>
|
||||
if (!isBacklink(tx, control.hierarchy)) {
|
||||
return false
|
||||
}
|
||||
const backlink = TxProcessor.createDoc2Doc(tx as TxCreateDoc<Backlink>)
|
||||
|
||||
if (!control.hierarchy.isDerived(backlink.attachedToClass, contact.class.Person)) {
|
||||
return false
|
||||
}
|
||||
const acc = (
|
||||
await control.modelDb.findAll(contact.class.PersonAccount, { person: backlink.attachedTo as Ref<Person> })
|
||||
)[0]
|
||||
return acc._id === user
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -85,11 +58,3 @@ export async function IsDirectMessage (
|
||||
const dm = (await control.findAll(chunter.class.DirectMessage, { _id: doc._id as Ref<DirectMessage> }))[0]
|
||||
return dm !== undefined
|
||||
}
|
||||
|
||||
function isBacklink (tx: TxCUD<Backlink>, hierarchy: Hierarchy): boolean {
|
||||
if (tx._class !== core.class.TxCreateDoc) {
|
||||
return false
|
||||
}
|
||||
|
||||
return hierarchy.isDerived(tx.objectClass, chunter.class.Backlink)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ export const serverChunterId = 'server-chunter' as Plugin
|
||||
*/
|
||||
export default plugin(serverChunterId, {
|
||||
trigger: {
|
||||
BacklinkTrigger: '' as Resource<TriggerFunc>,
|
||||
ChunterTrigger: '' as Resource<TriggerFunc>,
|
||||
OnDirectMessageSent: '' as Resource<TriggerFunc>,
|
||||
OnChatMessageRemoved: '' as Resource<TriggerFunc>,
|
||||
@@ -41,7 +40,6 @@ export default plugin(serverChunterId, {
|
||||
IsDirectMessage: '' as TypeMatchFunc,
|
||||
IsChannelMessage: '' as TypeMatchFunc,
|
||||
IsThreadMessage: '' as TypeMatchFunc,
|
||||
IsMeMentioned: '' as TypeMatchFunc,
|
||||
ChunterNotificationContentProvider: '' as Resource<NotificationContentProvider>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import chunter, { Backlink, ChatMessage } from '@hcengineering/chunter'
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import contact, { Employee, formatName, Person, PersonAccount } from '@hcengineering/contact'
|
||||
import core, {
|
||||
Account,
|
||||
@@ -27,7 +27,6 @@ import core, {
|
||||
Data,
|
||||
Doc,
|
||||
DocumentUpdate,
|
||||
Hierarchy,
|
||||
MeasureContext,
|
||||
MixinUpdate,
|
||||
Ref,
|
||||
@@ -45,11 +44,12 @@ import core, {
|
||||
} from '@hcengineering/core'
|
||||
import notification, {
|
||||
ActivityInboxNotification,
|
||||
BaseNotificationType,
|
||||
ClassCollaborators,
|
||||
Collaborators,
|
||||
CommonInboxNotification,
|
||||
DocNotifyContext,
|
||||
InboxNotification,
|
||||
NotificationProvider,
|
||||
NotificationType
|
||||
} from '@hcengineering/notification'
|
||||
import { getMetadata, getResource } from '@hcengineering/platform'
|
||||
@@ -57,177 +57,103 @@ import type { TriggerControl } from '@hcengineering/server-core'
|
||||
import serverNotification, {
|
||||
getEmployee,
|
||||
getPersonAccount,
|
||||
getPersonAccountById,
|
||||
HTMLPresenter,
|
||||
TextPresenter
|
||||
getPersonAccountById
|
||||
} from '@hcengineering/server-notification'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
|
||||
import { Content } from './types'
|
||||
import { Content, NotifyResult } from './types'
|
||||
import {
|
||||
getHTMLPresenter,
|
||||
getNotificationContent,
|
||||
getTextPresenter,
|
||||
isAllowed,
|
||||
isMixinTx,
|
||||
isShouldNotify,
|
||||
isShouldNotifyTx,
|
||||
isUserEmployeeInFieldValue,
|
||||
isUserInFieldValue,
|
||||
replaceAll,
|
||||
updateNotifyContextsSpace
|
||||
} from './utils'
|
||||
|
||||
export async function OnBacklinkCreate (
|
||||
originTx: TxCollectionCUD<Doc, AttachedDoc>,
|
||||
control: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
const hierarchy = control.hierarchy
|
||||
const isTxCorrect = await isBacklinkCreated(originTx, hierarchy, control)
|
||||
export function getPushCollaboratorTx (
|
||||
control: TriggerControl,
|
||||
user: Ref<Account>,
|
||||
doc: Doc
|
||||
): TxMixin<Doc, Doc> | undefined {
|
||||
const mixin = control.hierarchy.as(doc, notification.mixin.Collaborators)
|
||||
|
||||
if (!isTxCorrect) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tx = originTx as TxCollectionCUD<Doc, Backlink>
|
||||
const receiver = await getPersonAccount(tx.objectId as Ref<Employee>, control)
|
||||
|
||||
if (receiver === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const sender = await getPersonAccountById(tx.modifiedBy, control)
|
||||
|
||||
if (sender === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (sender === receiver) {
|
||||
return []
|
||||
}
|
||||
|
||||
const backlink = TxProcessor.createDoc2Doc(tx.tx as TxCreateDoc<Backlink>)
|
||||
|
||||
if (!hierarchy.isDerived(backlink.backlinkClass, activity.class.ActivityMessage)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const message = (
|
||||
await control.findAll<ActivityMessage>(
|
||||
backlink.backlinkClass,
|
||||
{
|
||||
_id: backlink.backlinkId as Ref<ActivityMessage>
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
|
||||
if (message === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const res: Tx[] = []
|
||||
const collabMixin = hierarchy.as(message as Doc, notification.mixin.Collaborators)
|
||||
|
||||
if (collabMixin.collaborators === undefined || !collabMixin.collaborators.includes(receiver._id)) {
|
||||
const collabTx = control.txFactory.createTxMixin(
|
||||
message._id,
|
||||
message._class,
|
||||
message.space,
|
||||
notification.mixin.Collaborators,
|
||||
{
|
||||
$push: {
|
||||
collaborators: receiver._id
|
||||
}
|
||||
if (mixin.collaborators === undefined || !mixin.collaborators.includes(user)) {
|
||||
return control.txFactory.createTxMixin(doc._id, doc._class, doc.space, notification.mixin.Collaborators, {
|
||||
$push: {
|
||||
collaborators: user
|
||||
}
|
||||
)
|
||||
res.push(collabTx)
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function isBacklinkNotified (tx: TxCollectionCUD<Doc, Backlink>, control: TriggerControl): Promise<boolean> {
|
||||
const receiver = await getPersonAccount(tx.objectId as Ref<Employee>, control)
|
||||
|
||||
if (receiver === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { hierarchy } = control
|
||||
const backlink = TxProcessor.createDoc2Doc(tx.tx as TxCreateDoc<Backlink>)
|
||||
|
||||
if (!hierarchy.isDerived(backlink.backlinkClass, activity.class.ActivityMessage)) {
|
||||
return false
|
||||
}
|
||||
|
||||
export async function isMessageAlreadyNotified (
|
||||
_id: Ref<ActivityMessage>,
|
||||
user: Ref<Account>,
|
||||
control: TriggerControl
|
||||
): Promise<boolean> {
|
||||
const exists = await control.findAll(
|
||||
notification.class.ActivityInboxNotification,
|
||||
{ attachedTo: backlink.backlinkId as Ref<ActivityMessage>, user: receiver._id },
|
||||
{ limit: 1 }
|
||||
{ attachedTo: _id, user },
|
||||
{ limit: 1, projection: { _id: 1 } }
|
||||
)
|
||||
|
||||
return exists.length > 0
|
||||
}
|
||||
|
||||
async function isAlreadyNotified (originTx: TxCUD<Doc>, control: TriggerControl): Promise<boolean> {
|
||||
if (originTx._class !== core.class.TxCollectionCUD) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hierarchy = control.hierarchy
|
||||
const isBacklink = await isBacklinkCreated(originTx as TxCollectionCUD<Doc, AttachedDoc>, hierarchy, control)
|
||||
|
||||
if (!isBacklink) {
|
||||
return false
|
||||
}
|
||||
|
||||
return await isBacklinkNotified(originTx as TxCollectionCUD<Doc, Backlink>, control)
|
||||
}
|
||||
|
||||
async function isBacklinkCreated (
|
||||
ptx: TxCollectionCUD<Doc, AttachedDoc>,
|
||||
hierarchy: Hierarchy,
|
||||
control: TriggerControl
|
||||
): Promise<boolean> {
|
||||
if (ptx.tx._class !== core.class.TxCreateDoc || !hierarchy.isDerived(ptx.tx.objectClass, chunter.class.Backlink)) {
|
||||
return false
|
||||
}
|
||||
if (ptx.objectClass === contact.class.Person) {
|
||||
// We need to check if person is employee.
|
||||
const [person] = await control.findAll(contact.class.Person, { _id: ptx.objectId as Ref<Person> })
|
||||
return person !== undefined ? hierarchy.hasMixin(person, contact.mixin.Employee) : false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function isAllowed (
|
||||
export async function getCommonNotificationTxes (
|
||||
control: TriggerControl,
|
||||
receiver: Ref<PersonAccount>,
|
||||
typeId: Ref<NotificationType>,
|
||||
providerId: Ref<NotificationProvider>
|
||||
): Promise<boolean> {
|
||||
const setting = (
|
||||
await control.findAll(
|
||||
notification.class.NotificationSetting,
|
||||
{
|
||||
attachedTo: providerId,
|
||||
type: typeId,
|
||||
modifiedBy: receiver
|
||||
},
|
||||
{ limit: 1 }
|
||||
doc: Doc,
|
||||
data: Partial<Data<CommonInboxNotification>>,
|
||||
receiver: Ref<Account>,
|
||||
sender: Ref<Account>,
|
||||
attachedTo: Ref<Doc>,
|
||||
attachedToClass: Ref<Class<Doc>>,
|
||||
space: Ref<Space>,
|
||||
modifiedOn: Timestamp,
|
||||
notifyResult: NotifyResult
|
||||
): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
|
||||
if (notifyResult.allowed) {
|
||||
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo })
|
||||
|
||||
await pushInboxNotifications(
|
||||
control,
|
||||
res,
|
||||
receiver,
|
||||
attachedTo,
|
||||
attachedToClass,
|
||||
space,
|
||||
notifyContexts,
|
||||
data,
|
||||
notification.class.CommonInboxNotification,
|
||||
modifiedOn
|
||||
)
|
||||
)[0]
|
||||
if (setting !== undefined) {
|
||||
return setting.enabled
|
||||
}
|
||||
const type = (
|
||||
await control.modelDb.findAll(notification.class.NotificationType, {
|
||||
_id: typeId
|
||||
})
|
||||
)[0]
|
||||
if (type === undefined) return false
|
||||
return type.providers[providerId] ?? false
|
||||
|
||||
if (notifyResult.emails.length === 0) {
|
||||
return res
|
||||
}
|
||||
|
||||
const receiverAccount = await getPersonAccountById(receiver, control)
|
||||
if (receiverAccount === undefined) {
|
||||
return res
|
||||
}
|
||||
const emp = await getEmployee(receiverAccount.person as Ref<Employee>, control)
|
||||
if (emp?.active === true) {
|
||||
for (const type of notifyResult.emails) {
|
||||
await notifyByEmail(control, type._id, doc, sender as Ref<PersonAccount>, receiverAccount._id)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function getTextPart (doc: Doc, control: TriggerControl): Promise<string | undefined> {
|
||||
@@ -243,20 +169,6 @@ async function getHtmlPart (doc: Doc, control: TriggerControl): Promise<string |
|
||||
return HTMLPresenter != null ? await (await getResource(HTMLPresenter.presenter))(doc, control) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getHTMLPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): HTMLPresenter | undefined {
|
||||
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.HTMLPresenter)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getTextPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): TextPresenter | undefined {
|
||||
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.TextPresenter)
|
||||
}
|
||||
|
||||
function fillTemplate (template: string, sender: string, doc: string, data: string): string {
|
||||
let res = replaceAll(template, '{sender}', sender)
|
||||
res = replaceAll(res, '{doc}', doc)
|
||||
@@ -270,7 +182,7 @@ function fillTemplate (template: string, sender: string, doc: string, data: stri
|
||||
export async function getContent (
|
||||
doc: Doc | undefined,
|
||||
sender: string,
|
||||
type: Ref<NotificationType>,
|
||||
type: Ref<BaseNotificationType>,
|
||||
control: TriggerControl,
|
||||
data: string
|
||||
): Promise<Content | undefined> {
|
||||
@@ -293,7 +205,7 @@ export async function getContent (
|
||||
|
||||
async function notifyByEmail (
|
||||
control: TriggerControl,
|
||||
type: Ref<NotificationType>,
|
||||
type: Ref<BaseNotificationType>,
|
||||
doc: Doc | undefined,
|
||||
senderId: Ref<PersonAccount>,
|
||||
receiverId: Ref<PersonAccount>,
|
||||
@@ -553,7 +465,7 @@ export async function getNotificationTxes (
|
||||
shouldUpdateTimestamp = true
|
||||
): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
const notifyResult = await isShouldNotify(control, tx, originTx, object, target, isOwn, isSpace)
|
||||
const notifyResult = await isShouldNotifyTx(control, tx, originTx, object, target, isOwn, isSpace)
|
||||
|
||||
if (notifyResult.allowed) {
|
||||
await pushActivityInboxNotifications(
|
||||
@@ -834,11 +746,37 @@ async function removeCollaboratorDoc (tx: TxRemoveDoc<Doc>, control: TriggerCont
|
||||
}
|
||||
|
||||
const res: Tx[] = []
|
||||
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: tx.objectId })
|
||||
const notifyContexts = await control.findAll(
|
||||
notification.class.DocNotifyContext,
|
||||
{ attachedTo: tx.objectId },
|
||||
{
|
||||
projection: {
|
||||
_id: 1,
|
||||
_class: 1,
|
||||
space: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (notifyContexts.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const notifyContextRefs = notifyContexts.map(({ _id }) => _id)
|
||||
const inboxNotifications = await control.findAll(notification.class.InboxNotification, {
|
||||
docNotifyContext: { $in: notifyContextRefs }
|
||||
})
|
||||
|
||||
const inboxNotifications = await control.findAll(
|
||||
notification.class.InboxNotification,
|
||||
{
|
||||
docNotifyContext: { $in: notifyContextRefs }
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
_id: 1,
|
||||
_class: 1,
|
||||
space: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
inboxNotifications.forEach((notification) => {
|
||||
res.push(control.txFactory.createTxRemoveDoc(notification._class, notification.space, notification._id))
|
||||
@@ -1010,10 +948,6 @@ export async function createCollaboratorNotifications (
|
||||
return []
|
||||
}
|
||||
|
||||
if (await isAlreadyNotified(originTx ?? tx, control)) {
|
||||
return []
|
||||
}
|
||||
|
||||
switch (tx._class) {
|
||||
case core.class.TxCreateDoc:
|
||||
return await createCollaboratorDoc(tx as TxCreateDoc<Doc>, control, activityMessages, originTx ?? tx)
|
||||
@@ -1025,8 +959,6 @@ export async function createCollaboratorNotifications (
|
||||
)
|
||||
return res
|
||||
}
|
||||
case core.class.TxRemoveDoc:
|
||||
return await removeCollaboratorDoc(tx as TxRemoveDoc<Doc>, control)
|
||||
case core.class.TxCollectionCUD:
|
||||
return await collectionCollabDoc(tx as TxCollectionCUD<Doc, AttachedDoc>, control, activityMessages)
|
||||
}
|
||||
@@ -1118,6 +1050,14 @@ export async function getCollaborators (
|
||||
}
|
||||
}
|
||||
|
||||
async function OnDocRemove (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const etx = TxProcessor.extractTx(tx)
|
||||
|
||||
if (etx._class !== core.class.TxRemoveDoc) return []
|
||||
|
||||
return await removeCollaboratorDoc(tx as TxRemoveDoc<Doc>, control)
|
||||
}
|
||||
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
|
||||
@@ -1127,8 +1067,8 @@ export default async () => ({
|
||||
OnChatMessageCreate,
|
||||
OnAttributeCreate,
|
||||
OnAttributeUpdate,
|
||||
OnBacklinkCreate,
|
||||
OnActivityNotificationViewed
|
||||
OnActivityNotificationViewed,
|
||||
OnDocRemove
|
||||
},
|
||||
function: {
|
||||
IsUserInFieldValue: isUserInFieldValue,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { NotificationType } from '@hcengineering/notification'
|
||||
import { BaseNotificationType } from '@hcengineering/notification'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -28,5 +28,5 @@ export interface Content {
|
||||
*/
|
||||
export interface NotifyResult {
|
||||
allowed: boolean
|
||||
emails: NotificationType[]
|
||||
emails: BaseNotificationType[]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import notification, { NotificationContent, NotificationType } from '@hcengineering/notification'
|
||||
import notification, {
|
||||
BaseNotificationType,
|
||||
CommonNotificationType,
|
||||
NotificationContent,
|
||||
NotificationProvider,
|
||||
NotificationType
|
||||
} from '@hcengineering/notification'
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
import core, {
|
||||
Account,
|
||||
@@ -28,11 +34,16 @@ import core, {
|
||||
TxMixin,
|
||||
TxUpdateDoc
|
||||
} from '@hcengineering/core'
|
||||
import serverNotification, { getPersonAccountById, NotificationPresenter } from '@hcengineering/server-notification'
|
||||
import serverNotification, {
|
||||
getPersonAccountById,
|
||||
HTMLPresenter,
|
||||
NotificationPresenter,
|
||||
TextPresenter
|
||||
} from '@hcengineering/server-notification'
|
||||
import { getResource, IntlString } from '@hcengineering/platform'
|
||||
import contact, { formatName, Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { getTextPresenter, isAllowed, NotifyResult } from './index'
|
||||
import { DocUpdateMessage } from '@hcengineering/activity'
|
||||
import { NotifyResult } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -94,7 +105,60 @@ async function findPersonForAccount (control: TriggerControl, personId: Ref<Pers
|
||||
return undefined
|
||||
}
|
||||
|
||||
export async function isShouldNotify (
|
||||
export async function shouldNotifyCommon (
|
||||
control: TriggerControl,
|
||||
user: Ref<Account>,
|
||||
typeId: Ref<CommonNotificationType>
|
||||
): Promise<NotifyResult> {
|
||||
const type = (await control.modelDb.findAll(notification.class.CommonNotificationType, { _id: typeId }))[0]
|
||||
|
||||
const emailTypes: BaseNotificationType[] = []
|
||||
let allowed = false
|
||||
|
||||
if (type === undefined) {
|
||||
return { allowed, emails: emailTypes }
|
||||
}
|
||||
|
||||
if (await isAllowed(control, user as Ref<PersonAccount>, type._id, notification.providers.PlatformNotification)) {
|
||||
allowed = true
|
||||
}
|
||||
if (await isAllowed(control, user as Ref<PersonAccount>, type._id, notification.providers.EmailNotification)) {
|
||||
emailTypes.push(type)
|
||||
}
|
||||
|
||||
return { allowed, emails: emailTypes }
|
||||
}
|
||||
|
||||
export async function isAllowed (
|
||||
control: TriggerControl,
|
||||
receiver: Ref<PersonAccount>,
|
||||
typeId: Ref<BaseNotificationType>,
|
||||
providerId: Ref<NotificationProvider>
|
||||
): Promise<boolean> {
|
||||
const setting = (
|
||||
await control.findAll(
|
||||
notification.class.NotificationSetting,
|
||||
{
|
||||
attachedTo: providerId,
|
||||
type: typeId,
|
||||
modifiedBy: receiver
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
)[0]
|
||||
if (setting !== undefined) {
|
||||
return setting.enabled
|
||||
}
|
||||
const type = (
|
||||
await control.modelDb.findAll(notification.class.BaseNotificationType, {
|
||||
_id: typeId
|
||||
})
|
||||
)[0]
|
||||
if (type === undefined) return false
|
||||
return type.providers[providerId] ?? false
|
||||
}
|
||||
|
||||
export async function isShouldNotifyTx (
|
||||
control: TriggerControl,
|
||||
tx: TxCUD<Doc>,
|
||||
originTx: TxCUD<Doc>,
|
||||
@@ -233,6 +297,14 @@ export function isMixinTx (tx: TxCUD<Doc>): tx is TxMixin<Doc, Doc> {
|
||||
return tx._class === core.class.TxMixin
|
||||
}
|
||||
|
||||
export function getHTMLPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): HTMLPresenter | undefined {
|
||||
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.HTMLPresenter)
|
||||
}
|
||||
|
||||
export function getTextPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): TextPresenter | undefined {
|
||||
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.TextPresenter)
|
||||
}
|
||||
|
||||
async function getFallbackNotificationFullfillment (
|
||||
object: Doc,
|
||||
originTx: TxCUD<Doc>,
|
||||
|
||||
@@ -143,12 +143,12 @@ export default plugin(serverNotificationId, {
|
||||
NotificationPresenter: '' as Ref<Mixin<NotificationPresenter>>
|
||||
},
|
||||
trigger: {
|
||||
OnBacklinkCreate: '' as Resource<TriggerFunc>,
|
||||
OnAttributeCreate: '' as Resource<TriggerFunc>,
|
||||
OnAttributeUpdate: '' as Resource<TriggerFunc>,
|
||||
OnReactionChanged: '' as Resource<TriggerFunc>,
|
||||
OnChatMessageCreate: '' as Resource<TriggerFunc>,
|
||||
OnActivityNotificationViewed: '' as Resource<TriggerFunc>
|
||||
OnActivityNotificationViewed: '' as Resource<TriggerFunc>,
|
||||
OnDocRemove: '' as Resource<TriggerFunc>
|
||||
},
|
||||
function: {
|
||||
IsUserInFieldValue: '' as TypeMatchFunc,
|
||||
|
||||
@@ -34,9 +34,9 @@ import notification, { CommonInboxNotification } from '@hcengineering/notificati
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
import {
|
||||
getCommonNotificationTxes,
|
||||
getNotificationContent,
|
||||
isShouldNotify,
|
||||
pushInboxNotifications
|
||||
isShouldNotifyTx
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
import task from '@hcengineering/task'
|
||||
import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker'
|
||||
@@ -182,32 +182,31 @@ export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Pr
|
||||
return []
|
||||
}
|
||||
|
||||
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: todo._id })
|
||||
const res: Tx[] = []
|
||||
const notifyResult = await isShouldNotifyTx(control, createTx, tx, todo, account._id, true, false)
|
||||
const content = await getNotificationContent(tx, account._id, todo, control)
|
||||
const details = todo.description != null && todo.description.length > 0 ? todo.description : todo.title
|
||||
const data: Partial<Data<CommonInboxNotification>> = {
|
||||
...content,
|
||||
header: time.string.CreatedToDo,
|
||||
message: time.string.NewToDoDetails,
|
||||
props: { details }
|
||||
}
|
||||
|
||||
const notifyResult = await isShouldNotify(control, createTx, tx, todo, account._id, true, false)
|
||||
if (notifyResult.allowed) {
|
||||
const content = await getNotificationContent(tx, account._id, todo, control)
|
||||
const details = todo.description != null && todo.description.length > 0 ? todo.description : todo.title
|
||||
const data: Partial<Data<CommonInboxNotification>> = {
|
||||
...content,
|
||||
header: time.string.CreatedToDo,
|
||||
message: time.string.NewToDoDetails,
|
||||
props: { details }
|
||||
}
|
||||
await pushInboxNotifications(
|
||||
res.push(
|
||||
...(await getCommonNotificationTxes(
|
||||
control,
|
||||
res,
|
||||
todo,
|
||||
data,
|
||||
account._id,
|
||||
tx.modifiedBy,
|
||||
todo._id,
|
||||
todo._class,
|
||||
todo.space,
|
||||
notifyContexts,
|
||||
data,
|
||||
notification.class.CommonInboxNotification,
|
||||
createTx.modifiedOn
|
||||
)
|
||||
}
|
||||
createTx.modifiedOn,
|
||||
notifyResult
|
||||
))
|
||||
)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import { LeftSideMenuPage } from '../model/left-side-menu-page'
|
||||
import { IssuesPage } from '../model/tracker/issues-page'
|
||||
import { IssuesDetailsPage } from '../model/tracker/issues-details-page'
|
||||
import { NewIssue } from '../model/tracker/types'
|
||||
import { ContactsNavigationMenuPage } from '../model/contacts/navigation-menu-page'
|
||||
import { EmployeesPage } from '../model/contacts/employees-page'
|
||||
import { EmployeeDetailsPage } from '../model/contacts/employee-details-page'
|
||||
|
||||
test.use({
|
||||
@@ -68,38 +66,6 @@ test.describe('Mentions issue tests', () => {
|
||||
await issuesDetailsPage.checkCollaborators(['Appleseed John', 'Dirak Kainin'])
|
||||
})
|
||||
|
||||
test('Check that the backlink shown in the Contact activity', async ({ page }) => {
|
||||
const mentionName = 'Dirak Kainin'
|
||||
const mentionIssue: NewIssue = {
|
||||
title: `Check that the backlink shown in the Contact activity-${generateId()}`,
|
||||
description: 'Check that the backlink shown in the Contact activity description'
|
||||
}
|
||||
|
||||
const leftSideMenuPage = new LeftSideMenuPage(page)
|
||||
await leftSideMenuPage.buttonTracker.click()
|
||||
|
||||
const issuesPage = new IssuesPage(page)
|
||||
await issuesPage.modelSelectorAll.click()
|
||||
await issuesPage.createNewIssue(mentionIssue)
|
||||
await issuesPage.searchIssueByName(mentionIssue.title)
|
||||
await issuesPage.openIssueByName(mentionIssue.title)
|
||||
|
||||
const issuesDetailsPage = new IssuesDetailsPage(page)
|
||||
await issuesDetailsPage.addMentions(mentionName)
|
||||
await issuesDetailsPage.checkCommentExist(`@${mentionName}`)
|
||||
|
||||
await leftSideMenuPage.buttonContacts.click()
|
||||
|
||||
const contactsNavigationMenuPage = new ContactsNavigationMenuPage(page)
|
||||
await contactsNavigationMenuPage.buttonEmployee.click()
|
||||
|
||||
const employeesPage = new EmployeesPage(page)
|
||||
await employeesPage.openEmployeeByName(mentionName)
|
||||
|
||||
const employeeDetailsPage = new EmployeeDetailsPage(page)
|
||||
await employeeDetailsPage.checkActivityExist(`mentioned ${mentionName} in`, `@${mentionName}`)
|
||||
})
|
||||
|
||||
test('Check that the backlink shown in the Issue activity', async ({ page }) => {
|
||||
const mentionName = 'Dirak Kainin'
|
||||
const backlinkIssue: NewIssue = {
|
||||
|
||||
Reference in New Issue
Block a user