TSK-925 Collaborators (#2849)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-03-29 15:18:59 +06:00 committed by GitHub
parent c2f32f997f
commit bdbe7de4f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 869 additions and 593 deletions

View File

@ -194,6 +194,16 @@ export function createModel (builder: Builder, options = { addApplication: true
getName: chunter.function.GetDmName
})
builder.mixin(chunter.class.Message, core.class.Class, notification.mixin.TrackedDoc, {})
builder.mixin(chunter.class.Message, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['createdBy', 'replies']
})
builder.mixin(chunter.class.ChunterSpace, core.class.Class, notification.mixin.TrackedDoc, {})
builder.mixin(chunter.class.DirectMessage, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['members']
})
builder.mixin(chunter.class.DirectMessage, core.class.Class, view.mixin.ObjectPresenter, {
presenter: chunter.component.DmPresenter
})

View File

@ -37,6 +37,7 @@
"@hcengineering/ui": "^0.6.3",
"@hcengineering/platform": "^0.6.8",
"@hcengineering/contact": "^0.6.11",
"@hcengineering/notification": "^0.6.7",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/view": "^0.6.2",
"cross-fetch": "^3.1.5",

View File

@ -55,6 +55,7 @@ import workbench from '@hcengineering/model-workbench'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui'
import notification from '@hcengineering/notification'
import templates from '@hcengineering/templates'
import contact from './plugin'
@ -313,6 +314,14 @@ export function createModel (builder: Builder): void {
inlineEditor: contact.component.ContactArrayEditor
})
builder.mixin(contact.class.Contact, core.class.Class, notification.mixin.TrackedDoc, {})
builder.mixin(contact.class.Channel, core.class.Class, notification.mixin.TrackedDoc, {})
builder.mixin(contact.class.Contact, core.class.Class, notification.mixin.ClassCollaborators, {
fields: []
})
builder.mixin(contact.class.Member, core.class.Class, view.mixin.ObjectPresenter, {
presenter: contact.component.MemberPresenter
})

View File

@ -36,6 +36,7 @@
"@hcengineering/core": "^0.6.21",
"@hcengineering/ui": "^0.6.3",
"@hcengineering/platform": "^0.6.8",
"@hcengineering/notification": "^0.6.7",
"@hcengineering/document": "^0.6.0",
"@hcengineering/document-resources": "^0.6.0",
"@hcengineering/view": "^0.6.2",

View File

@ -46,6 +46,7 @@ import presentation from '@hcengineering/model-presentation'
import view, { actionTemplates, createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import tags from '@hcengineering/tags'
import notification from '@hcengineering/notification'
import document from './plugin'
export const DOMAIN_DOCUMENT = 'document' as Domain
@ -182,6 +183,8 @@ export function createModel (builder: Builder): void {
component: document.component.CreateDocument
})
builder.mixin(document.class.Document, core.class.Class, notification.mixin.TrackedDoc, {})
builder.mixin(document.class.Document, core.class.Class, view.mixin.ObjectPanel, {
component: document.component.EditDoc
})

View File

@ -156,7 +156,7 @@ export function createModel (builder: Builder): void {
inventory.category.Inventory
)
builder.mixin(inventory.class.Product, core.class.Class, notification.mixin.LastViewAttached, {})
builder.mixin(inventory.class.Product, core.class.Class, notification.mixin.TrackedDoc, {})
createAction(builder, {
label: inventory.string.CreateSubcategory,

View File

@ -14,14 +14,13 @@
// limitations under the License.
//
import { Account, Doc, Domain, DOMAIN_MODEL, Ref, Timestamp, TxCUD } from '@hcengineering/core'
import { ArrOf, Builder, Mixin, Model, Prop, TypeRef, TypeString, TypeTimestamp } from '@hcengineering/model'
import { Account, Doc, Domain, DOMAIN_MODEL, IndexKind, Ref, TxCUD } from '@hcengineering/core'
import { ArrOf, Builder, Index, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@hcengineering/model'
import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import type {
AnotherUserNotifications,
EmailNotification,
LastView,
LastViewAttached,
Notification,
NotificationProvider,
NotificationSetting,
@ -35,12 +34,10 @@ import notification from './plugin'
export const DOMAIN_NOTIFICATION = 'notification' as Domain
@Model(notification.class.LastView, core.class.AttachedDoc, DOMAIN_NOTIFICATION)
export class TLastView extends TAttachedDoc implements LastView {
@Prop(TypeTimestamp(), notification.string.LastView)
lastView!: Timestamp
@Model(notification.class.LastView, core.class.Doc, DOMAIN_NOTIFICATION)
export class TLastView extends TDoc implements LastView {
@Prop(TypeRef(core.class.Account), core.string.ModifiedBy)
@Index(IndexKind.Indexed)
user!: Ref<Account>
}
@ -112,8 +109,20 @@ export class TAnotherUserNotifications extends TClass implements AnotherUserNoti
fields!: string[]
}
@Mixin(notification.mixin.LastViewAttached, core.class.Class)
export class TLastViewAttached extends TClass implements LastViewAttached {}
@Mixin(notification.mixin.ClassCollaborators, core.class.Class)
export class TClassCollaborators extends TClass {
fields!: string[]
}
@Mixin(notification.mixin.TrackedDoc, core.class.Class)
export class TTrackedDoc extends TClass {}
@Mixin(notification.mixin.Collaborators, core.class.Doc)
@UX(notification.string.Collaborators)
export class TCollaborators extends TDoc {
@Prop(ArrOf(TypeRef(core.class.Account)), notification.string.Collaborators)
collaborators!: Ref<Account>[]
}
export function createModel (builder: Builder): void {
builder.createModel(
@ -125,7 +134,9 @@ export function createModel (builder: Builder): void {
TNotificationSetting,
TSpaceLastEdit,
TAnotherUserNotifications,
TLastViewAttached
TClassCollaborators,
TTrackedDoc,
TCollaborators
)
builder.createDoc(
@ -155,24 +166,28 @@ export function createModel (builder: Builder): void {
)
builder.createDoc(
notification.class.NotificationProvider,
notification.class.NotificationType,
core.space.Model,
{
label: notification.string.PlatformNotification,
default: true
label: notification.string.Notification,
hidden: true,
textTemplate: '',
htmlTemplate: '',
subjectTemplate: ''
},
notification.ids.PlatformNotification
notification.ids.CollaboratorNotification
)
builder.createDoc(
notification.class.NotificationProvider,
core.space.Model,
{
label: notification.string.BrowserNotification,
default: true
},
notification.ids.BrowserNotification
)
// Temporarily disabled, we should think about it
// builder.createDoc(
// notification.class.NotificationProvider,
// core.space.Model,
// {
// label: notification.string.BrowserNotification,
// default: true
// },
// notification.ids.BrowserNotification
// )
builder.createDoc(
notification.class.NotificationProvider,

View File

@ -13,9 +13,21 @@
// limitations under the License.
//
import core, { DOMAIN_TX, Ref, TxCreateDoc, TxOperations } from '@hcengineering/core'
import core, {
Account,
AttachedDoc,
Class,
Doc,
DOMAIN_TX,
generateId,
Ref,
TxCollectionCUD,
TxCreateDoc,
TxOperations,
TxRemoveDoc
} from '@hcengineering/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import notification, { Notification, NotificationType } from '@hcengineering/notification'
import notification, { LastView, Notification, NotificationType } from '@hcengineering/notification'
import { DOMAIN_NOTIFICATION } from '.'
async function fillNotificationText (client: MigrationClient): Promise<void> {
@ -79,9 +91,104 @@ async function createSpace (client: MigrationUpgradeClient): Promise<void> {
}
}
async function migrateLastView (client: MigrationClient): Promise<void> {
// lets clear last view txes (it should be derived and shouldn't store in tx collection)
const txes = await client.find(DOMAIN_TX, {
objectClass: notification.class.LastView
})
for (const tx of txes) {
await client.delete(DOMAIN_TX, tx._id)
}
const h = client.hierarchy
const docClasses = h.getDescendants(core.class.Doc)
const trackedClasses = docClasses.filter((p) => h.hasMixin(h.getClass(p), notification.mixin.TrackedDoc))
const allowedClasses = new Set<Ref<Class<Doc>>>()
trackedClasses.forEach((p) => h.getDescendants(p).forEach((a) => allowedClasses.add(a)))
const removeTxes = await client.find<TxRemoveDoc<Doc>>(
DOMAIN_TX,
{
_class: core.class.TxRemoveDoc
},
{ projection: { objectId: 1 } }
)
const removedDocs: Set<Ref<Doc>> = new Set(removeTxes.map((p) => p.objectId))
const removedCollectionTxes = await client.find<TxCollectionCUD<Doc, AttachedDoc>>(
DOMAIN_TX,
{
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxRemoveDoc
},
{ projection: { tx: 1 } }
)
removedCollectionTxes.forEach((p) => p.tx.objectId)
const newLastView: Map<Ref<Account>, LastView> = new Map()
let total = 0
while (true) {
const lastViews = await client.find<LastView>(
DOMAIN_NOTIFICATION,
{
_class: notification.class.LastView,
attachedTo: { $exists: true }
},
{ limit: 10000 }
)
total += lastViews.length
console.log(`migrate ${total} notifications`)
if (lastViews.length === 0) break
for (const lastView of lastViews) {
if (
lastView.user !== core.account.System &&
allowedClasses.has(lastView.attachedToClass) &&
!removedDocs.has(lastView.attachedTo)
) {
const obj: LastView = newLastView.get(lastView.user) ?? {
user: lastView.user,
modifiedBy: lastView.user,
modifiedOn: Date.now(),
_id: generateId(),
space: notification.space.Notifications,
_class: notification.class.LastView
}
obj[lastView.attachedTo] = lastView.lastView
newLastView.set(lastView.user, obj)
}
}
await Promise.all(lastViews.map((p) => client.delete(DOMAIN_NOTIFICATION, p._id)))
}
for (const [, lastView] of newLastView) {
await client.create(DOMAIN_NOTIFICATION, lastView)
}
}
async function fillCollaborators (client: MigrationClient): Promise<void> {
const targetClasses = await client.model.findAll(notification.mixin.ClassCollaborators, {})
for (const targetClass of targetClasses) {
const domain = client.hierarchy.getDomain(targetClass._id)
const desc = client.hierarchy.getDescendants(targetClass._id)
await client.update(
domain,
{
_class: { $in: desc },
'notification:mixin:Collaborators': { $exists: false }
},
{
'notification:mixin:Collaborators': {
collaborators: []
}
}
)
}
}
export const notificationOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await fillNotificationText(client)
await migrateLastView(client)
await fillCollaborators(client)
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
await createSpace(client)

View File

@ -26,7 +26,8 @@ export default mergeIds(notificationId, notification, {
MentionNotification: '' as IntlString,
PlatformNotification: '' as IntlString,
BrowserNotification: '' as IntlString,
EmailNotification: '' as IntlString
EmailNotification: '' as IntlString,
Collaborators: '' as IntlString
},
component: {
NotificationSettings: '' as AnyComponent

View File

@ -37,6 +37,7 @@
"@hcengineering/recruit": "^0.6.7",
"@hcengineering/recruit-resources": "^0.6.0",
"@hcengineering/chunter": "^0.6.2",
"@hcengineering/notification": "^0.6.7",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/model-chunter": "^0.6.0",
"@hcengineering/view": "^0.6.2",

View File

@ -41,6 +41,7 @@ import presentation from '@hcengineering/model-presentation'
import tags from '@hcengineering/model-tags'
import task, { actionTemplates, DOMAIN_TASK, TSpaceWithStates, TTask } from '@hcengineering/model-task'
import tracker from '@hcengineering/model-tracker'
import notification from '@hcengineering/notification'
import view, { actionTemplates as viewTemplates, createAction } from '@hcengineering/model-view'
import workbench, { Application, createNavigateAction } from '@hcengineering/model-workbench'
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
@ -211,6 +212,14 @@ export function createModel (builder: Builder): void {
editor: recruit.component.VacancyList
})
builder.mixin(recruit.class.Vacancy, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['createdBy']
})
builder.mixin(recruit.class.Applicant, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['createdBy']
})
builder.mixin(recruit.mixin.Candidate, core.class.Mixin, view.mixin.ObjectFactory, {
component: recruit.component.CreateCandidate
})
@ -603,6 +612,10 @@ export function createModel (builder: Builder): void {
recruit.viewlet.ApplicantDashboard
)
builder.mixin(recruit.class.Applicant, core.class.Class, notification.mixin.TrackedDoc, {})
builder.mixin(recruit.class.Vacancy, core.class.Class, notification.mixin.TrackedDoc, {})
builder.mixin(recruit.class.Applicant, core.class.Class, task.mixin.KanbanCard, {
card: recruit.component.KanbanCard
})

View File

@ -42,4 +42,16 @@ export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.UpdateLastView
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.CreateCollaboratorDoc
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.UpdateCollaboratorDoc
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.OnAddCollborator
})
}

View File

@ -382,7 +382,7 @@ export function createModel (builder: Builder): void {
editor: task.component.TaskHeader
})
builder.mixin(task.class.Task, core.class.Class, notification.mixin.LastViewAttached, {})
builder.mixin(task.class.Task, core.class.Class, notification.mixin.TrackedDoc, {})
builder.mixin(task.class.Task, core.class.Class, notification.mixin.AnotherUserNotifications, {
fields: ['assignee']
})

View File

@ -886,6 +886,10 @@ export function createModel (builder: Builder): void {
presenter: tracker.component.PriorityPresenter
})
builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['createdBy', 'assignee']
})
builder.mixin(tracker.class.TypeIssuePriority, core.class.Class, view.mixin.AttributeFilter, {
component: view.component.ValueFilter
})
@ -937,7 +941,8 @@ export function createModel (builder: Builder): void {
inlineEditor: tracker.component.ComponentStatusEditor
})
builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.LastViewAttached, {})
builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.TrackedDoc, {})
builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.AnotherUserNotifications, {
fields: ['assignee']
})

View File

@ -74,6 +74,17 @@ export class Hierarchy {
return typeof (d as any)[mixin] === 'object'
}
classHierarchyMixin<D extends Doc, M extends D>(_class: Ref<Class<D>>, mixin: Ref<Mixin<M>>): M | undefined {
let clazz = this.getClass(_class)
while (true) {
if (this.hasMixin(clazz, mixin)) {
return this.as(clazz, mixin) as any as M
}
if (clazz.extends === undefined) return
clazz = this.getClass(clazz.extends)
}
}
isMixin (_class: Ref<Class<Doc>>): boolean {
const data = this.classifiers.get(_class)
return data !== undefined && this._isMixin(data)

View File

@ -150,13 +150,24 @@ function $inc (document: Doc, keyval: Record<string, number>): void {
}
}
function $unset (document: Doc, keyval: Record<string, PropertyType>): void {
const doc = document as any
for (const key in keyval) {
if (doc[key] !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete doc[key]
}
}
}
const operators: Record<string, _OperatorFunc> = {
$push,
$pull,
$update,
$move,
$pushMixin,
$inc
$inc,
$unset
}
/**

View File

@ -201,6 +201,20 @@ export interface PushOptions<T extends object> {
$move?: Partial<OmitNever<ArrayMoveDescriptor<Required<T>>>>
}
/**
* @public
*/
export interface UnsetProperties {
[key: string]: any
}
/**
* @public
*/
export interface UnsetOptions {
$unset?: UnsetProperties
}
/**
* @public
*/

View File

@ -1,5 +1,4 @@
import {
ArrayAsElementPosition,
Client,
Doc,
DocumentQuery,
@ -9,25 +8,18 @@ import {
IncOptions,
ModelDb,
ObjQueryType,
OmitNever,
PushOptions,
Ref
Ref,
UnsetOptions
} from '@hcengineering/core'
/**
* @public
*/
export interface UnsetOptions<T extends object> {
$unset?: Partial<OmitNever<ArrayAsElementPosition<Required<T>>>>
}
/**
* @public
*/
export type MigrateUpdate<T extends Doc> = Partial<T> &
Omit<PushOptions<T>, '$move'> &
IncOptions<T> &
UnsetOptions<T> & {
UnsetOptions & {
// For any other mongo stuff
[key: string]: any
}

View File

@ -40,8 +40,7 @@
"@hcengineering/chunter": "^0.6.2",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/calendar": "^0.6.3",
"@hcengineering/notification": "^0.6.7"
"@hcengineering/calendar": "^0.6.3"
},
"repository": "https://github.com/hcenginneing/anticrm",
"publishConfig": {

View File

@ -17,10 +17,15 @@
import activity from '@hcengineering/activity'
import calendar from '@hcengineering/calendar'
import type { Doc } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import type { Asset } from '@hcengineering/platform'
import { AnySvelteComponent, Component, Panel, Icon, Scroller } from '@hcengineering/ui'
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import {
AnySvelteComponent,
Component,
deviceOptionsStore as deviceInfo,
Icon,
Panel,
Scroller
} from '@hcengineering/ui'
export let title: string | undefined = undefined
export let subtitle: string | undefined = undefined
@ -89,7 +94,6 @@
<svelte:fragment slot="utils">
<Component is={calendar.component.DocReminder} props={{ value: object, title }} />
<Component is={notification.component.LastViewEditor} props={{ value: object }} />
{#if isUtils && $$slots.utils}
<div class="buttons-divider" />
<slot name="utils" />

View File

@ -15,8 +15,8 @@
-->
<script lang="ts">
import type { Class, Doc, Ref } from '@hcengineering/core'
import { Label, tooltip } from '@hcengineering/ui'
import type { AnySvelteComponent, ButtonKind, ButtonSize } from '@hcengineering/ui'
import { Label, tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getAttribute, KeyedAttribute, updateAttribute } from '../attributes'
import { getAttributeEditor, getClient } from '../utils'
@ -38,8 +38,7 @@
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let editor: Promise<void | AnySvelteComponent> | undefined
let editor: AnySvelteComponent | undefined
function onChange (value: any) {
const doc = object as Doc
@ -47,63 +46,64 @@
;(doc as any)[attributeKey] = value
dispatch('update', { key, value })
} else {
updateAttribute(client, doc, _class, { key: attributeKey, attr: attribute }, value)
updateAttribute(client, doc, doc._class, { key: attributeKey, attr: attribute }, value)
}
}
function getEditor (_class: Ref<Class<Doc>>, key: KeyedAttribute | string) {
getAttributeEditor(client, _class, key).then((p) => (editor = p))
}
$: getEditor(_class, key)
$: attribute = typeof key === 'string' ? hierarchy.getAttribute(_class, key) : key.attr
$: attributeKey = typeof key === 'string' ? key : key.key
$: editor = getAttributeEditor(client, _class, key)
$: isReadonly = (attribute.readonly ?? false) || readonly
</script>
{#if editor}
{#await editor then instance}
{#if instance}
{#if showHeader}
<span
class="overflow-label"
use:tooltip={{
component: Label,
props: { label: attribute.label }
}}><Label label={attribute.label} /></span
>
<div class="flex flex-grow min-w-0">
<svelte:component
this={instance}
readonly={isReadonly}
label={attribute?.label}
placeholder={attribute?.label}
{kind}
{size}
{width}
{justify}
type={attribute?.type}
{maxWidth}
{attributeKey}
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
space={object.space}
{onChange}
{focus}
{object}
/>
</div>
{:else}
<div style="grid-column: 1/3;">
<svelte:component
this={instance}
type={attribute?.type}
{maxWidth}
{attributeKey}
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
readonly={isReadonly}
space={object.space}
{onChange}
{focus}
{object}
/>
</div>
{/if}
{/if}
{/await}
{#if showHeader}
<span
class="overflow-label"
use:tooltip={{
component: Label,
props: { label: attribute.label }
}}><Label label={attribute.label} /></span
>
<div class="flex flex-grow min-w-0">
<svelte:component
this={editor}
readonly={isReadonly}
label={attribute?.label}
placeholder={attribute?.label}
{kind}
{size}
{width}
{justify}
type={attribute?.type}
{maxWidth}
{attributeKey}
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
space={object.space}
{onChange}
{focus}
{object}
/>
</div>
{:else}
<div style="grid-column: 1/3;">
<svelte:component
this={editor}
type={attribute?.type}
{maxWidth}
{attributeKey}
value={getAttribute(client, object, { key: attributeKey, attr: attribute })}
readonly={isReadonly}
space={object.space}
{onChange}
{focus}
{object}
/>
</div>
{/if}
{/if}

View File

@ -312,6 +312,7 @@ export async function getAttributeEditor (
_class: Ref<Class<Obj>>,
key: KeyedAttribute | string
): Promise<AnySvelteComponent | undefined> {
console.log('get attribute editor', _class, key)
const hierarchy = client.getHierarchy()
const attribute = typeof key === 'string' ? hierarchy.getAttribute(_class, key) : key.attr
const presenterClass = attribute !== undefined ? getAttributePresenterClass(hierarchy, attribute) : undefined
@ -320,7 +321,6 @@ export async function getAttributeEditor (
return
}
const typeClass = hierarchy.getClass(presenterClass.attrClass)
let mixin: Ref<Mixin<AttributeEditor>>
switch (presenterClass.category) {
@ -337,16 +337,9 @@ export async function getAttributeEditor (
}
}
let editorMixin = hierarchy.as(typeClass, mixin)
let parent = typeClass.extends
const editorMixin = hierarchy.classHierarchyMixin(presenterClass.attrClass, mixin)
while (editorMixin.inlineEditor === undefined && parent !== undefined) {
const parentClass = hierarchy.getClass(parent)
editorMixin = hierarchy.as(parentClass, mixin)
parent = parentClass.extends
}
if (editorMixin.inlineEditor === undefined) {
if (editorMixin?.inlineEditor === undefined) {
// if (presenterClass.category === 'array') {
// // NOTE: Don't show error for array attributes for compatibility with previous implementation
// } else {

View File

@ -13,32 +13,32 @@
// limitations under the License.
-->
<script lang="ts">
import activity, { TxViewlet, ActivityFilter } from '@hcengineering/activity'
import activity, { ActivityFilter, TxViewlet } from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import core, { Class, Doc, Ref, SortingOrder } from '@hcengineering/core'
import notification, { LastView } from '@hcengineering/notification'
import { getResource, IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import notification from '@hcengineering/notification'
import {
ActionIcon,
Component,
eventToHTMLElement,
Grid,
Icon,
IconActivity,
Label,
Scroller,
Icon,
showPopup,
Spinner,
ActionIcon,
eventToHTMLElement
Spinner
} from '@hcengineering/ui'
import { ActivityKey, activityKey, DisplayTx, newActivity } from '../activity'
import TxView from './TxView.svelte'
import { filterCollectionTxes } from '../utils'
import { Writable } from 'svelte/store'
import { ActivityKey, activityKey, DisplayTx, newActivity } from '../activity'
import activityPlg from '../plugin'
import { filterCollectionTxes } from '../utils'
import FilterPopup from './FilterPopup.svelte'
import IconFilter from './icons/Filter.svelte'
import IconClose from './icons/Close.svelte'
import IconFilter from './icons/Filter.svelte'
import TxView from './TxView.svelte'
export let object: Doc
export let integrate: boolean = false
@ -71,7 +71,7 @@
getResource(notification.function.GetNotificationClient).then((res) => {
lastViews = res().getLastViews()
})
let lastViews: Writable<Map<Ref<Doc>, number>> | undefined
let lastViews: Writable<LastView> | undefined
let viewlets: Map<ActivityKey, TxViewlet>
@ -112,8 +112,8 @@
$: newTxPos = newTx(filtered, $lastViews)
function newTx (txes: DisplayTx[], lastViews: Map<Ref<Doc>, number> | undefined): number {
const lastView = lastViews?.get(object._id)
function newTx (txes: DisplayTx[], lastViews: LastView | undefined): number {
const lastView = (lastViews as any)?.[object._id]
if (lastView === undefined || lastView === -1) return -1
for (let index = 0; index < txes.length; index++) {
const tx = txes[index]

View File

@ -1,7 +1,6 @@
<script lang="ts">
import notification from '@hcengineering/notification'
import { State } from '@hcengineering/task'
import { Button, Component, getEventPositionElement, getPlatformColor, IconMoreV, showPopup } from '@hcengineering/ui'
import { Button, getEventPositionElement, getPlatformColor, IconMoreV, showPopup } from '@hcengineering/ui'
import { ContextMenu } from '@hcengineering/view-resources'
export let state: State
@ -16,7 +15,6 @@
<div class="flex-between h-full font-medium pr-2 pl-4">
<span class="lines-limit-2">{state.title}</span>
<div class="flex">
<Component is={notification.component.LastViewEditor} props={{ value: state }} />
<Button icon={IconMoreV} kind="transparent" on:click={showMenu} />
</div>
</div>

View File

@ -23,7 +23,7 @@
const notificationClient = NotificationClientImpl.getClient()
const lastViews = notificationClient.getLastViews()
$: lastView = $lastViews.get(object._id)
$: lastView = $lastViews[object._id]
$: subscribed = lastView !== undefined && lastView !== -1
</script>

View File

@ -15,7 +15,8 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import type { ChunterMessage, Message } from '@hcengineering/chunter'
import core, { Doc, Ref, Space, Timestamp, WithLookup } from '@hcengineering/core'
import core, { Ref, Space, Timestamp, WithLookup } from '@hcengineering/core'
import { LastView } from '@hcengineering/notification'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { createQuery } from '@hcengineering/presentation'
import { location as locationStore } from '@hcengineering/ui'
@ -103,7 +104,7 @@
function newMessagesStart (messages: Message[]): number {
if (space === undefined) return -1
const lastView = $lastViews.get(space)
const lastView = ($lastViews as any)[space]
if (lastView === undefined || lastView === -1) return -1
for (let index = 0; index < messages.length; index++) {
const message = messages[index]
@ -113,7 +114,7 @@
}
$: markUnread($lastViews)
function markUnread (lastViews: Map<Ref<Doc>, number>) {
function markUnread (lastViews: LastView) {
if (messages === undefined) return
const newPos = newMessagesStart(messages)
if (newPos !== -1 || newMessagesPos === -1) {

View File

@ -15,9 +15,8 @@
<script lang="ts">
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { ChunterMessage, Message, ChunterSpace } from '@hcengineering/chunter'
import { ChunterMessage, ChunterSpace, Message } from '@hcengineering/chunter'
import { generateId, getCurrentAccount, Ref, Space } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import { location, navigate } from '@hcengineering/ui'
import { get } from 'svelte/store'
@ -51,22 +50,6 @@
},
_id
)
if (
chunterSpace._class === chunter.class.DirectMessage &&
!chunterSpace.lastMessage &&
chunterSpace.members.length !== 1
) {
await Promise.all(
chunterSpace.members
.filter((accId) => accId !== me)
.map((accId) =>
client.addCollection(notification.class.LastView, space, space, chunterSpace._class, 'lastViews', {
user: accId,
lastView: 0
})
)
)
}
// Create an backlink to document
await createBacklinks(client, space, chunter.class.ChunterSpace, _id, message)

View File

@ -17,12 +17,10 @@
import { AttachmentList, AttachmentRefInput } from '@hcengineering/attachment-resources'
import type { ChunterMessage, Message, Reaction } from '@hcengineering/chunter'
import { EmployeeAccount } from '@hcengineering/contact'
import { employeeByIdStore, EmployeePresenter } from '@hcengineering/contact-resources'
import { Avatar, employeeByIdStore, EmployeePresenter } from '@hcengineering/contact-resources'
import { getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
import { getClient, MessageViewer } from '@hcengineering/presentation'
import { Avatar } from '@hcengineering/contact-resources'
import { EmojiPopup } from '@hcengineering/text-editor'
import ui, { ActionIcon, Button, IconMoreH, Label, showPopup, tooltip } from '@hcengineering/ui'
import { Action } from '@hcengineering/view'
@ -32,6 +30,7 @@
import chunter from '../plugin'
import { getTime } from '../utils'
// import Share from './icons/Share.svelte'
import notification, { Collaborators } from '@hcengineering/notification'
import Bookmark from './icons/Bookmark.svelte'
import Emoji from './icons/Emoji.svelte'
import Thread from './icons/Thread.svelte'
@ -52,13 +51,15 @@
$: attachments = (message.$lookup?.attachments ?? []) as Attachment[]
const client = getClient()
const hieararchy = client.getHierarchy()
const dispatch = createEventDispatcher()
const me = getCurrentAccount()._id
$: reactions = message.$lookup?.reactions as Reaction[] | undefined
const notificationClient = NotificationClientImpl.getClient()
const lastViews = notificationClient.getLastViews()
$: subscribed = ($lastViews.get(message._id) ?? -1) > -1
$: subscribed = (
hieararchy.as(message, notification.mixin.Collaborators) as any as Collaborators
).collaborators?.includes(me)
$: subscribeAction = subscribed
? ({
label: chunter.string.TurnOffReplies,

View File

@ -149,7 +149,7 @@
await client.tx(tx)
// Create an backlink to document
await createBacklinks(client, parent.space, chunter.class.ChunterSpace, commentId, message)
await createBacklinks(client, parent._id, parent._class, commentId, message)
commentId = generateId()
loading = false

View File

@ -16,7 +16,8 @@
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import type { ChunterMessage, Message, ThreadMessage } from '@hcengineering/chunter'
import core, { Doc, generateId, getCurrentAccount, Ref, Space } from '@hcengineering/core'
import core, { generateId, getCurrentAccount, Ref, Space } from '@hcengineering/core'
import { LastView } from '@hcengineering/notification'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { createQuery, getClient } from '@hcengineering/presentation'
import { getCurrentLocation, IconClose, Label, navigate } from '@hcengineering/ui'
@ -150,7 +151,7 @@
)
// Create an backlink to document
await createBacklinks(client, currentSpace, chunter.class.ChunterSpace, commentId, message)
await createBacklinks(client, _id, chunter.class.Message, commentId, message)
commentId = generateId()
isScrollForced = true
@ -158,8 +159,8 @@
}
let comments: ThreadMessage[] = []
function newMessagesStart (comments: ThreadMessage[], lastViews: Map<Ref<Doc>, number>): number {
const lastView = lastViews.get(_id)
function newMessagesStart (comments: ThreadMessage[], lastViews: LastView): number {
const lastView = (lastViews as any)[_id]
if (lastView === undefined || lastView === -1) return -1
for (let index = 0; index < comments.length; index++) {
const comment = comments[index]
@ -169,7 +170,7 @@
}
$: markUnread($lastViews)
function markUnread (lastViews: Map<Ref<Doc>, number>) {
function markUnread (lastViews: LastView) {
const newPos = newMessagesStart(comments, lastViews)
if (newPos !== -1 || newMessagesPos === -1) {
newMessagesPos = newPos

View File

@ -22,7 +22,7 @@ import chunter, {
Message,
ThreadMessage
} from '@hcengineering/chunter'
import core, { Data, Doc, DocumentQuery, Ref, RelatedDocument, Space } from '@hcengineering/core'
import core, { Data, Doc, DocumentQuery, getCurrentAccount, Ref, RelatedDocument, Space } from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { IntlString, Resources, translate } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
@ -55,6 +55,7 @@ import { get, writable } from 'svelte/store'
import { DisplayTx } from '../../activity/lib'
import { updateBacklinksList } from './backlinks'
import { getDmName, getTitle, getLink, resolveLocation } from './utils'
import notification from '@hcengineering/notification'
export { default as Header } from './components/Header.svelte'
export { classIcon } from './utils'
@ -72,20 +73,53 @@ async function MarkCommentUnread (object: ThreadMessage): Promise<void> {
async function SubscribeMessage (object: Message): Promise<void> {
const client = getClient()
const notificationClient = NotificationClientImpl.getClient()
if (client.getHierarchy().isDerived(object._class, chunter.class.ThreadMessage)) {
await notificationClient.updateLastView(object.attachedTo, object.attachedToClass, undefined, true)
const acc = getCurrentAccount()
const hierarchy = client.getHierarchy()
if (hierarchy.isDerived(object._class, chunter.class.ThreadMessage)) {
await client.updateMixin(
object.attachedTo,
object.attachedToClass,
object.space,
notification.mixin.Collaborators,
{
$push: {
collaborators: acc._id
}
}
)
} else {
await notificationClient.updateLastView(object._id, object._class, undefined, true)
await client.updateMixin(object._id, object._class, object.space, notification.mixin.Collaborators, {
$push: {
collaborators: acc._id
}
})
}
}
async function UnsubscribeMessage (object: Message): Promise<void> {
async function UnsubscribeMessage (object: ChunterMessage): Promise<void> {
const client = getClient()
const acc = getCurrentAccount()
const hierarchy = client.getHierarchy()
const notificationClient = NotificationClientImpl.getClient()
if (client.getHierarchy().isDerived(object._class, chunter.class.ThreadMessage)) {
if (hierarchy.isDerived(object._class, chunter.class.ThreadMessage)) {
await client.updateMixin(
object.attachedTo,
object.attachedToClass,
object.space,
notification.mixin.Collaborators,
{
$pull: {
collaborators: acc._id
}
}
)
await notificationClient.unsubscribe(object.attachedTo)
} else {
await client.updateMixin(object._id, object._class, object.space, notification.mixin.Collaborators, {
$pull: {
collaborators: acc._id
}
})
await notificationClient.unsubscribe(object._id)
}
}

View File

@ -38,6 +38,7 @@
"@hcengineering/ui": "^0.6.3",
"@hcengineering/setting": "^0.6.2",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/notification": "^0.6.7",
"@hcengineering/core": "^0.6.21",
"@hcengineering/view": "^0.6.2",
"@hcengineering/attachment-resources": "^0.6.0",

View File

@ -16,7 +16,8 @@
<script lang="ts">
import type { Channel, ChannelProvider } from '@hcengineering/contact'
import contact from '@hcengineering/contact'
import type { AttachedData, Doc, Ref, Timestamp } from '@hcengineering/core'
import type { AttachedData, Doc, Ref } from '@hcengineering/core'
import { LastView } from '@hcengineering/notification'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import type { Asset, IntlString } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
@ -69,7 +70,7 @@
function getProvider (
item: AttachedData<Channel>,
map: Map<Ref<ChannelProvider>, ChannelProvider>,
lastViews: Map<Ref<Doc>, Timestamp>
lastViews: LastView
): Item | undefined {
const provider = map.get(item.provider)
if (provider) {
@ -91,13 +92,13 @@
}
}
function isNew (item: Channel, lastViews: Map<Ref<Doc>, Timestamp>): boolean {
function isNew (item: Channel, lastViews: LastView): boolean {
if (item.lastMessage === undefined) return false
const lastView = (item as Channel)._id !== undefined ? lastViews.get((item as Channel)._id) : undefined
const lastView = (item as Channel)._id !== undefined ? lastViews[(item as Channel)._id] : undefined
return lastView ? lastView < item.lastMessage : (item.items ?? 0) > 0
}
async function update (value: AttachedData<Channel>[] | Channel | null, lastViews: Map<Ref<Doc>, Timestamp>) {
async function update (value: AttachedData<Channel>[] | Channel | null, lastViews: LastView) {
if (value == null) {
displayItems = []
return

View File

@ -15,14 +15,15 @@
-->
<script lang="ts">
import type { Channel, ChannelProvider } from '@hcengineering/contact'
import type { AttachedData, Doc, Ref, Timestamp } from '@hcengineering/core'
import type { AttachedData, Doc, Ref } from '@hcengineering/core'
import { LastView } from '@hcengineering/notification'
import type { Asset, IntlString } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { Button } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { getChannelProviders } from '../utils'
import ChannelsPopup from './ChannelsPopup.svelte'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
export let value: AttachedData<Channel>[] | Channel | null
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'large'
@ -46,7 +47,7 @@
function getProvider (
item: AttachedData<Channel>,
map: Map<Ref<ChannelProvider>, ChannelProvider>,
lastViews: Map<Ref<Doc>, Timestamp>
lastViews: LastView
): any | undefined {
const provider = map.get(item.provider)
if (provider) {
@ -64,13 +65,13 @@
}
}
function isNew (item: Channel, lastViews: Map<Ref<Doc>, Timestamp>): boolean {
function isNew (item: Channel, lastViews: LastView): boolean {
if (item.lastMessage === undefined) return false
const lastView = (item as Channel)._id !== undefined ? lastViews.get((item as Channel)._id) : undefined
const lastView = (item as Channel)._id !== undefined ? lastViews[(item as Channel)._id] : undefined
return lastView ? lastView < item.lastMessage : (item.items ?? 0) > 0
}
async function update (value: AttachedData<Channel>[] | Channel | null, lastViews: Map<Ref<Doc>, Timestamp>) {
async function update (value: AttachedData<Channel>[] | Channel | null, lastViews: LastView) {
if (value === null) {
displayItems = []
return

View File

@ -18,12 +18,12 @@
import type { IntlString } from '@hcengineering/platform'
import { Button, ButtonKind, ButtonSize, Label, showPopup, TooltipAlignment } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { createQuery } from '@hcengineering/presentation'
import Members from './icons/Members.svelte'
import UsersPopup from './UsersPopup.svelte'
import UserInfo from './UserInfo.svelte'
import CombineAvatars from './CombineAvatars.svelte'
import plugin from '../plugin'
import { employeeByIdStore } from '../utils'
import CombineAvatars from './CombineAvatars.svelte'
import Members from './icons/Members.svelte'
import UserInfo from './UserInfo.svelte'
import UsersPopup from './UsersPopup.svelte'
export let items: Ref<Employee>[] = []
export let _class: Ref<Class<Employee>> = contact.class.Employee
@ -40,13 +40,8 @@
export let emptyLabel = plugin.string.Members
export let readonly: boolean = false
let persons: Employee[] = []
const query = createQuery()
$: query.query<Employee>(_class, { _id: { $in: items } }, (result) => {
persons = result
})
let persons: Employee[] = items.map((p) => $employeeByIdStore.get(p)).filter((p) => p !== undefined) as Employee[]
$: persons = items.map((p) => $employeeByIdStore.get(p)).filter((p) => p !== undefined) as Employee[]
const dispatch = createEventDispatcher()

View File

@ -15,6 +15,7 @@
"Remove": "Delete notification",
"RemoveAll": "Delete all notifications",
"MarkAllAsRead": "Mark all notifications as read",
"MarkAsRead": "Mark as read"
"MarkAsRead": "Mark as read",
"Collaborators": "Collaborators"
}
}

View File

@ -15,6 +15,7 @@
"Remove": "Удалить нотификацию",
"RemoveAll": "Удалить все нотификации",
"MarkAllAsRead": "Отметить все нотификации как прочитанные",
"MarkAsRead": "Отметить нотификация прочитанной"
"MarkAsRead": "Отметить нотификация прочитанной",
"Collaborators": "Участники"
}
}

View File

@ -112,6 +112,7 @@
}
}
const client = getClient()
const hierarchy = client.getHierarchy()
let clearTimer: number | undefined
@ -136,7 +137,7 @@
alreadyShown.clear()
}, 5000)
const lastView = $lastViews.get(lastViewId)
const lastView = ($lastViews as any)[lastViewId]
if ((lastView ?? notifyInstance.modifiedOn) > 0) {
await notificationClient.updateLastView(
lastViewId,
@ -154,16 +155,10 @@
})
notification.onclick = () => {
if (notifyInstance.action !== undefined) {
showPanel(
notifyInstance.action.component,
notifyInstance.action.objectId,
notifyInstance.action.objectClass,
'content'
)
} else {
showPanel(view.component.EditDoc, notifyInstance.attachedTo, notifyInstance.attachedToClass, 'content')
}
const targetClass = hierarchy.getClass(notifyInstance.attachedToClass)
const panelComponent = hierarchy.as(targetClass, view.mixin.ObjectPanel)
const component = panelComponent.component ?? view.component.EditDoc
showPanel(component, notifyInstance.attachedTo, notifyInstance.attachedToClass, 'content')
}
}
</script>

View File

@ -1,39 +0,0 @@
<!--
// Copyright © 2022 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 { Doc } from '@hcengineering/core'
import { Button, tooltip } from '@hcengineering/ui'
import notification from '../plugin'
import { NotificationClientImpl } from '../utils'
export let value: Doc
const notificationClient = NotificationClientImpl.getClient()
const lastViews = notificationClient.getLastViews()
$: lastView = $lastViews.get(value._id)
$: subscribed = lastView !== undefined && lastView !== -1
</script>
<div use:tooltip={{ label: subscribed ? notification.string.DontTrack : notification.string.Track }}>
<Button
size={'medium'}
kind={'transparent'}
icon={subscribed ? notification.icon.Track : notification.icon.DontTrack}
on:click={() => {
if (subscribed) notificationClient.unsubscribe(value._id)
else notificationClient.updateLastView(value._id, value._class, undefined, true)
}}
/>
</div>

View File

@ -23,7 +23,7 @@
const notificationClient = NotificationClientImpl.getClient()
const lastViews = notificationClient.getLastViews()
$: lastView = $lastViews.get(value._id)
$: lastView = ($lastViews as any)[value._id]
$: hasNotification = lastView !== undefined && lastView !== -1 && lastView < value.modifiedOn
</script>

View File

@ -18,7 +18,6 @@ import { Resources } from '@hcengineering/platform'
import NotificationsPopup from './components/NotificationsPopup.svelte'
import NotificationSettings from './components/NotificationSettings.svelte'
import NotificationPresenter from './components/NotificationPresenter.svelte'
import LastViewEditor from './components/LastViewEditor.svelte'
import { NotificationClientImpl } from './utils'
export * from './utils'
@ -29,8 +28,7 @@ export default async (): Promise<Resources> => ({
component: {
NotificationsPopup,
NotificationPresenter,
NotificationSettings,
LastViewEditor
NotificationSettings
},
function: {
GetNotificationClient: NotificationClientImpl.getClient

View File

@ -14,31 +14,33 @@
// limitations under the License.
//
import core, { Class, Doc, getCurrentAccount, Ref, Timestamp } from '@hcengineering/core'
import core, { Account, Class, Doc, getCurrentAccount, Ref, Timestamp } from '@hcengineering/core'
import notification, { LastView, NotificationClient } from '@hcengineering/notification'
import { createQuery, getClient } from '@hcengineering/presentation'
import { writable, Writable } from 'svelte/store'
import { get, writable, Writable } from 'svelte/store'
/**
* @public
*/
export class NotificationClientImpl implements NotificationClient {
protected static _instance: NotificationClientImpl | undefined = undefined
private lastViews = new Map<Ref<Doc>, LastView>()
private readonly lastViewsStore = writable(new Map<Ref<Doc>, Timestamp>())
private readonly lastViewsStore = writable<LastView>()
private readonly lastViewQuery = createQuery()
private readonly user: Ref<Account>
private constructor () {
this.lastViewQuery.query(notification.class.LastView, { user: getCurrentAccount()._id }, (result) => {
const res: Map<Ref<Doc>, Timestamp> = new Map<Ref<Doc>, Timestamp>()
const lastViews: Map<Ref<Doc>, LastView> = new Map<Ref<Doc>, LastView>()
result.forEach((p) => {
res.set(p.attachedTo, p.lastView)
lastViews.set(p.attachedTo, p)
})
this.lastViews = lastViews
this.lastViewsStore.set(res)
this.user = getCurrentAccount()._id
this.lastViewQuery.query(notification.class.LastView, { user: this.user }, (result) => {
this.lastViewsStore.set(result[0])
if (result[0] === undefined) {
const client = getClient()
const u = client.txFactory.createTxCreateDoc(notification.class.LastView, notification.space.Notifications, {
user: this.user
})
u.space = core.space.DerivedTx
void client.tx(u)
}
})
}
@ -53,7 +55,7 @@ export class NotificationClientImpl implements NotificationClient {
return NotificationClientImpl._instance
}
getLastViews (): Writable<Map<Ref<Doc>, Timestamp>> {
getLastViews (): Writable<LastView> {
return this.lastViewsStore
}
@ -64,39 +66,35 @@ export class NotificationClientImpl implements NotificationClient {
force: boolean = false
): Promise<void> {
const client = getClient()
const user = getCurrentAccount()._id
const hierarchy = client.getHierarchy()
const mixin = hierarchy.classHierarchyMixin(_class, notification.mixin.TrackedDoc)
if (mixin === undefined) return
const lastView = time ?? new Date().getTime()
const current = this.lastViews.get(_id)
if (current !== undefined) {
if (current.lastView === -1 && !force) return
if (current.lastView < lastView || force) {
const u = client.txFactory.createTxUpdateDoc(current._class, current.space, current._id, {
lastView
})
u.space = core.space.DerivedTx
await client.tx(u)
const obj = get(this.lastViewsStore)
if (obj !== undefined) {
const current = obj[_id] as Timestamp | undefined
if (current !== undefined || force) {
if (current === -1 && !force) return
if (force || (current ?? 0) < lastView) {
const u = client.txFactory.createTxUpdateDoc(obj._class, obj.space, obj._id, {
[_id]: lastView
})
u.space = core.space.DerivedTx
await client.tx(u)
}
}
} else if (force) {
const u = client.txFactory.createTxCreateDoc(notification.class.LastView, notification.space.Notifications, {
user,
lastView,
attachedTo: _id,
attachedToClass: _class,
collection: 'lastViews'
})
u.space = core.space.DerivedTx
await client.tx(u)
}
}
async unsubscribe (_id: Ref<Doc>): Promise<void> {
const client = getClient()
const user = getCurrentAccount()._id
const current = await client.findOne(notification.class.LastView, { attachedTo: _id, user })
if (current !== undefined) {
await client.updateDoc(current._class, current.space, current._id, {
lastView: -1
const obj = get(this.lastViewsStore)
if (obj !== undefined) {
const u = client.txFactory.createTxUpdateDoc(obj._class, obj.space, obj._id, {
[_id]: -1
})
u.space = core.space.DerivedTx
await client.tx(u)
}
}
}

View File

@ -13,29 +13,20 @@
// limitations under the License.
//
import type { Account, AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp, TxCUD } from '@hcengineering/core'
import { Account, AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp, TxCUD } from '@hcengineering/core'
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { IntegrationType } from '@hcengineering/setting'
import { AnyComponent } from '@hcengineering/ui'
import { Writable } from './types'
import { IntegrationType } from '@hcengineering/setting'
export * from './types'
/**
* @public
*/
export interface LastView extends AttachedDoc {
lastView: Timestamp
export interface LastView extends Doc {
user: Ref<Account>
}
/**
* @public
*/
export interface NotificationAction {
component: AnyComponent
objectId: Ref<Doc>
objectClass: Ref<Class<Doc>>
[key: string]: any
}
/**
@ -46,9 +37,6 @@ export interface Notification extends AttachedDoc {
status: NotificationStatus
text: string
type: Ref<NotificationType>
// Defined to open particular item if required.
action?: NotificationAction
}
/**
@ -111,13 +99,27 @@ export interface SpaceLastEdit extends Class<Doc> {
/**
* @public
*/
export interface LastViewAttached extends Class<AttachedDoc> {}
export interface AnotherUserNotifications extends Class<Doc> {
fields: string[]
}
/**
* @public
*/
export interface AnotherUserNotifications extends Class<Doc> {
fields: string[]
export interface ClassCollaborators extends Class<Doc> {
fields: string[] // Ref<Account> | Ref<Employee> | Ref<Account>[] | Ref<Employee>[]
}
/**
* @public
*/
export interface TrackedDoc extends Class<Doc> {}
/**
* @public
*/
export interface Collaborators extends Doc {
collaborators: Ref<Account>[]
}
/**
@ -129,7 +131,7 @@ export const notificationId = 'notification' as Plugin
* @public
*/
export interface NotificationClient {
getLastViews: () => Writable<Map<Ref<Doc>, Timestamp>>
getLastViews: () => Writable<LastView>
updateLastView: (_id: Ref<Doc>, _class: Ref<Class<Doc>>, time?: Timestamp, force?: boolean) => Promise<void>
unsubscribe: (_id: Ref<Doc>) => Promise<void>
}
@ -146,7 +148,9 @@ const notification = plugin(notificationId, {
mixin: {
SpaceLastEdit: '' as Ref<Mixin<SpaceLastEdit>>,
AnotherUserNotifications: '' as Ref<Mixin<AnotherUserNotifications>>,
LastViewAttached: '' as Ref<Mixin<LastViewAttached>>
ClassCollaborators: '' as Ref<Mixin<ClassCollaborators>>,
Collaborators: '' as Ref<Mixin<Collaborators>>,
TrackedDoc: '' as Ref<Mixin<TrackedDoc>>
},
class: {
LastView: '' as Ref<Class<LastView>>,
@ -159,6 +163,7 @@ const notification = plugin(notificationId, {
ids: {
MentionNotification: '' as Ref<NotificationType>,
DMNotification: '' as Ref<NotificationType>,
CollaboratorNotification: '' as Ref<NotificationType>,
PlatformNotification: '' as Ref<NotificationProvider>,
BrowserNotification: '' as Ref<NotificationProvider>,
EmailNotification: '' as Ref<NotificationProvider>,
@ -169,8 +174,7 @@ const notification = plugin(notificationId, {
},
component: {
NotificationsPopup: '' as AnyComponent,
NotificationPresenter: '' as AnyComponent,
LastViewEditor: '' as AnyComponent
NotificationPresenter: '' as AnyComponent
},
icon: {
Notifications: '' as Asset,

View File

@ -15,15 +15,14 @@
-->
<script lang="ts">
import { Attachments } from '@hcengineering/attachment-resources'
import type { Ref } from '@hcengineering/core'
import core from '@hcengineering/core'
import core, { ClassifierKind, Doc, Mixin, Ref } from '@hcengineering/core'
import { Panel } from '@hcengineering/panel'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Vacancy } from '@hcengineering/recruit'
import { FullDescriptionBox } from '@hcengineering/text-editor'
import tracker from '@hcengineering/tracker'
import { Button, Component, EditBox, Grid, IconMoreH, showPopup } from '@hcengineering/ui'
import { ClassAttributeBar, ContextMenu } from '@hcengineering/view-resources'
import { ContextMenu, DocAttributeBar } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
import VacancyApplications from './VacancyApplications.svelte'
@ -60,6 +59,21 @@
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
}
}
const ignoreMixins: Set<Ref<Mixin<Doc>>> = new Set<Ref<Mixin<Doc>>>()
const hierarchy = client.getHierarchy()
let mixins: Mixin<Doc>[] = []
function getMixins (object: Doc): void {
if (object === undefined) return
const descendants = hierarchy.getDescendants(core.class.Doc).map((p) => hierarchy.getClass(p))
mixins = descendants.filter(
(m) => m.kind === ClassifierKind.MIXIN && !ignoreMixins.has(m._id) && hierarchy.hasMixin(object, m._id)
)
}
$: getMixins(object)
</script>
{#if object}
@ -88,11 +102,10 @@
{#if dir === 'column'}
<div class="ac-subtitle">
<div class="ac-subtitle-content">
<ClassAttributeBar
<DocAttributeBar
{object}
_class={object._class}
{mixins}
ignoreKeys={['name', 'description', 'fullDescription', 'private', 'archived']}
to={core.class.Doc}
/>
</div>
</div>

View File

@ -57,7 +57,7 @@
$: getMixins(issue, showAllMixins)
function getMixins (object: Issue, showAllMixins: boolean): void {
const descendants = hierarchy.getDescendants(tracker.class.Issue).map((p) => hierarchy.getClass(p))
const descendants = hierarchy.getDescendants(core.class.Doc).map((p) => hierarchy.getClass(p))
mixins = descendants.filter(
(m) =>

View File

@ -14,8 +14,7 @@
-->
<script lang="ts">
import type { EmployeeAccount } from '@hcengineering/contact'
import { DocumentQuery, getCurrentAccount, Ref } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Doc, DocumentQuery, getCurrentAccount, Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import type { Issue } from '@hcengineering/tracker'
@ -36,10 +35,10 @@
const subscribedQuery = createQuery()
$: subscribedQuery.query(
notification.class.LastView,
{ user: getCurrentAccount()._id, attachedToClass: tracker.class.Issue, lastView: { $ne: -1 } },
tracker.class.Issue,
{ 'notification:mixin:Collaborators.collaborators': getCurrentAccount()._id },
(result) => {
const newSub = result.map(({ attachedTo }) => attachedTo as Ref<Issue>)
const newSub = result.map((p) => p._id as Ref<Doc> as Ref<Issue>)
const curSub = subscribed._id.$in
if (curSub.length !== newSub.length || curSub.some((id, i) => newSub[i] !== id)) {
subscribed = { _id: { $in: newSub } }

View File

@ -15,7 +15,7 @@
-->
<script lang="ts">
import contact, { Contact, getName } from '@hcengineering/contact'
import { Class, ClassifierKind, Doc, Mixin, Obj, Ref } from '@hcengineering/core'
import core, { Class, ClassifierKind, Doc, Mixin, Obj, Ref } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { Asset, getResource, translate } from '@hcengineering/platform'
@ -44,7 +44,6 @@
let lastId: Ref<Doc> = _id
let lastClass: Ref<Class<Doc>> = _class
let object: Doc
let parentClass: Ref<Class<Doc>>
const client = getClient()
const hierarchy = client.getHierarchy()
@ -93,9 +92,9 @@
const dispatch = createEventDispatcher()
function getMixins (parentClass: Ref<Class<Doc>>, object: Doc, showAllMixins: boolean): void {
if (object === undefined || parentClass === undefined) return
const descendants = hierarchy.getDescendants(parentClass).map((p) => hierarchy.getClass(p))
function getMixins (object: Doc, showAllMixins: boolean): void {
if (object === undefined) return
const descendants = hierarchy.getDescendants(core.class.Doc).map((p) => hierarchy.getClass(p))
mixins = descendants.filter(
(m) =>
@ -106,7 +105,7 @@
)
}
$: getMixins(parentClass, object, showAllMixins)
$: getMixins(object, showAllMixins)
let ignoreKeys: string[] = []
let activityOptions = { enabled: true, showInput: true }
@ -172,7 +171,6 @@
$: getEditorOrDefault(realObjectClass, _id)
async function getEditorOrDefault (_class: Ref<Class<Doc>>, _id: Ref<Doc>): Promise<void> {
parentClass = hierarchy.getParentClass(_class)
await updateKeys()
mainEditor = getEditor(_class)
}
@ -232,10 +230,8 @@
}
async function getHeaderEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent | undefined> {
const clazz = hierarchy.getClass(_class)
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditorHeader)
if (editorMixin.editor != null) return editorMixin.editor
if (clazz.extends != null) return getHeaderEditor(clazz.extends)
const editorMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectEditorHeader)
return editorMixin?.editor
}
let headerEditor: AnyComponent | undefined = undefined
@ -265,7 +261,7 @@
ignoreMixins = new Set(ev.detail.ignoreMixins)
allowedCollections = ev.detail.allowedCollections ?? []
collectionArrays = ev.detail.collectionArrays ?? []
getMixins(parentClass, object, showAllMixins)
getMixins(object, showAllMixins)
updateKeys()
}
</script>

View File

@ -129,20 +129,13 @@
if (result.findIndex((p) => p.value === attribute.name) !== -1) return
if (result.findIndex((p) => p.value === value) !== -1) return
const { attrClass, category } = getAttributePresenterClass(hierarchy, attribute)
const typeClass = hierarchy.getClass(attrClass)
const mixin =
category === 'object'
? view.mixin.ObjectPresenter
: category === 'collection'
? view.mixin.CollectionPresenter
: view.mixin.AttributePresenter
let presenter = hierarchy.as(typeClass, mixin).presenter
let parent = typeClass.extends
while (presenter === undefined && parent !== undefined) {
const pclazz = hierarchy.getClass(parent)
presenter = hierarchy.as(pclazz, mixin).presenter
parent = pclazz.extends
}
const presenter = hierarchy.classHierarchyMixin(attrClass, mixin)?.presenter
if (presenter === undefined) return
const clazz = hierarchy.getClass(attribute.attributeOf)

View File

@ -70,13 +70,8 @@ export async function getObjectPresenter (
const hierarchy = client.getHierarchy()
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.ObjectPresenter
const clazz = hierarchy.getClass(_class)
let mixinClazz = hierarchy.getClass(_class)
let presenterMixin = hierarchy.as(clazz, mixin)
while (presenterMixin.presenter === undefined && mixinClazz.extends !== undefined) {
presenterMixin = hierarchy.as(mixinClazz, mixin)
mixinClazz = hierarchy.getClass(mixinClazz.extends)
}
if (presenterMixin.presenter === undefined) {
const presenterMixin = hierarchy.classHierarchyMixin(_class, mixin)
if (presenterMixin?.presenter === undefined) {
throw new Error(
`object presenter not found for class=${_class}, mixin=${mixin}, preserve key ${JSON.stringify(preserveKey)}`
)
@ -141,16 +136,8 @@ async function getAttributePresenter (
const presenterClass = getAttributePresenterClass(hierarchy, attribute)
const isCollectionAttr = presenterClass.category === 'collection'
const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.AttributePresenter
const clazz = hierarchy.getClass(presenterClass.attrClass)
let presenterMixin = hierarchy.as(clazz, mixin)
let parent = clazz.extends
while (presenterMixin.presenter === undefined && parent !== undefined) {
const pclazz = hierarchy.getClass(parent)
presenterClass.attrClass = parent
presenterMixin = hierarchy.as(pclazz, mixin)
parent = pclazz.extends
}
if (presenterMixin.presenter === undefined) {
const presenterMixin = hierarchy.classHierarchyMixin(presenterClass.attrClass, mixin)
if (presenterMixin?.presenter === undefined) {
throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey))
}
const resultKey = preserveKey.sortingKey ?? preserveKey.key
@ -657,14 +644,8 @@ export async function moveToSpace (
*/
export function getAdditionalHeader (client: TxOperations, _class: Ref<Class<Doc>>): AnyComponent[] | undefined {
const hierarchy = client.getHierarchy()
const clazz = hierarchy.getClass(_class)
let mixinClazz = hierarchy.getClass(_class)
let presenterMixin = hierarchy.as(clazz, view.mixin.ListHeaderExtra)
while (presenterMixin.presenters === undefined && mixinClazz.extends !== undefined) {
presenterMixin = hierarchy.as(mixinClazz, view.mixin.ListHeaderExtra)
mixinClazz = hierarchy.getClass(mixinClazz.extends)
}
return presenterMixin.presenters
const presenterMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ListHeaderExtra)
return presenterMixin?.presenters
}
export async function getObjectLinkFragment (
@ -673,12 +654,7 @@ export async function getObjectLinkFragment (
props: Record<string, any> = {},
component: AnyComponent = view.component.EditDoc
): Promise<Location> {
let clazz = hierarchy.getClass(object._class)
let provider = hierarchy.as(clazz, view.mixin.LinkProvider)
while (provider.encode === undefined && clazz.extends !== undefined) {
clazz = hierarchy.getClass(clazz.extends)
provider = hierarchy.as(clazz, view.mixin.LinkProvider)
}
const provider = hierarchy.classHierarchyMixin(object._class, view.mixin.LinkProvider)
if (provider?.encode !== undefined) {
const f = await getResource(provider.encode)
const res = await f(object, props)

View File

@ -15,7 +15,7 @@
<script lang="ts">
import type { Doc, Ref, Space } from '@hcengineering/core'
import core from '@hcengineering/core'
import notification from '@hcengineering/notification'
import notification, { LastView } from '@hcengineering/notification'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
@ -107,9 +107,9 @@
$: clazz = hierarchy.getClass(model.spaceClass)
$: lastEditMixin = hierarchy.as(clazz, notification.mixin.SpaceLastEdit)
function isChanged (space: Space, lastViews: Map<Ref<Doc>, number>): boolean {
function isChanged (space: Space, lastViews: LastView): boolean {
const field = lastEditMixin?.lastEditField
const lastView = lastViews.get(space._id)
const lastView = lastViews[space._id]
if (lastView === undefined || lastView === -1) return false
if (field === undefined) return false
const value = (space as any)[field]

View File

@ -15,7 +15,7 @@
<script lang="ts">
import type { Doc, Ref, Space } from '@hcengineering/core'
import core from '@hcengineering/core'
import notification from '@hcengineering/notification'
import notification, { LastView } from '@hcengineering/notification'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getResource, IntlString } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
@ -77,11 +77,11 @@
const lastViews = notificationClient.getLastViews()
const hierarchy = client.getHierarchy()
function isChanged (space: Space, lastViews: Map<Ref<Doc>, number>): boolean {
function isChanged (space: Space, lastViews: LastView): boolean {
const clazz = hierarchy.getClass(space._class)
const lastEditMixin = hierarchy.as(clazz, notification.mixin.SpaceLastEdit)
const field = lastEditMixin?.lastEditField
const lastView = lastViews.get(space._id)
const lastView = lastViews[space._id]
if (lastView === undefined || lastView === -1) return false
if (field === undefined) return false
const value = (space as any)[field]

View File

@ -166,7 +166,7 @@ async function processRefArrAttribute<T extends Doc> (
const res: Tx[] = []
if (attr.type._class === core.class.ArrOf) {
const arrOf = (attr.type as ArrOf<RefTo<Doc>>).of
if (arrOf._class === core.class.ArrOf) {
if (arrOf._class === core.class.RefTo) {
if (targetClasses.includes((arrOf as RefTo<Doc>).to)) {
const docs = await control.findAll(clazz, { [key]: oldValue })
for (const doc of docs) {

View File

@ -18,44 +18,46 @@ import chunter, { Backlink } from '@hcengineering/chunter'
import contact, { Employee, EmployeeAccount, formatName } from '@hcengineering/contact'
import core, {
Account,
AnyAttribute,
ArrOf,
AttachedDoc,
Class,
Data,
Doc,
generateId,
Hierarchy,
Obj,
IdMap,
Ref,
RefTo,
Space,
Timestamp,
toIdMap,
Tx,
TxCollectionCUD,
TxCreateDoc,
TxCUD,
TxFactory,
TxProcessor
TxMixin,
TxProcessor,
TxUpdateDoc
} from '@hcengineering/core'
import notification, {
ClassCollaborators,
Collaborators,
EmailNotification,
NotificationAction,
Notification,
NotificationProvider,
NotificationStatus,
NotificationType
} from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core'
import serverNotification, {
HTMLPresenter,
TextPresenter,
createLastViewTx,
getEmployeeAccount,
getEmployeeAccountById,
getUpdateLastViewTx,
getEmployee
HTMLPresenter,
TextPresenter
} from '@hcengineering/server-notification'
import { replaceAll } from './utils'
import { Content } from './types'
import { replaceAll } from './utils'
/**
* @public
@ -63,16 +65,35 @@ import { Content } from './types'
export async function OnBacklinkCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
const ptx = tx as TxCollectionCUD<Doc, Backlink>
let res: Tx[] = []
if (!checkTx(ptx, hierarchy)) return []
const receiver = await getEmployeeAccount(ptx.objectId as Ref<Employee>, control)
if (receiver === undefined) return []
const sender = await getEmployeeAccountById(ptx.modifiedBy, control)
if (sender === undefined) return []
const backlink = getBacklink(ptx)
const doc = await getBacklinkDoc(backlink, control)
return await createNotificationTxes(
if (doc !== undefined) {
const collab = hierarchy.as(doc, notification.mixin.Collaborators)
if (!collab.collaborators.includes(receiver._id)) {
const collabTx = control.txFactory.createTxMixin(
doc._id,
doc._class,
doc.space,
notification.mixin.Collaborators,
{
$push: {
collaborators: receiver._id
}
}
)
res.push(collabTx)
}
}
const notifyTx = await createNotificationTxes(
control,
ptx,
notification.ids.MentionNotification,
@ -81,6 +102,8 @@ export async function OnBacklinkCreate (tx: Tx, control: TriggerControl): Promis
receiver,
backlink.message
)
res = [...res, ...notifyTx]
return res
}
function checkTx (ptx: TxCollectionCUD<Doc, Backlink>, hierarchy: Hierarchy): boolean {
@ -142,23 +165,11 @@ async function getHtmlPart (doc: Doc, control: TriggerControl): Promise<string |
}
function getHTMLPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): HTMLPresenter | undefined {
let clazz: Ref<Class<Obj>> | undefined = _class
while (clazz !== undefined) {
const _class = hierarchy.getClass(clazz)
const presenter = hierarchy.as(_class, serverNotification.mixin.HTMLPresenter)
if (presenter.presenter != null) return presenter
clazz = _class.extends
}
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.HTMLPresenter)
}
function getTextPresenter (_class: Ref<Class<Doc>>, hierarchy: Hierarchy): TextPresenter | undefined {
let clazz: Ref<Class<Obj>> | undefined = _class
while (clazz !== undefined) {
const _class = hierarchy.getClass(clazz)
const presenter = hierarchy.as(_class, serverNotification.mixin.TextPresenter)
if (presenter.presenter != null) return presenter
clazz = _class.extends
}
return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.TextPresenter)
}
function fillTemplate (template: string, sender: string, doc: string, data: string): string {
@ -204,8 +215,7 @@ export async function createNotificationTxes (
doc: Doc | undefined,
sender: EmployeeAccount,
receiver: EmployeeAccount,
data: string = '',
action?: NotificationAction
data: string = ''
): Promise<Tx[]> {
const res: Tx[] = []
@ -213,22 +223,6 @@ export async function createNotificationTxes (
const content = await getContent(doc, senderName, type, control, data)
if (await isAllowed(control, receiver, notification.ids.PlatformNotification)) {
const target = await getEmployee(receiver.employee, control)
if (target !== undefined) {
const createNotificationTx = await getPlatformNotificationTx(
ptx,
type,
control.txFactory,
target,
content?.text,
action
)
res.push(createNotificationTx)
}
}
if (content !== undefined && (await isAllowed(control, receiver, notification.ids.EmailNotification))) {
const emailTx = await getEmailNotificationTx(ptx, senderName, content.text, content.html, content.subject, receiver)
if (emailTx !== undefined) {
@ -239,41 +233,6 @@ export async function createNotificationTxes (
return res
}
async function getPlatformNotificationTx (
ptx: TxCollectionCUD<Doc, AttachedDoc>,
type: Ref<NotificationType>,
txFactory: TxFactory,
target: Employee,
text?: string,
action?: NotificationAction
): Promise<TxCollectionCUD<Doc, Notification>> {
const createTx: TxCreateDoc<Notification> = {
objectClass: notification.class.Notification,
objectSpace: notification.space.Notifications,
objectId: generateId(),
modifiedOn: ptx.modifiedOn,
modifiedBy: ptx.modifiedBy,
space: ptx.space,
_id: generateId(),
_class: core.class.TxCreateDoc,
attributes: {
tx: ptx._id,
status: NotificationStatus.New,
type
} as unknown as Data<Notification>
}
if (text !== undefined) {
createTx.attributes.text = text
}
if (action !== undefined) {
createTx.attributes.action = action
}
return txFactory.createTxCollectionCUD(target._class, target._id, target.space, 'notifications', createTx)
}
async function getEmailNotificationTx (
ptx: TxCollectionCUD<Doc, AttachedDoc>,
sender: string,
@ -312,7 +271,7 @@ async function getUpdateLastViewTxes (
): Promise<Tx[]> {
const updatedUsers: Set<Ref<Account>> = new Set<Ref<Account>>()
const result: Tx[] = []
const tx = await getUpdateLastViewTx(control.findAll, _id, _class, modifiedOn, user)
const tx = await getUpdateLastViewTx(control.findAll, _id, modifiedOn, user)
if (tx !== undefined) {
updatedUsers.add(user)
result.push(tx)
@ -323,10 +282,10 @@ async function getUpdateLastViewTxes (
const value = (doc as any)[field]
if (value != null) {
for (const employeeId of Array.isArray(value) ? value : [value]) {
const account = (await control.modelDb.findAll(core.class.Account, { employee: employeeId }, { limit: 1 }))[0]
const account = await getEmployeeAccount(employeeId, control)
if (account !== undefined) {
if (updatedUsers.has(account._id)) continue
const assigneeTx = await createLastViewTx(control.findAll, _id, _class, account._id)
const assigneeTx = await createLastViewTx(control.findAll, _id, account._id)
if (assigneeTx !== undefined) {
updatedUsers.add(account._id)
result.push(assigneeTx)
@ -343,7 +302,11 @@ async function getUpdateLastViewTxes (
*/
export async function UpdateLastView (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx)
if (![core.class.TxUpdateDoc, core.class.TxCreateDoc, core.class.TxMixin].includes(actualTx._class)) {
if (
![core.class.TxUpdateDoc, core.class.TxCreateDoc, core.class.TxMixin, core.class.TxRemoveDoc].includes(
actualTx._class
)
) {
return []
}
@ -356,21 +319,22 @@ export async function UpdateLastView (tx: Tx, control: TriggerControl): Promise<
switch (actualTx._class) {
case core.class.TxCreateDoc: {
const createTx = actualTx as TxCreateDoc<Doc>
if (control.hierarchy.isDerived(createTx.objectClass, notification.class.LastView)) {
return []
}
if (control.hierarchy.isDerived(createTx.objectClass, core.class.AttachedDoc)) {
const doc = TxProcessor.createDoc2Doc(createTx as TxCreateDoc<AttachedDoc>)
const attachedTxes = await getUpdateLastViewTxes(
doc,
doc.attachedTo,
doc.attachedToClass,
createTx.modifiedOn,
createTx.modifiedBy,
control
)
const docClass = control.hierarchy.getClass(doc._class)
if (!control.hierarchy.hasMixin(docClass, notification.mixin.LastViewAttached)) return attachedTxes
if (control.hierarchy.classHierarchyMixin(doc.attachedToClass, notification.mixin.TrackedDoc) !== undefined) {
const attachedTxes = await getUpdateLastViewTxes(
doc,
doc.attachedTo,
doc.attachedToClass,
createTx.modifiedOn,
createTx.modifiedBy,
control
)
result.push(...attachedTxes)
}
}
if (control.hierarchy.classHierarchyMixin(createTx.objectClass, notification.mixin.TrackedDoc) !== undefined) {
const doc = TxProcessor.createDoc2Doc(createTx)
const parentTxes = await getUpdateLastViewTxes(
doc,
doc._id,
@ -379,21 +343,34 @@ export async function UpdateLastView (tx: Tx, control: TriggerControl): Promise<
createTx.modifiedBy,
control
)
return [...attachedTxes, ...parentTxes]
} else {
const doc = TxProcessor.createDoc2Doc(createTx)
return await getUpdateLastViewTxes(doc, doc._id, doc._class, createTx.modifiedOn, createTx.modifiedBy, control)
result.push(...parentTxes)
}
return result
}
case core.class.TxUpdateDoc:
case core.class.TxMixin: {
const tx = actualTx as TxCUD<Doc>
const doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
if (doc !== undefined) {
return await getUpdateLastViewTxes(doc, doc._id, doc._class, tx.modifiedOn, tx.modifiedBy, control)
if (control.hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.TrackedDoc) !== undefined) {
const doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
if (doc !== undefined) {
return await getUpdateLastViewTxes(doc, doc._id, doc._class, tx.modifiedOn, tx.modifiedBy, control)
}
}
break
}
case core.class.TxRemoveDoc: {
const tx = actualTx as TxCUD<Doc>
const lastViews = await control.findAll(notification.class.LastView, { [tx.objectId]: { $exists: true } })
for (const lastView of lastViews) {
const clearTx = control.txFactory.createTxUpdateDoc(lastView._class, lastView.space, lastView._id, {
$unset: {
[tx.objectId]: ''
}
})
result.push(clearTx)
}
return result
}
default:
break
}
@ -417,12 +394,241 @@ async function getBacklinkDoc (backlink: Backlink, control: TriggerControl): Pro
)[0]
}
async function getValueCollaborators (
value: any,
attr: AnyAttribute,
control: TriggerControl
): Promise<EmployeeAccount[]> {
const hierarchy = control.hierarchy
if (attr.type._class === core.class.RefTo) {
const to = (attr.type as RefTo<Doc>).to
if (hierarchy.isDerived(to, contact.class.Employee)) {
const acc = await getEmployeeAccount(value, control)
return acc !== undefined ? [acc] : []
} else if (hierarchy.isDerived(to, core.class.Account)) {
const acc = await getEmployeeAccountById(value, control)
return acc !== undefined ? [acc] : []
}
} else if (attr.type._class === core.class.ArrOf) {
const arrOf = (attr.type as ArrOf<RefTo<Doc>>).of
if (arrOf._class === core.class.RefTo) {
const to = (arrOf as RefTo<Doc>).to
if (hierarchy.isDerived(to, contact.class.Employee)) {
const employeeAccounts = await control.modelDb.findAll(contact.class.EmployeeAccount, {
employee: { $in: Array.isArray(value) ? value : [value] }
})
return employeeAccounts
} else if (hierarchy.isDerived(to, core.class.Account)) {
const employeeAccounts = await control.modelDb.findAll(contact.class.EmployeeAccount, {
_id: { $in: Array.isArray(value) ? value : [value] }
})
return employeeAccounts
}
}
}
return []
}
async function getKeyCollaborators (
doc: Doc,
value: any,
field: string,
control: TriggerControl
): Promise<EmployeeAccount[] | undefined> {
if (value !== undefined && value !== null) {
const attr = control.hierarchy.findAttribute(doc._class, field)
if (attr !== undefined) {
return await getValueCollaborators(value, attr, control)
}
}
}
async function getDocCollaborators (
doc: Doc,
mixin: ClassCollaborators,
control: TriggerControl
): Promise<EmployeeAccount[]> {
const collaborators: IdMap<EmployeeAccount> = new Map()
for (const field of mixin.fields) {
const value = (doc as any)[field]
const newCollaborators = await getKeyCollaborators(doc, value, field, control)
if (newCollaborators !== undefined) {
for (const newCollaborator of newCollaborators) {
collaborators.set(newCollaborator._id, newCollaborator)
}
}
}
return Array.from(collaborators.values())
}
function getMixinTx (
actualTx: TxCUD<Doc>,
control: TriggerControl,
collaborators: EmployeeAccount[]
): TxMixin<Doc, Collaborators> {
return control.txFactory.createTxMixin(
actualTx.objectId,
actualTx.objectClass,
actualTx.objectSpace,
notification.mixin.Collaborators,
{
collaborators: collaborators.map((p) => p._id)
}
)
}
/**
* @public
*/
export async function CreateCollaboratorDoc (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
const hierarchy = control.hierarchy
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
if (actualTx._class !== core.class.TxCreateDoc) return []
const mixin = hierarchy.classHierarchyMixin(actualTx.objectClass, notification.mixin.ClassCollaborators)
if (mixin !== undefined) {
const doc = TxProcessor.createDoc2Doc(actualTx)
const collaborators = await getDocCollaborators(doc, mixin, control)
const mixinTx = getMixinTx(actualTx, control, collaborators)
res.push(mixinTx)
}
return res
}
async function getNewCollaborators (
actualTx: TxUpdateDoc<Doc>,
mixin: ClassCollaborators,
doc: Doc,
control: TriggerControl
): Promise<EmployeeAccount[]> {
const newCollaborators: IdMap<EmployeeAccount> = new Map()
if (actualTx.operations.$push !== undefined) {
for (const key in actualTx.operations.$push) {
if (mixin.fields.includes(key)) {
const value = (actualTx.operations.$push as any)[key]
const newCollabs = await getKeyCollaborators(doc, value, key, control)
if (newCollabs !== undefined) {
for (const newCollab of newCollabs) {
newCollaborators.set(newCollab._id, newCollab)
}
}
}
}
}
for (const key in actualTx.operations) {
if (key.startsWith('$')) continue
if (mixin.fields.includes(key)) {
const value = (actualTx.operations as any)[key]
const newCollabs = await getKeyCollaborators(doc, value, key, control)
if (newCollabs !== undefined) {
for (const newCollab of newCollabs) {
newCollaborators.set(newCollab._id, newCollab)
}
}
}
}
return Array.from(newCollaborators.values())
}
/**
* @public
*/
export async function UpdateCollaboratorDoc (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
const actualTx = TxProcessor.extractTx(tx) as TxUpdateDoc<Doc>
if (actualTx._class !== core.class.TxUpdateDoc) return []
const clazz = hierarchy.getClass(actualTx.objectClass)
if (!hierarchy.hasMixin(clazz, notification.mixin.ClassCollaborators)) return []
const mixin = hierarchy.as(clazz, notification.mixin.ClassCollaborators)
const doc = (await control.findAll(actualTx.objectClass, { _id: actualTx.objectId }, { limit: 1 }))[0]
if (doc === undefined) return []
let collaborators: EmployeeAccount[] = []
let mixinTx: TxMixin<Doc, Collaborators> | undefined
if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) {
// we should handle change field and subscribe new collaborators
const collabMixin = hierarchy.as(doc, notification.mixin.Collaborators)
const oldCollaborators = await control.modelDb.findAll(contact.class.EmployeeAccount, {
_id: { $in: collabMixin.collaborators as Ref<EmployeeAccount>[] }
})
const collabs = toIdMap(oldCollaborators)
const newCollaborators = (await getNewCollaborators(actualTx, mixin, doc, control)).filter(
(p) => !collabs.has(p._id)
)
if (newCollaborators.length > 0) {
mixinTx = control.txFactory.createTxMixin(
actualTx.objectId,
actualTx.objectClass,
actualTx.objectSpace,
notification.mixin.Collaborators,
{
$push: {
collaborators: {
$each: newCollaborators.map((p) => p._id),
$position: 0
}
}
}
)
}
} else {
collaborators = await getDocCollaborators(doc, mixin, control)
mixinTx = getMixinTx(actualTx, control, collaborators)
}
return mixinTx !== undefined ? [mixinTx] : []
}
/**
* @public
*/
export async function OnAddCollborator (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const result: Tx[] = []
const actualTx = TxProcessor.extractTx(tx) as TxMixin<Doc, Collaborators>
if (actualTx._class !== core.class.TxMixin) return []
if (actualTx.mixin !== notification.mixin.Collaborators) return []
if (actualTx.attributes.collaborators !== undefined) {
for (const collab of actualTx.attributes.collaborators) {
const resTx = await createLastViewTx(control.findAll, actualTx.objectId, collab)
if (resTx !== undefined) {
result.push(resTx)
}
}
}
if (actualTx.attributes.$push?.collaborators !== undefined) {
const collab = actualTx.attributes.$push?.collaborators
if (typeof collab === 'object') {
if ('$each' in collab) {
for (const collaborator of collab.$each) {
const resTx = await createLastViewTx(control.findAll, actualTx.objectId, collaborator)
if (resTx !== undefined) {
result.push(resTx)
}
}
}
} else {
const resTx = await createLastViewTx(control.findAll, actualTx.objectId, collab)
if (resTx !== undefined) {
result.push(resTx)
}
}
}
return result
}
export * from './types'
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
OnBacklinkCreate,
UpdateLastView
UpdateLastView,
CreateCollaboratorDoc,
UpdateCollaboratorDoc,
OnAddCollborator
}
})

View File

@ -31,7 +31,6 @@ export const serverNotificationId = 'server-notification' as Plugin
export async function getUpdateLastViewTx (
findAll: TriggerControl['findAll'],
attachedTo: Ref<Doc>,
attachedToClass: Ref<Class<Doc>>,
lastView: number,
user: Ref<Account>
): Promise<TxUpdateDoc<LastView> | TxCreateDoc<LastView> | undefined> {
@ -39,8 +38,6 @@ export async function getUpdateLastViewTx (
await findAll(
notification.class.LastView,
{
attachedTo,
attachedToClass,
user
},
{ limit: 1 }
@ -48,21 +45,18 @@ export async function getUpdateLastViewTx (
)[0]
const factory = new TxFactory(user)
if (current !== undefined) {
if (current.lastView === -1) {
if (current[attachedTo] === -1 || current[attachedTo] >= lastView) {
return
}
const u = factory.createTxUpdateDoc(current._class, current.space, current._id, {
lastView
[attachedTo]: lastView
})
u.space = core.space.DerivedTx
return u
} else {
const u = factory.createTxCreateDoc(notification.class.LastView, notification.space.Notifications, {
user,
lastView,
attachedTo,
attachedToClass,
collection: 'lastViews'
[attachedTo]: lastView
})
u.space = core.space.DerivedTx
return u
@ -129,28 +123,28 @@ export async function getEmployee (employee: Ref<Employee>, control: TriggerCont
export async function createLastViewTx (
findAll: TriggerControl['findAll'],
attachedTo: Ref<Doc>,
attachedToClass: Ref<Class<Doc>>,
user: Ref<Account>
): Promise<TxCreateDoc<LastView> | undefined> {
): Promise<TxCreateDoc<LastView> | TxUpdateDoc<LastView> | undefined> {
const current = (
await findAll(
notification.class.LastView,
{
attachedTo,
attachedToClass,
user
},
{ limit: 1 }
)
)[0]
const factory = new TxFactory(user)
if (current === undefined) {
const factory = new TxFactory(user)
const u = factory.createTxCreateDoc(notification.class.LastView, notification.space.Notifications, {
user,
lastView: 1,
attachedTo,
attachedToClass,
collection: 'lastViews'
[attachedTo]: 1
})
u.space = core.space.DerivedTx
return u
} else if (current[attachedTo] === undefined) {
const u = factory.createTxUpdateDoc(current._class, current.space, current._id, {
[attachedTo]: 1
})
u.space = core.space.DerivedTx
return u
@ -186,6 +180,9 @@ export default plugin(serverNotificationId, {
},
trigger: {
OnBacklinkCreate: '' as Resource<TriggerFunc>,
UpdateLastView: '' as Resource<TriggerFunc>
UpdateLastView: '' as Resource<TriggerFunc>,
CreateCollaboratorDoc: '' as Resource<TriggerFunc>,
UpdateCollaboratorDoc: '' as Resource<TriggerFunc>,
OnAddCollborator: '' as Resource<TriggerFunc>
}
})

View File

@ -14,22 +14,12 @@
//
import { Employee } from '@hcengineering/contact'
import core, {
AttachedDoc,
concatLink,
Doc,
Ref,
Tx,
TxCollectionCUD,
TxProcessor,
TxUpdateDoc
} from '@hcengineering/core'
import { NotificationAction } from '@hcengineering/notification'
import { getMetadata, Resource } from '@hcengineering/platform'
import { AttachedDoc, concatLink, Doc, Ref, Tx, TxCollectionCUD } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { getEmployeeAccount, getEmployeeAccountById, getUpdateLastViewTx } from '@hcengineering/server-notification'
import { getEmployeeAccount, getEmployeeAccountById } from '@hcengineering/server-notification'
import { createNotificationTxes } from '@hcengineering/server-notification-resources'
import task, { Issue, Task, taskId } from '@hcengineering/task'
import task, { Issue, taskId } from '@hcengineering/task'
import view from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
@ -60,8 +50,7 @@ export async function addAssigneeNotification (
res: Tx[],
issue: Doc,
assignee: Ref<Employee>,
ptx: TxCollectionCUD<AttachedDoc, AttachedDoc>,
component?: Resource<string>
ptx: TxCollectionCUD<AttachedDoc, AttachedDoc>
): Promise<void> {
const sender = await getEmployeeAccountById(ptx.modifiedBy, control)
if (sender === undefined) {
@ -73,74 +62,11 @@ export async function addAssigneeNotification (
return
}
// eslint-disable-next-line
const action: NotificationAction = {
component: component ?? view.component.EditDoc,
objectId: issue._id,
objectClass: issue._class
} as NotificationAction
const result = await createNotificationTxes(
control,
ptx,
task.ids.AssigneedNotification,
issue,
sender,
receiver,
undefined,
action
)
const result = await createNotificationTxes(control, ptx, task.ids.AssigneedNotification, issue, sender, receiver)
res.push(...result)
}
/**
* @public
*/
export async function OnTaskUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx)
if (actualTx._class !== core.class.TxUpdateDoc) {
return []
}
const updateTx = actualTx as TxUpdateDoc<Task>
if (!control.hierarchy.isDerived(updateTx.objectClass, task.class.Task)) {
return []
}
const txes: Tx[] = []
const mainTx = await getUpdateLastViewTx(
control.findAll,
updateTx.objectId,
updateTx.objectClass,
updateTx.modifiedOn,
updateTx.modifiedBy
)
if (mainTx !== undefined) {
txes.push(mainTx)
}
if (updateTx.operations.assignee != null) {
const assignee = (
await control.modelDb.findAll(core.class.Account, { employee: updateTx.operations.assignee }, { limit: 1 })
)[0]
if (assignee !== undefined) {
const assigneeTx = await getUpdateLastViewTx(
control.findAll,
updateTx.objectId,
updateTx.objectClass,
updateTx.modifiedOn,
assignee._id
)
if (assigneeTx !== undefined) {
txes.push(assigneeTx)
}
}
}
return txes
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
function: {

View File

@ -31,7 +31,6 @@ import core, {
WithLookup
} from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import { Resource } from '@hcengineering/platform/lib/platform'
import serverCore, { TriggerControl } from '@hcengineering/server-core'
import { addAssigneeNotification } from '@hcengineering/server-task-resources'
import tracker, { Component, Issue, IssueParentInfo, Project, TimeSpendReport, trackerId } from '@hcengineering/tracker'
@ -82,14 +81,7 @@ export async function addTrackerAssigneeNotification (
assignee: Ref<Employee>,
ptx: TxCollectionCUD<Issue, AttachedDoc>
): Promise<void> {
await addAssigneeNotification(
control,
res,
issue,
assignee,
ptx,
tracker.component.EditIssue as unknown as Resource<string>
)
await addAssigneeNotification(control, res, issue, assignee, ptx)
}
/**
@ -169,7 +161,7 @@ export async function OnIssueUpdate (tx: Tx, control: TriggerControl): Promise<T
if (control.hierarchy.isDerived(createTx.objectClass, tracker.class.Issue)) {
const issue = TxProcessor.createDoc2Doc(createTx)
const res: Tx[] = []
await updateIssueParentEstimations(issue, res, control, [], issue.parents)
updateIssueParentEstimations(issue, res, control, [], issue.parents)
if (issue.assignee != null) {
await addTrackerAssigneeNotification(

View File

@ -115,9 +115,11 @@ test('my-issues', async ({ page }) => {
await expect(page.locator('.antiPanel-component')).toContainText(name)
await openIssue(page, name)
// click "Don't track"
await page.click('.buttons-group > div > .button')
await page.click('button:has-text("Appleseed John") >> nth=1')
await page.click('.selectPopup >> button:has-text("Appleseed John")')
await page.waitForTimeout(100)
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
await expect(page.locator('.antiPanel-component')).not.toContainText(name)
})