New Activity - squashed (#4032)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
Co-authored-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Kristina
2023-12-11 18:54:36 +07:00
committed by GitHub
co-authored by Andrey Sobolev
parent 119d65653a
commit ea3eb4af83
212 changed files with 7611 additions and 3159 deletions
+21 -21
View File
@@ -37,29 +37,29 @@
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"commander": "^8.1.0",
"@hcengineering/account": "^0.6.0",
"@hcengineering/core": "^0.6.28",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/tracker": "^0.6.13",
"@hcengineering/model-all": "^0.6.0",
"@hcengineering/model-telegram": "^0.6.0",
"@hcengineering/telegram": "^0.6.14",
"@hcengineering/client-resources": "^0.6.23",
"ws": "^8.10.0",
"@hcengineering/client": "^0.6.14",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/model": "^0.6.7",
"@hcengineering/recruit": "^0.6.21",
"faker": "~5.5.3",
"@hcengineering/model-recruit": "^0.6.0",
"@hcengineering/chunter": "^0.6.12",
"pdfkit": "~0.13.0",
"@hcengineering/attachment": "^0.6.9",
"@types/pdfkit": "~0.12.3",
"@hcengineering/task": "^0.6.13",
"jpeg-js": "~0.4.3",
"@hcengineering/client": "^0.6.14",
"@hcengineering/client-resources": "^0.6.23",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/minio": "^0.6.0",
"@hcengineering/model": "^0.6.7",
"@hcengineering/model-all": "^0.6.0",
"@hcengineering/model-recruit": "^0.6.0",
"@hcengineering/model-telegram": "^0.6.0",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/recruit": "^0.6.21",
"@hcengineering/server-token": "^0.6.7",
"@hcengineering/minio": "^0.6.0"
"@hcengineering/task": "^0.6.13",
"@hcengineering/telegram": "^0.6.14",
"@hcengineering/tracker": "^0.6.13",
"@hcengineering/notification": "^0.6.16",
"@types/pdfkit": "~0.12.3",
"commander": "^8.1.0",
"faker": "~5.5.3",
"jpeg-js": "~0.4.3",
"pdfkit": "~0.13.0",
"ws": "^8.10.0"
}
}
+13 -5
View File
@@ -1,5 +1,5 @@
import chunter, { Comment } from '@hcengineering/chunter'
import { AttachedData, Class, Doc, generateId, Ref, Space, TxOperations } from '@hcengineering/core'
import notification, { ChatMessage } from '@hcengineering/notification'
import faker from 'faker'
export interface CommentOptions {
@@ -20,17 +20,25 @@ export async function addComments<T extends Doc> (
): Promise<void> {
const commentsCount = options.min + faker.datatype.number(options.max)
for (let i = 0; i < commentsCount; i++) {
const commentId = `candidate-comment-${generateId()}-${i}` as Ref<Comment>
const commentId = `candidate-comment-${generateId()}-${i}` as Ref<ChatMessage>
const commentData: AttachedData<Comment> = {
const commentData: AttachedData<ChatMessage> = {
message: faker.lorem.paragraphs(options.paragraphMin + faker.datatype.number(options.paragraphMax))
}
await client.addCollection(chunter.class.Comment, space, objectId, _class, collection, commentData, commentId)
await client.addCollection(
notification.class.ChatMessage,
space,
objectId,
_class,
collection,
commentData,
commentId
)
if (faker.datatype.number(100) > options.updateFactor) {
const updateMsg = faker.lorem.paragraphs(options.paragraphMin + faker.datatype.number(options.paragraphMax))
await client.updateCollection(chunter.class.Comment, space, commentId, objectId, _class, collection, {
await client.updateCollection(notification.class.ChatMessage, space, commentId, objectId, _class, collection, {
message: updateMsg
})
}
+56 -56
View File
@@ -46,81 +46,81 @@
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"mongodb": "^4.11.0",
"commander": "^8.1.0",
"@hcengineering/account": "^0.6.0",
"@hcengineering/core": "^0.6.28",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/model-all": "^0.6.0",
"@hcengineering/model-telegram": "^0.6.0",
"@hcengineering/telegram": "^0.6.14",
"@hcengineering/client-resources": "^0.6.23",
"ws": "^8.10.0",
"@hcengineering/client": "^0.6.14",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/model": "^0.6.7",
"fast-equals": "^2.0.3",
"@elastic/elasticsearch": "^7.14.0",
"@hcengineering/account": "^0.6.0",
"@hcengineering/attachment": "^0.6.9",
"@hcengineering/client": "^0.6.14",
"@hcengineering/client-resources": "^0.6.23",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/dev-storage": "^0.6.6",
"@hcengineering/elastic": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-token": "^0.6.7",
"@hcengineering/lead": "^0.6.0",
"@hcengineering/minio": "^0.6.0",
"@hcengineering/model": "^0.6.7",
"@hcengineering/model-all": "^0.6.0",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/model-contact": "^0.6.1",
"@hcengineering/model-recruit": "^0.6.0",
"@hcengineering/model-telegram": "^0.6.0",
"@hcengineering/mongo": "^0.6.1",
"@hcengineering/dev-storage": "^0.6.6",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/openai": "^0.6.0",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/recruit": "^0.6.21",
"@hcengineering/rekoni": "^0.6.0",
"@hcengineering/server-attachment": "^0.6.1",
"@hcengineering/server-attachment-resources": "^0.6.0",
"xml2js": "~0.4.23",
"@hcengineering/model-recruit": "^0.6.0",
"@hcengineering/recruit": "^0.6.21",
"@hcengineering/task": "^0.6.13",
"@hcengineering/chunter": "^0.6.12",
"mime-types": "~2.1.34",
"@hcengineering/attachment": "^0.6.9",
"@hcengineering/server-contact": "^0.6.1",
"@hcengineering/server-contact-resources": "^0.6.0",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/server-setting": "^0.6.0",
"@hcengineering/server-setting-resources": "^0.6.0",
"@hcengineering/server-tool": "^0.6.0",
"@hcengineering/server-backup": "^0.6.0",
"@hcengineering/server-calendar": "^0.6.0",
"@hcengineering/server-calendar-resources": "^0.6.0",
"@hcengineering/server-chunter": "^0.6.0",
"@hcengineering/server-chunter-resources": "^0.6.0",
"@hcengineering/server-contact": "^0.6.1",
"@hcengineering/server-contact-resources": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-gmail": "^0.6.0",
"@hcengineering/server-gmail-resources": "^0.6.0",
"@hcengineering/server-hr": "^0.6.0",
"@hcengineering/server-hr-resources": "^0.6.0",
"@hcengineering/server-inventory": "^0.6.1",
"@hcengineering/server-inventory-resources": "^0.6.0",
"@hcengineering/server-lead": "^0.6.0",
"@hcengineering/server-lead-resources": "^0.6.0",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/server-recruit": "^0.6.0",
"@hcengineering/server-recruit-resources": "^0.6.0",
"@hcengineering/server-task": "^0.6.0",
"@hcengineering/server-task-resources": "^0.6.0",
"@hcengineering/server-tracker": "^0.6.0",
"@hcengineering/server-tracker-resources": "^0.6.0",
"@hcengineering/server-tags": "^0.6.0",
"@hcengineering/server-tags-resources": "^0.6.0",
"@hcengineering/server-calendar": "^0.6.0",
"@hcengineering/server-calendar-resources": "^0.6.0",
"@hcengineering/server-gmail": "^0.6.0",
"@hcengineering/server-gmail-resources": "^0.6.0",
"@hcengineering/server-telegram": "^0.6.0",
"@hcengineering/server-telegram-resources": "^0.6.0",
"@hcengineering/server-hr": "^0.6.0",
"@hcengineering/server-hr-resources": "^0.6.0",
"@hcengineering/server-request": "^0.6.0",
"@hcengineering/server-request-resources": "^0.6.0",
"@hcengineering/server-setting": "^0.6.0",
"@hcengineering/server-setting-resources": "^0.6.0",
"@hcengineering/server-tags": "^0.6.0",
"@hcengineering/server-tags-resources": "^0.6.0",
"@hcengineering/server-task": "^0.6.0",
"@hcengineering/server-task-resources": "^0.6.0",
"@hcengineering/server-telegram": "^0.6.0",
"@hcengineering/server-telegram-resources": "^0.6.0",
"@hcengineering/server-token": "^0.6.7",
"@hcengineering/server-tool": "^0.6.0",
"@hcengineering/server-tracker": "^0.6.0",
"@hcengineering/server-tracker-resources": "^0.6.0",
"@hcengineering/server-view": "^0.6.0",
"@hcengineering/server-view-resources": "^0.6.0",
"@hcengineering/rekoni": "^0.6.0",
"got": "^11.8.3",
"@hcengineering/tags": "^0.6.12",
"@hcengineering/server-backup": "^0.6.0",
"csv-parse": "~5.1.0",
"@hcengineering/lead": "^0.6.0",
"email-addresses": "^5.0.0",
"libphonenumber-js": "^1.9.46",
"@hcengineering/setting": "^0.6.11",
"@hcengineering/minio": "^0.6.0",
"@hcengineering/openai": "^0.6.0",
"@hcengineering/tracker": "^0.6.13"
"@hcengineering/tags": "^0.6.12",
"@hcengineering/task": "^0.6.13",
"@hcengineering/telegram": "^0.6.14",
"@hcengineering/tracker": "^0.6.13",
"commander": "^8.1.0",
"csv-parse": "~5.1.0",
"email-addresses": "^5.0.0",
"fast-equals": "^2.0.3",
"got": "^11.8.3",
"libphonenumber-js": "^1.9.46",
"mime-types": "~2.1.34",
"mongodb": "^4.11.0",
"ws": "^8.10.0",
"xml2js": "~0.4.23"
}
}
+5 -5
View File
@@ -14,7 +14,6 @@
//
import attachment from '@hcengineering/attachment'
import chunter, { Comment } from '@hcengineering/chunter'
import contact from '@hcengineering/contact'
import { deepEqual } from 'fast-equals'
import core, {
@@ -39,6 +38,7 @@ import { connect } from '@hcengineering/server-tool'
import tracker from '@hcengineering/tracker'
import tags, { TagCategory, TagElement, TagReference } from '@hcengineering/tags'
import { MongoClient } from 'mongodb'
import notification, { ChatMessage } from '@hcengineering/notification'
export const DOMAIN_COMMENT = 'comment' as Domain
@@ -322,11 +322,11 @@ export async function fixCommentDoubleIdCreate (workspaceId: WorkspaceId, transa
try {
const commentTxes = await connection.findAll(core.class.TxCollectionCUD, {
'tx._class': core.class.TxCreateDoc,
'tx.objectClass': chunter.class.Comment
'tx.objectClass': notification.class.ChatMessage
})
const commentTxesRemoved = await connection.findAll(core.class.TxCollectionCUD, {
'tx._class': core.class.TxRemoveDoc,
'tx.objectClass': chunter.class.Comment
'tx.objectClass': notification.class.ChatMessage
})
const removed = new Map(commentTxesRemoved.map((it) => [it.tx.objectId, it]))
// Do not checked removed
@@ -341,7 +341,7 @@ export async function fixCommentDoubleIdCreate (workspaceId: WorkspaceId, transa
objSet.add(cid)
if (has) {
// We have found duplicate one, let's rename it.
const doc = TxProcessor.createDoc2Doc<Comment>(c.tx as unknown as TxCreateDoc<Comment>)
const doc = TxProcessor.createDoc2Doc<ChatMessage>(c.tx as unknown as TxCreateDoc<ChatMessage>)
if (doc.message !== '' && doc.message.trim() !== '<p></p>') {
await connection.clean(DOMAIN_TX, [c._id])
if (oldValue.get(cid) === doc.message.trim()) {
@@ -351,7 +351,7 @@ export async function fixCommentDoubleIdCreate (workspaceId: WorkspaceId, transa
console.log('renaming', cid, doc.message)
// Remove previous transaction.
c.tx.objectId = generateId()
doc._id = c.tx.objectId as Ref<Comment>
doc._id = c.tx.objectId as Ref<ChatMessage>
await connection.upload(DOMAIN_TX, [c])
// Also we need to create snapsot
await connection.upload(DOMAIN_COMMENT, [doc])
+8 -9
View File
@@ -15,23 +15,22 @@
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/activity-resources": "^0.6.1",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/activity-resources": "^0.6.1"
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.11"
}
}
+5 -35
View File
@@ -13,18 +13,11 @@
// limitations under the License.
//
import type {
ActivityExtension,
ActivityExtensionKind,
ActivityFilter,
DisplayTx,
ExtraActivityComponent,
TxViewlet
} from '@hcengineering/activity'
import type { TxViewlet } from '@hcengineering/activity'
import core, { DOMAIN_MODEL, type Class, type Doc, type DocumentQuery, type Ref, type Tx } from '@hcengineering/core'
import { Mixin, Model, type Builder } from '@hcengineering/model'
import { TClass, TDoc } from '@hcengineering/model-core'
import type { Asset, IntlString, Resource } from '@hcengineering/platform'
import { Model, type Builder } from '@hcengineering/model'
import { TDoc } from '@hcengineering/model-core'
import type { Asset, IntlString } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import activity from './plugin'
@@ -46,31 +39,8 @@ export class TTxViewlet extends TDoc implements TxViewlet {
hideOnRemove!: boolean
}
@Model(activity.class.ActivityFilter, core.class.Doc, DOMAIN_MODEL)
export class TActivityFilter extends TDoc implements ActivityFilter {
label!: IntlString
filter!: Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>
}
@Model(activity.class.ActivityExtension, core.class.Doc, DOMAIN_MODEL)
export class TActivityExtension extends TDoc implements ActivityExtension {
ofClass!: Ref<Class<Doc>>
components?: Partial<Record<ActivityExtensionKind, AnyComponent>>
mentionClass?: Ref<Class<Doc>>
}
@Mixin(activity.mixin.ExtraActivityComponent, core.class.Class)
export class TExtraActivityComponent extends TClass implements ExtraActivityComponent {
component!: AnyComponent
}
export function createModel (builder: Builder): void {
builder.createModel(TTxViewlet, TActivityFilter, TExtraActivityComponent, TActivityExtension)
builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
label: activity.string.Attributes,
filter: activity.filter.AttributeFilter
})
builder.createModel(TTxViewlet)
}
export default activity
+1 -11
View File
@@ -12,18 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import type { DisplayTx } from '@hcengineering/activity'
import { activityId } from '@hcengineering/activity'
import activity from '@hcengineering/activity-resources/src/plugin'
import type { Resource, IntlString } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { type Doc, type Ref } from '@hcengineering/core'
export default mergeIds(activityId, activity, {
filter: {
AttributeFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>
},
string: {
Attributes: '' as IntlString
}
})
export default mergeIds(activityId, activity, {})
+3 -1
View File
@@ -36,6 +36,7 @@ import { boardOperation } from '@hcengineering/model-board'
import { hrOperation } from '@hcengineering/model-hr'
import { bitrixOperation } from '@hcengineering/model-bitrix'
import { calendarOperation } from '@hcengineering/model-calendar'
import { notificationServerOperation } from '@hcengineering/model-server-notification'
export const migrateOperations: [string, MigrateOperation][] = [
['core', coreOperation],
@@ -58,5 +59,6 @@ export const migrateOperations: [string, MigrateOperation][] = [
['board', boardOperation],
['hr', hrOperation],
['bitrix', bitrixOperation],
['inventiry', inventoryOperation]
['inventiry', inventoryOperation],
['notificationServer', notificationServerOperation]
]
+11 -10
View File
@@ -15,26 +15,27 @@
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/attachment": "^0.6.9",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/model-preference": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9"
}
}
+28 -1
View File
@@ -32,6 +32,7 @@ import core, { TAttachedDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction } from '@hcengineering/model-view'
import attachment from './plugin'
import notification from '@hcengineering/notification'
export { attachmentId } from '@hcengineering/attachment'
export { attachmentOperation } from './migration'
@@ -122,7 +123,33 @@ export function createModel (builder: Builder): void {
attachment.ids.TxAttachmentRemove
)
builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: attachment.class.Attachment,
action: 'create',
icon: attachment.icon.Attachment,
label: attachment.string.AddAttachment,
component: attachment.notification.NotificationAttachmentChanged
},
attachment.ids.NotificationAttachmentCreated
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: attachment.class.Attachment,
action: 'remove',
icon: attachment.icon.Attachment,
label: attachment.string.RemovedAttachment,
component: attachment.notification.NotificationAttachmentChanged
},
attachment.ids.NotificationAttachmentRemoved
)
builder.createDoc(notification.class.ActivityMessagesFilter, core.space.Model, {
label: attachment.string.FilterAttachments,
filter: attachment.filter.AttachmentsFilter
})
+9 -3
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import type { DisplayTx, TxViewlet } from '@hcengineering/activity'
import type { TxViewlet } from '@hcengineering/activity'
import { attachmentId } from '@hcengineering/attachment'
import attachment from '@hcengineering/attachment-resources/src/plugin'
import type { Ref, Doc } from '@hcengineering/core'
@@ -21,6 +21,7 @@ import type { IntlString, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import type { ActionCategory } from '@hcengineering/view'
import { type ActivityMessage, type DocUpdateMessageViewlet } from '@hcengineering/notification'
export default mergeIds(attachmentId, attachment, {
component: {
@@ -45,15 +46,20 @@ export default mergeIds(attachmentId, attachment, {
},
ids: {
TxAttachmentCreate: '' as Ref<TxViewlet>,
TxAttachmentRemove: '' as Ref<TxViewlet>
TxAttachmentRemove: '' as Ref<TxViewlet>,
NotificationAttachmentCreated: '' as Ref<DocUpdateMessageViewlet>,
NotificationAttachmentRemoved: '' as Ref<DocUpdateMessageViewlet>
},
activity: {
TxAttachmentCreate: '' as AnyComponent
},
notification: {
NotificationAttachmentChanged: '' as AnyComponent
},
category: {
Attachments: '' as Ref<ActionCategory>
},
filter: {
AttachmentsFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>
AttachmentsFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
}
})
+12
View File
@@ -243,6 +243,18 @@ export function createModel (builder: Builder): void {
calendar.ids.ReminderViewlet
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: calendar.class.Event,
icon: calendar.icon.Reminder,
action: 'update',
hideIfRemoved: true
},
calendar.ids.UpdateRemainderNotificationViewlet
)
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
+2 -1
View File
@@ -17,7 +17,7 @@ import { type TxViewlet } from '@hcengineering/activity'
import { calendarId } from '@hcengineering/calendar'
import calendar from '@hcengineering/calendar-resources/src/plugin'
import { type Ref } from '@hcengineering/core'
import { type NotificationGroup } from '@hcengineering/notification'
import { type DocUpdateMessageViewlet, type NotificationGroup } from '@hcengineering/notification'
import type { IntlString } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
@@ -66,6 +66,7 @@ export default mergeIds(calendarId, calendar, {
},
ids: {
ReminderViewlet: '' as Ref<TxViewlet>,
UpdateRemainderNotificationViewlet: '' as Ref<DocUpdateMessageViewlet>,
CalendarNotificationGroup: '' as Ref<NotificationGroup>
}
})
+16 -15
View File
@@ -15,32 +15,33 @@
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/attachment": "^0.6.9",
"@hcengineering/chunter": "^0.6.12",
"@hcengineering/chunter-resources": "^0.6.0",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/model-preference": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/model-preference": "^0.6.0"
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/workbench": "^0.6.9"
}
}
+109 -48
View File
@@ -195,6 +195,8 @@ export function createModel (builder: Builder, options = { addApplication: true
const spaceClasses = [chunter.class.Channel, chunter.class.DirectMessage]
spaceClasses.forEach((spaceClass) => {
builder.mixin(spaceClass, core.class.Class, notification.mixin.ActivityDoc, {})
builder.mixin(spaceClass, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: chunter.class.Message
@@ -475,18 +477,6 @@ export function createModel (builder: Builder, options = { addApplication: true
)
}
builder.mixin(chunter.class.Comment, core.class.Class, view.mixin.ObjectPresenter, {
presenter: chunter.component.CommentPresenter
})
builder.mixin(chunter.class.Comment, core.class.Class, view.mixin.CollectionPresenter, {
presenter: chunter.component.CommentsPresenter
})
builder.mixin(chunter.class.Comment, core.class.Class, view.mixin.ObjectPanel, {
component: chunter.component.CommentPanel
})
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
@@ -503,6 +493,22 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.ids.TxCommentCreate
)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
{
objectClass: notification.class.ChatMessage,
icon: chunter.icon.Chunter,
txClass: core.class.TxCreateDoc,
component: chunter.activity.TxCommentCreate,
label: chunter.string.LeftComment,
display: 'content',
editable: true,
hideOnRemove: true
},
chunter.ids.TxChatMessageCreate
)
createAction(
builder,
{
@@ -606,6 +612,19 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.ids.TxCommentRemove
)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
{
objectClass: notification.class.ChatMessage,
icon: chunter.icon.Chunter,
txClass: core.class.TxRemoveDoc,
display: 'inline',
hideOnRemove: true
},
chunter.ids.TxChatMessageRemove
)
builder.createDoc(
activity.class.TxViewlet,
core.space.Model,
@@ -629,9 +648,9 @@ export function createModel (builder: Builder, options = { addApplication: true
objectClass: chunter.class.Backlink,
icon: chunter.icon.Chunter,
txClass: core.class.TxCreateDoc,
component: chunter.activity.TxBacklinkCreate,
component: chunter.component.BacklinkContent,
label: chunter.string.MentionedIn,
labelComponent: chunter.activity.TxBacklinkReference,
labelComponent: chunter.component.BacklinkReference,
display: 'emphasized',
editable: false,
hideOnRemove: true
@@ -653,17 +672,7 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.ids.TxBacklinkRemove
)
builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
label: chunter.string.FilterComments,
filter: chunter.filter.CommentsFilter
})
builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
label: chunter.string.FilterPinnedComments,
filter: chunter.filter.PinnedCommentsFilter
})
builder.createDoc(activity.class.ActivityFilter, core.space.Model, {
builder.createDoc(notification.class.ActivityMessagesFilter, core.space.Model, {
label: chunter.string.FilterBacklinks,
filter: chunter.filter.BacklinksFilter
})
@@ -750,38 +759,41 @@ export function createModel (builder: Builder, options = { addApplication: true
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ActivityMessageExtension,
core.space.Model,
{
ofClass: chunter.class.Comment,
components: {
footer: chunter.component.CommentReactions,
action: chunter.component.ReactionsAction
}
ofMessage: notification.class.DocUpdateMessage,
components: [
{
kind: 'footer',
component: chunter.component.ReactionsPresenter
},
{
kind: 'action',
component: chunter.component.ActivityMessageReactionsAction
}
]
},
chunter.ids.ActivityExtension
chunter.ids.DocUpdateMessageExtension
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ActivityMessageExtension,
core.space.Model,
{
ofClass: chunter.class.Comment,
components: {
action: chunter.component.PinComment
}
ofMessage: notification.class.ChatMessage,
components: [
{
kind: 'footer',
component: chunter.component.ReactionsPresenter
},
{
kind: 'action',
component: chunter.component.ActivityMessageReactionsAction
}
]
},
chunter.ids.PinExtension
)
builder.createDoc(
activity.class.ActivityExtension,
core.space.Model,
{
ofClass: chunter.class.Backlink,
isMention: true
},
chunter.ids.BackLinkActivityExtension
chunter.ids.ChatMessageExtension
)
builder.createDoc(
@@ -801,6 +813,55 @@ export function createModel (builder: Builder, options = { addApplication: true
chunter.ids.ThreadNotification
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: chunter.class.Backlink,
action: 'create',
component: chunter.component.BacklinkContent,
labelComponent: chunter.component.NotificationBacklinkLabel,
hideIfRemoved: true
},
chunter.ids.NotificationBacklinkCreated
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: chunter.class.Backlink,
action: 'remove',
hideIfRemoved: true
},
chunter.ids.NotificationBacklinkRemoved
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: chunter.class.Reaction,
action: 'create',
component: chunter.component.NotificationReactionCreated,
label: chunter.string.Reacted,
onlyWithParent: true,
hideIfRemoved: true
},
chunter.ids.NotificationReactionCreated
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: chunter.class.Reaction,
action: 'remove',
hideIfRemoved: true
},
chunter.ids.NotificationReactionRemoved
)
createAction(builder, {
...viewTemplates.open,
target: chunter.class.Channel,
+24 -2
View File
@@ -14,7 +14,17 @@
//
import core, { TxOperations } from '@hcengineering/core'
import { type MigrateOperation, type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model'
import {
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
tryMigrate
} from '@hcengineering/model'
import { chunterId } from '@hcengineering/chunter'
import notification from '@hcengineering/notification'
import { DOMAIN_NOTIFICATION } from '@hcengineering/model-notification'
import { DOMAIN_COMMENT } from './index'
import chunter from './plugin'
export async function createGeneral (tx: TxOperations): Promise<void> {
@@ -79,8 +89,20 @@ async function createBacklink (tx: TxOperations): Promise<void> {
}
}
async function convertCommentsToChatMessages (client: MigrationClient): Promise<void> {
await client.update(DOMAIN_COMMENT, { _class: chunter.class.Comment }, { _class: notification.class.ChatMessage })
await client.move(DOMAIN_COMMENT, { _class: notification.class.ChatMessage }, DOMAIN_NOTIFICATION)
}
export const chunterOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {},
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, chunterId, [
{
state: 'create-chat-messages',
func: convertCommentsToChatMessages
}
])
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createGeneral(tx)
+22 -17
View File
@@ -13,11 +13,16 @@
// limitations under the License.
//
import type { ActivityExtension, DisplayTx, TxViewlet } from '@hcengineering/activity'
import type { TxViewlet } from '@hcengineering/activity'
import { chunterId, type Channel } from '@hcengineering/chunter'
import chunter from '@hcengineering/chunter-resources/src/plugin'
import type { Doc, Ref, Space } from '@hcengineering/core'
import { type NotificationGroup } from '@hcengineering/notification'
import {
type ActivityMessage,
type ActivityMessageExtension,
type DocUpdateMessageViewlet,
type NotificationGroup
} from '@hcengineering/notification'
import type { IntlString, Resource } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import type { AnyComponent, Location } from '@hcengineering/ui/src/types'
@@ -25,7 +30,6 @@ import type { Action, ActionCategory, ViewAction, ViewletDescriptor } from '@hce
export default mergeIds(chunterId, chunter, {
component: {
CommentPresenter: '' as AnyComponent,
ChannelPresenter: '' as AnyComponent,
DirectMessagePresenter: '' as AnyComponent,
MessagePresenter: '' as AnyComponent,
@@ -33,9 +37,11 @@ export default mergeIds(chunterId, chunter, {
Threads: '' as AnyComponent,
SavedMessages: '' as AnyComponent,
ChunterBrowser: '' as AnyComponent,
CommentReactions: '' as AnyComponent,
ReactionsPresenter: '' as AnyComponent,
ReactionsAction: '' as AnyComponent,
PinComment: '' as AnyComponent
ActivityMessageReactionsAction: '' as AnyComponent,
BacklinkContent: '' as AnyComponent,
BacklinkReference: '' as AnyComponent
},
action: {
MarkCommentUnread: '' as Ref<Action>,
@@ -74,15 +80,13 @@ export default mergeIds(chunterId, chunter, {
PinnedMessages: '' as IntlString,
SavedMessages: '' as IntlString,
ThreadMessage: '' as IntlString,
Reactions: '' as IntlString,
Emoji: '' as IntlString,
FilterComments: '' as IntlString,
FilterPinnedComments: '' as IntlString,
FilterBacklinks: '' as IntlString,
DM: '' as IntlString,
DMNotification: '' as IntlString,
ConfigLabel: '' as IntlString,
ConfigDescription: '' as IntlString
ConfigDescription: '' as IntlString,
Reacted: '' as IntlString
},
viewlet: {
Chat: '' as Ref<ViewletDescriptor>
@@ -93,15 +97,18 @@ export default mergeIds(chunterId, chunter, {
TxCommentRemove: '' as Ref<TxViewlet>,
TxBacklinkRemove: '' as Ref<TxViewlet>,
TxMessageCreate: '' as Ref<TxViewlet>,
TxChatMessageCreate: '' as Ref<TxViewlet>,
TxChatMessageRemove: '' as Ref<TxViewlet>,
ChunterNotificationGroup: '' as Ref<NotificationGroup>,
ActivityExtension: '' as Ref<ActivityExtension>,
BackLinkActivityExtension: '' as Ref<ActivityExtension>,
PinExtension: '' as Ref<ActivityExtension>
DocUpdateMessageExtension: '' as Ref<ActivityMessageExtension>,
ChatMessageExtension: '' as Ref<ActivityMessageExtension>,
NotificationBacklinkCreated: '' as Ref<DocUpdateMessageViewlet>,
NotificationBacklinkRemoved: '' as Ref<DocUpdateMessageViewlet>,
NotificationReactionCreated: '' as Ref<DocUpdateMessageViewlet>,
NotificationReactionRemoved: '' as Ref<DocUpdateMessageViewlet>
},
activity: {
TxCommentCreate: '' as AnyComponent,
TxBacklinkCreate: '' as AnyComponent,
TxBacklinkReference: '' as AnyComponent,
TxMessageCreate: '' as AnyComponent
},
space: {
@@ -114,8 +121,6 @@ export default mergeIds(chunterId, chunter, {
GetFragment: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>
},
filter: {
CommentsFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>,
PinnedCommentsFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>,
BacklinksFilter: '' as Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>
BacklinksFilter: '' as Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
}
})
+48 -16
View File
@@ -112,7 +112,7 @@ export class TContact extends TDoc implements Contact {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(notification.class.ChatMessage), notification.string.Comments)
comments?: number
@Prop(TypeString(), contact.string.Location)
@@ -221,6 +221,26 @@ export function createModel (builder: Builder): void {
TContactsTab
)
builder.mixin(contact.class.Contact, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(contact.class.Person, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(contact.mixin.Employee, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(contact.class.Organization, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(contact.class.Person, core.class.Class, notification.mixin.NotificationObjectPreposition, {
preposition: contact.string.For
})
builder.mixin(contact.mixin.Employee, core.class.Class, notification.mixin.NotificationObjectPreposition, {
preposition: contact.string.For
})
builder.mixin(contact.mixin.Employee, core.class.Class, view.mixin.ObjectFactory, {
component: contact.component.CreateEmployee
})
@@ -309,6 +329,16 @@ export function createModel (builder: Builder): void {
match: { 'operations.name': { $exists: true } }
})
builder.createDoc(notification.class.DocUpdateMessageViewlet, core.space.Model, {
objectClass: contact.class.Person,
action: 'update',
config: {
name: {
presenter: contact.notification.NotificationNameChanged
}
}
})
builder.createDoc<Viewlet>(
view.class.Viewlet,
core.space.Model,
@@ -938,32 +968,34 @@ export function createModel (builder: Builder): void {
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: contact.class.Person,
components: {
input: chunter.component.CommentInput
}
objectClass: contact.class.Person,
label: chunter.string.LeftComment
},
contact.ids.PersonActivityExtension
contact.ids.PersonChatMessageViewlet
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: contact.class.Organization,
components: {
input: chunter.component.CommentInput
}
objectClass: contact.mixin.Employee,
label: chunter.string.LeftComment
},
contact.ids.OrganizationActivityExtension
contact.ids.EmployeeChatMessageViewlet
)
builder.mixin(contact.class.Contact, core.class.Class, activity.mixin.ExtraActivityComponent, {
component: contact.component.ActivityChannelMessage
})
builder.createDoc(
notification.class.ChatMessageViewlet,
core.space.Model,
{
objectClass: contact.class.Organization,
label: chunter.string.LeftComment
},
contact.ids.OrganizationChatMessageViewlet
)
builder.createDoc(
notification.class.NotificationGroup,
+8 -7
View File
@@ -17,19 +17,20 @@
import { contactId } from '@hcengineering/contact'
import contact from '@hcengineering/contact-resources/src/plugin'
import type { Client, Doc, Ref } from '@hcengineering/core'
import {} from '@hcengineering/core'
import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineering/model-presentation'
import { type NotificationGroup } from '@hcengineering/notification'
import { type ChatMessageViewlet, type NotificationGroup } from '@hcengineering/notification'
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
import { type TemplateFieldFunc } from '@hcengineering/templates'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ActionCategory, type ViewAction } from '@hcengineering/view'
import { type ActivityExtension } from '@hcengineering/activity'
export default mergeIds(contactId, contact, {
activity: {
TxNameChange: '' as AnyComponent
},
notification: {
NotificationNameChanged: '' as AnyComponent
},
component: {
PersonPresenter: '' as AnyComponent,
ContactRefPresenter: '' as AnyComponent,
@@ -57,7 +58,6 @@ export default mergeIds(contactId, contact, {
AccountArrayEditor: '' as AnyComponent,
ChannelFilter: '' as AnyComponent,
MergePersons: '' as AnyComponent,
ActivityChannelMessage: '' as AnyComponent,
ChannelPanel: '' as AnyComponent,
ActivityChannelPresenter: '' as AnyComponent,
EmployeeFilter: '' as AnyComponent,
@@ -116,8 +116,9 @@ export default mergeIds(contactId, contact, {
ids: {
OrganizationNotificationGroup: '' as Ref<NotificationGroup>,
PersonNotificationGroup: '' as Ref<NotificationGroup>,
PersonActivityExtension: '' as Ref<ActivityExtension>,
OrganizationActivityExtension: '' as Ref<ActivityExtension>
OrganizationChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
PersonChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
EmployeeChatMessageViewlet: '' as Ref<ChatMessageViewlet>
},
action: {
KickEmployee: '' as Ref<Action>,
@@ -135,6 +136,6 @@ export default mergeIds(contactId, contact, {
GetContactName: '' as Resource<TemplateFieldFunc>,
GetContactFirstName: '' as Resource<TemplateFieldFunc>,
GetContactLastName: '' as Resource<TemplateFieldFunc>,
ContactTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>) => Promise<string>>
ContactTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
}
})
-1
View File
@@ -35,7 +35,6 @@
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/model-contact": "^0.6.1",
"@hcengineering/model-chunter": "^0.6.0",
"@hcengineering/model-calendar": "^0.6.0",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/hr": "^0.6.12",
+2 -3
View File
@@ -51,7 +51,6 @@ import {
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import calendar from '@hcengineering/model-calendar'
import chunter from '@hcengineering/model-chunter'
import contact, { TEmployee, TPersonAccount } from '@hcengineering/model-contact'
import core, { TAttachedDoc, TDoc, TSpace, TType } from '@hcengineering/model-core'
import view, { classPresenter, createAction } from '@hcengineering/model-view'
@@ -84,7 +83,7 @@ export class TDepartment extends TSpace implements Department {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(notification.class.ChatMessage), notification.string.Comments)
comments?: number
avatar?: string | null
@@ -158,7 +157,7 @@ export class TRequest extends TAttachedDoc implements Request {
@Hidden()
type!: Ref<RequestType>
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(notification.class.ChatMessage), notification.string.Comments)
comments?: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
-1
View File
@@ -25,7 +25,6 @@
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/model-workbench": "^0.6.1",
+14 -14
View File
@@ -14,7 +14,6 @@
//
import { type Domain, IndexKind, type Ref } from '@hcengineering/core'
import activity from '@hcengineering/activity'
import { type Category, type Product, type Variant, inventoryId } from '@hcengineering/inventory'
import { type Builder, Collection, Index, Model, Prop, TypeRef, TypeString, UX } from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
@@ -23,9 +22,10 @@ import { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import setting from '@hcengineering/setting'
import view, { type Viewlet } from '@hcengineering/view'
import notification from '@hcengineering/notification'
import chunter from '@hcengineering/model-chunter'
import inventory from './plugin'
import inventory from './plugin'
export { inventoryId } from '@hcengineering/inventory'
export { inventoryOperation } from './migration'
export { default } from './plugin'
@@ -79,6 +79,10 @@ export class TVariant extends TAttachedDoc implements Variant {
export function createModel (builder: Builder): void {
builder.createModel(TCategory, TProduct, TVariant)
builder.mixin(inventory.class.Product, core.class.Class, notification.mixin.ActivityDoc, {})
builder.mixin(inventory.class.Category, core.class.Class, notification.mixin.ActivityDoc, {})
builder.mixin(inventory.class.Variant, core.class.Class, notification.mixin.ActivityDoc, {})
builder.mixin(inventory.class.Category, core.class.Class, view.mixin.ObjectPresenter, {
presenter: inventory.component.CategoryPresenter
})
@@ -165,27 +169,23 @@ export function createModel (builder: Builder): void {
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: inventory.class.Product,
components: {
input: chunter.component.CommentInput
}
objectClass: inventory.class.Product,
label: chunter.string.LeftComment
},
inventory.ids.ProductActivityExtension
inventory.ids.ProductChatMessageViewlet
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: inventory.class.Category,
components: {
input: chunter.component.CommentInput
}
objectClass: inventory.class.Category,
label: chunter.string.LeftComment
},
inventory.ids.CategoryActivityExtension
inventory.ids.CategoryChatMessageViewlet
)
createAction(builder, {
+3 -4
View File
@@ -20,8 +20,7 @@ import inventory from '@hcengineering/inventory-resources/src/plugin'
import { type IntlString, mergeIds } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ActionCategory, type ViewAction, type Viewlet } from '@hcengineering/view'
import { type ActivityExtension } from '@hcengineering/activity'
import { type ChatMessageViewlet } from '@hcengineering/notification'
export default mergeIds(inventoryId, inventory, {
action: {
CreateSubcategory: '' as Ref<Action>
@@ -50,7 +49,7 @@ export default mergeIds(inventoryId, inventory, {
ConfigDescription: '' as IntlString
},
ids: {
ProductActivityExtension: '' as Ref<ActivityExtension>,
CategoryActivityExtension: '' as Ref<ActivityExtension>
ProductChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
CategoryChatMessageViewlet: '' as Ref<ChatMessageViewlet>
}
})
-1
View File
@@ -25,7 +25,6 @@
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/ui": "^0.6.11",
+13 -8
View File
@@ -43,7 +43,6 @@ import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification'
import setting from '@hcengineering/setting'
import { type ViewOptionsModel } from '@hcengineering/view'
import activity from '@hcengineering/activity'
import lead from './plugin'
export { leadId } from '@hcengineering/lead'
@@ -60,7 +59,7 @@ export class TFunnel extends TProject implements Funnel {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(notification.class.ChatMessage), notification.string.Comments)
comments?: number
}
@@ -103,6 +102,14 @@ export function createModel (builder: Builder): void {
builder.createModel(TFunnel, TLead, TCustomer)
builder.mixin(lead.class.Lead, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(lead.mixin.Customer, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(lead.class.Funnel, core.class.Class, notification.mixin.ActivityDoc, {})
builder.mixin(lead.class.Funnel, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: lead.class.Lead,
@@ -511,15 +518,13 @@ export function createModel (builder: Builder): void {
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: lead.class.Lead,
components: {
input: chunter.component.CommentInput
}
objectClass: lead.class.Lead,
label: chunter.string.LeftComment
},
lead.ids.LeadActivityExtension
lead.ids.LeadChatMessageViewlet
)
builder.mixin(lead.class.Lead, core.class.Class, task.mixin.KanbanCard, {
+2 -3
View File
@@ -17,13 +17,12 @@
import type { Ref } from '@hcengineering/core'
import { type Funnel, leadId } from '@hcengineering/lead'
import lead from '@hcengineering/lead-resources/src/plugin'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import { type ChatMessageViewlet, type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import type { IntlString } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { type ProjectType } from '@hcengineering/task'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ActionCategory, type Viewlet } from '@hcengineering/view'
import { type ActivityExtension } from '@hcengineering/activity'
export default mergeIds(leadId, lead, {
string: {
@@ -71,6 +70,6 @@ export default mergeIds(leadId, lead, {
FunnelNotificationGroup: '' as Ref<NotificationGroup>,
LeadCreateNotification: '' as Ref<NotificationType>,
AssigneeNotification: '' as Ref<NotificationType>,
LeadActivityExtension: '' as Ref<ActivityExtension>
LeadChatMessageViewlet: '' as Ref<ChatMessageViewlet>
}
})
+11 -10
View File
@@ -15,29 +15,30 @@
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/chunter": "^0.6.12",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-preference": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/view": "^0.6.9",
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/setting": "^0.6.11",
"@hcengineering/chunter": "^0.6.12"
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/workbench": "^0.6.9"
}
}
+148
View File
@@ -0,0 +1,148 @@
import { type Builder } from '@hcengineering/model'
import view, { createAction } from '@hcengineering/model-view'
import core from '@hcengineering/model-core'
import notification from './plugin'
export function buildActivityMessages (builder: Builder): void {
builder.mixin(notification.class.DocUpdateMessage, core.class.Class, notification.mixin.ActivityDoc, {})
builder.mixin(notification.class.ChatMessage, core.class.Class, notification.mixin.ActivityDoc, {})
builder.mixin(notification.class.ChatMessage, core.class.Class, view.mixin.CollectionPresenter, {
presenter: notification.component.ChatMessagesPresenter
})
builder.mixin(notification.class.ChatMessage, core.class.Class, view.mixin.ObjectPresenter, {
presenter: notification.component.ChatMessagePresenter
})
builder.mixin(notification.class.DocUpdateMessage, core.class.Class, view.mixin.ObjectPresenter, {
presenter: notification.component.DocUpdateMessagePresenter
})
builder.createDoc(
notification.class.ActivityMessageExtension,
core.space.Model,
{
ofMessage: notification.class.DocUpdateMessage,
components: [
{
kind: 'action',
component: notification.component.PinMessageAction
}
]
},
notification.ids.DocUpdateMessagePinExtension
)
builder.createDoc(
notification.class.ActivityMessageExtension,
core.space.Model,
{
ofMessage: notification.class.ChatMessage,
components: [
{
kind: 'action',
component: notification.component.PinMessageAction
}
]
},
notification.ids.ChatMessagePinExtension
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: notification.mixin.Collaborators,
action: 'update',
icon: notification.icon.Notifications,
component: notification.activity.TxCollaboratorsChange,
label: notification.string.ChangeCollaborators
},
notification.ids.NotificationCollaboratorsChanged
)
builder.mixin(notification.mixin.Collaborators, core.class.Class, notification.mixin.NotificationAttributePresenter, {
presenter: notification.component.NotificationCollaboratorsChanged
})
builder.createDoc(notification.class.ActivityMessagesFilter, core.space.Model, {
label: notification.string.Attributes,
filter: notification.filter.AttributesFilter
})
builder.createDoc(notification.class.ActivityMessagesFilter, core.space.Model, {
label: notification.string.Pinned,
filter: notification.filter.PinnedFilter
})
builder.createDoc(notification.class.ActivityMessagesFilter, core.space.Model, {
label: notification.string.Comments,
filter: notification.filter.ChatMessagesFilter
})
buildActions(builder)
}
function buildActions (builder: Builder): void {
createAction(
builder,
{
action: notification.actionImpl.DeleteChatMessage,
label: view.string.Delete,
icon: view.icon.Delete,
input: 'focus',
keyBinding: ['Backspace'],
category: notification.category.Notification,
target: notification.class.ChatMessage,
context: { mode: ['context', 'browser'], group: 'edit' }
},
notification.action.DeleteChatMessage
)
createAction(
builder,
{
action: notification.actionImpl.MarkAsReadInboxNotification,
label: notification.string.MarkAsRead,
icon: notification.icon.Notifications,
input: 'focus',
visibilityTester: notification.function.HasInboxNotifications,
category: notification.category.Notification,
target: notification.class.ActivityMessage,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
},
notification.action.MarkAsReadInboxNotification
)
createAction(
builder,
{
action: notification.actionImpl.MarkAsUnreadInboxNotification,
label: notification.string.MarkAsUnread,
icon: notification.icon.Track,
input: 'focus',
visibilityTester: notification.function.HasntInboxNotifications,
category: notification.category.Notification,
target: notification.class.ActivityMessage,
context: { mode: 'context', application: notification.app.Notification, group: 'edit' }
},
notification.action.MarkAsUnreadInboxNotification
)
createAction(
builder,
{
action: notification.actionImpl.DeleteInboxNotification,
label: notification.string.Archive,
icon: view.icon.Archive,
input: 'focus',
keyBinding: ['Backspace'],
category: notification.category.Notification,
target: notification.class.ActivityMessage,
context: { mode: ['context', 'browser'], group: 'edit' }
},
notification.action.DeleteInboxNotification
)
}
+255 -9
View File
@@ -17,9 +17,6 @@
import activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import {
DOMAIN_MODEL,
Hierarchy,
IndexKind,
type Account,
type AttachedDoc,
type Class,
@@ -27,20 +24,49 @@ import {
type Data,
type Doc,
type Domain,
DOMAIN_MODEL,
Hierarchy,
IndexKind,
type Ref,
type Timestamp,
type Tx,
type TxCUD
} from '@hcengineering/core'
import { ArrOf, Index, Mixin, Model, Prop, TypeRef, TypeString, UX, type Builder } from '@hcengineering/model'
import {
ArrOf,
type Builder,
Index,
Mixin,
Model,
Prop,
TypeMarkup,
TypeRef,
TypeString,
UX,
Collection as PropCollection,
TypeBoolean,
TypeDate
} from '@hcengineering/model'
import core, { TAttachedDoc, TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import {
notificationId,
type DocUpdateTx,
type ActivityMessage,
type ActivityMessageExtension,
type ActivityMessageExtensionKind,
type ChatMessage,
type ChatMessageViewlet,
type DocUpdateAction,
type DocAttributeUpdates,
type DocUpdateMessage,
type DocUpdateMessageViewlet,
type DocUpdateMessageViewletAttributesConfig,
type DocUpdates,
type DocUpdateTx,
inboxId,
type InboxNotification,
type DocNotifyContext,
type Notification,
type NotificationGroup,
type NotificationObjectPresenter,
@@ -50,12 +76,21 @@ import {
type NotificationSetting,
type NotificationStatus,
type NotificationTemplate,
type NotificationType
type NotificationType,
type ActivityMessagesFilter,
type ActivityDoc,
type NotificationObjectPreposition,
notificationId
} from '@hcengineering/notification'
import type { Asset, IntlString } from '@hcengineering/platform'
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import attachment from '@hcengineering/model-attachment'
import { type NotificationAttributePresenter } from '@hcengineering/view'
import notification from './plugin'
import { buildActivityMessages } from './activityMessages'
export { notificationId } from '@hcengineering/notification'
export { notificationOperation } from './migration'
export { notification as default }
@@ -139,6 +174,21 @@ export class TNotificationPreview extends TClass implements NotificationPreview
presenter!: AnyComponent
}
@Mixin(notification.mixin.ActivityDoc, core.class.Class)
export class TActivityDoc extends TClass implements ActivityDoc {
ignoreCollections?: string[]
}
@Mixin(notification.mixin.NotificationObjectPreposition, core.class.Class)
export class TNotificationObjectPreposition extends TClass implements NotificationObjectPreposition {
preposition!: IntlString
}
@Mixin(notification.mixin.NotificationAttributePresenter, core.class.Class)
export class TNotificationAttributePresenter extends TClass implements NotificationAttributePresenter {
presenter!: AnyComponent
}
@Model(notification.class.DocUpdates, core.class.Doc, DOMAIN_NOTIFICATION)
export class TDocUpdates extends TDoc implements DocUpdates {
@Index(IndexKind.Indexed)
@@ -155,6 +205,149 @@ export class TDocUpdates extends TDoc implements DocUpdates {
txes!: DocUpdateTx[]
}
@Model(notification.class.DocNotifyContext, core.class.Doc, DOMAIN_NOTIFICATION)
export class TDocNotifyContext extends TDoc implements DocNotifyContext {
@Prop(TypeRef(core.class.Account), core.string.Account)
@Index(IndexKind.Indexed)
user!: Ref<Account>
@Prop(TypeRef(core.class.Doc), core.string.AttachedTo)
@Index(IndexKind.Indexed)
attachedTo!: Ref<Doc>
@Prop(TypeRef(core.class.Class), core.string.AttachedToClass)
@Index(IndexKind.Indexed)
attachedToClass!: Ref<Class<Doc>>
@Prop(TypeBoolean(), core.string.Archived)
@Index(IndexKind.Indexed)
hidden!: boolean
@Prop(TypeDate(), core.string.Date)
@Index(IndexKind.Indexed)
lastViewedTimestamp?: Timestamp
@Prop(TypeDate(), core.string.Date)
@Index(IndexKind.Indexed)
lastUpdateTimestamp?: Timestamp
}
@Model(notification.class.ActivityMessage, core.class.AttachedDoc, DOMAIN_NOTIFICATION)
export class TActivityMessage extends TAttachedDoc implements ActivityMessage {
@Prop(TypeBoolean(), notification.string.Pinned)
@Index(IndexKind.Indexed)
isPinned?: boolean
@Prop(PropCollection(chunter.class.Reaction), chunter.string.Reactions)
reactions?: number
}
@Model(notification.class.DocUpdateMessage, notification.class.ActivityMessage, DOMAIN_NOTIFICATION)
export class TDocUpdateMessage extends TActivityMessage implements DocUpdateMessage {
@Prop(TypeRef(core.class.Doc), core.string.Object)
@Index(IndexKind.Indexed)
objectId!: Ref<Doc>
@Prop(TypeRef(core.class.Class), core.string.Class)
@Index(IndexKind.Indexed)
objectClass!: Ref<Class<Doc>>
@Prop(TypeRef(core.class.TxCUD), core.string.Object)
@Index(IndexKind.Indexed)
txId!: Ref<TxCUD<Doc>>
action!: DocUpdateAction
updateCollection?: string
attributeUpdates?: DocAttributeUpdates
}
@Model(notification.class.ChatMessage, notification.class.ActivityMessage, DOMAIN_NOTIFICATION)
export class TChatMessage extends TActivityMessage implements ChatMessage {
@Prop(TypeMarkup(), chunter.string.Message)
@Index(IndexKind.FullText)
message!: string
@Prop(PropCollection(attachment.class.Attachment), attachment.string.Attachments, {
shortLabel: attachment.string.Files
})
attachments?: number
@Prop(TypeBoolean(), core.string.Boolean)
@Index(IndexKind.Indexed)
isEdited?: boolean
}
@Model(notification.class.InboxNotification, core.class.Doc, DOMAIN_NOTIFICATION)
export class TInboxNotification extends TDoc implements InboxNotification {
@Prop(TypeRef(notification.class.ActivityMessage), core.string.AttachedTo)
@Index(IndexKind.Indexed)
attachedTo!: Ref<ActivityMessage>
@Prop(TypeRef(notification.class.ActivityMessage), core.string.AttachedToClass)
@Index(IndexKind.Indexed)
attachedToClass!: Ref<Class<ActivityMessage>>
@Prop(TypeRef(notification.class.DocNotifyContext), core.string.AttachedTo)
@Index(IndexKind.Indexed)
docNotifyContext!: Ref<DocNotifyContext>
@Prop(TypeRef(core.class.Account), core.string.Account)
@Index(IndexKind.Indexed)
user!: Ref<Account>
@Prop(TypeBoolean(), core.string.Boolean)
@Index(IndexKind.Indexed)
isViewed!: boolean
}
@Model(notification.class.DocUpdateMessageViewlet, core.class.Doc, DOMAIN_MODEL)
export class TDocUpdateMessageViewlet extends TDoc implements DocUpdateMessageViewlet {
@Prop(TypeRef(core.class.Doc), core.string.Class)
@Index(IndexKind.Indexed)
objectClass!: Ref<Class<Doc>>
@Prop(TypeString(), core.string.String)
@Index(IndexKind.Indexed)
action!: DocUpdateAction
label?: IntlString
labelComponent?: AnyComponent
valueAttr?: string
icon?: Asset
component?: AnyComponent
config?: DocUpdateMessageViewletAttributesConfig
hideIfRemoved?: boolean
onlyWithParent?: boolean
}
@Model(notification.class.ChatMessageViewlet, core.class.Doc, DOMAIN_MODEL)
export class TChatMessageViewlet extends TDoc implements ChatMessageViewlet {
@Prop(TypeRef(core.class.Doc), core.string.Class)
@Index(IndexKind.Indexed)
objectClass!: Ref<Class<Doc>>
label?: IntlString
hidden?: boolean
onlyWithParent?: boolean
}
@Model(notification.class.ActivityMessageExtension, core.class.Doc, DOMAIN_MODEL)
export class TActivityMessageExtension extends TDoc implements ActivityMessageExtension {
@Prop(TypeRef(notification.class.ActivityMessage), core.string.Class)
@Index(IndexKind.Indexed)
ofMessage!: Ref<Class<ActivityMessage>>
components!: { kind: ActivityMessageExtensionKind, component: AnyComponent }[]
}
@Model(notification.class.ActivityMessagesFilter, core.class.Doc, DOMAIN_MODEL)
export class TActivityMessagesFilter extends TDoc implements ActivityMessagesFilter {
label!: IntlString
filter!: Resource<(message: ActivityMessage, _class?: Ref<Doc>) => boolean>
}
export function createModel (builder: Builder): void {
builder.createModel(
TNotification,
@@ -167,7 +360,19 @@ export function createModel (builder: Builder): void {
TCollaborators,
TDocUpdates,
TNotificationObjectPresenter,
TNotificationPreview
TNotificationPreview,
TDocNotifyContext,
TDocUpdateMessage,
TChatMessage,
TActivityMessage,
TInboxNotification,
TDocUpdateMessageViewlet,
TActivityMessageExtension,
TChatMessageViewlet,
TActivityMessagesFilter,
TActivityDoc,
TNotificationObjectPreposition,
TNotificationAttributePresenter
)
// Temporarily disabled, we should think about it
@@ -228,6 +433,45 @@ export function createModel (builder: Builder): void {
notification.app.Notification
)
builder.createDoc(
workbench.class.Application,
core.space.Model,
{
label: notification.string.Inbox,
icon: notification.icon.Notifications,
alias: inboxId,
hidden: true,
locationResolver: notification.resolver.Location,
navigatorModel: {
aside: notification.component.InboxAside,
spaces: [],
specials: [
{
id: 'all',
component: notification.component.NewInbox,
icon: notification.icon.Activity,
label: notification.string.AllActivity,
componentProps: {
type: 'all',
label: notification.string.AllActivity
}
},
{
id: 'reactions',
component: notification.component.NewInbox,
icon: notification.icon.Emoji,
label: notification.string.Reactions,
componentProps: {
_class: chunter.class.Reaction,
label: notification.string.Reactions
}
}
]
}
},
notification.app.Inbox
)
createAction(
builder,
{
@@ -352,6 +596,8 @@ export function createModel (builder: Builder): void {
},
notification.ids.TxDmCreation
)
buildActivityMessages(builder)
}
export function generateClassNotificationTypes (
+31 -8
View File
@@ -15,7 +15,11 @@
//
import { type Doc, type Ref } from '@hcengineering/core'
import notification, { notificationId } from '@hcengineering/notification'
import notification, {
type ActivityMessageExtension,
type DocUpdateMessageViewlet,
notificationId
} from '@hcengineering/notification'
import { type IntlString, type Resource, mergeIds } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ActionCategory, type ViewAction } from '@hcengineering/view'
@@ -29,24 +33,39 @@ export default mergeIds(notificationId, notification, {
EmailNotification: '' as IntlString,
Collaborators: '' as IntlString,
Archive: '' as IntlString,
MarkAsUnread: '' as IntlString
MarkAsUnread: '' as IntlString,
MarkAsRead: '' as IntlString,
ChangeCollaborators: '' as IntlString,
AllActivity: '' as IntlString,
Threads: '' as IntlString,
Mentions: '' as IntlString,
Reactions: '' as IntlString
},
app: {
Notification: '' as Ref<Application>
Notification: '' as Ref<Application>,
Inbox: '' as Ref<Application>
},
activity: {
TxCollaboratorsChange: '' as AnyComponent,
TxDmCreation: '' as AnyComponent
},
ids: {
TxCollaboratorsChange: '' as Ref<TxViewlet>,
TxDmCreation: '' as Ref<TxViewlet>
TxDmCreation: '' as Ref<TxViewlet>,
NotificationCollaboratorsChanged: '' as Ref<DocUpdateMessageViewlet>,
DocUpdateMessagePinExtension: '' as Ref<ActivityMessageExtension>,
ChatMessagePinExtension: '' as Ref<ActivityMessageExtension>
},
component: {
NotificationSettings: '' as AnyComponent
NotificationSettings: '' as AnyComponent,
InboxAside: '' as AnyComponent,
ChatMessagePresenter: '' as AnyComponent,
DocUpdateMessagePresenter: '' as AnyComponent,
PinMessageAction: '' as AnyComponent
},
function: {
HasntNotifications: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
HasntNotifications: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
HasntInboxNotifications: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
HasInboxNotifications: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>
},
category: {
Notification: '' as Ref<ActionCategory>
@@ -60,6 +79,10 @@ export default mergeIds(notificationId, notification, {
actionImpl: {
Unsubscribe: '' as ViewAction,
Hide: '' as ViewAction,
MarkAsUnread: '' as ViewAction
MarkAsUnread: '' as ViewAction,
MarkAsUnreadInboxNotification: '' as ViewAction,
MarkAsReadInboxNotification: '' as ViewAction,
DeleteInboxNotification: '' as ViewAction,
DeleteChatMessage: '' as ViewAction
}
})
+17 -19
View File
@@ -25,32 +25,30 @@
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@anticrm/skillset": "^0.6.0",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/model-calendar": "^0.6.0",
"@hcengineering/model-chunter": "^0.6.0",
"@hcengineering/model-contact": "^0.6.1",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/model-tags": "^0.6.0",
"@hcengineering/model-task": "^0.6.0",
"@hcengineering/model-tracker": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/model-contact": "^0.6.1",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/recruit": "^0.6.21",
"@hcengineering/recruit-resources": "^0.6.0",
"@hcengineering/chunter": "^0.6.12",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/model-chunter": "^0.6.0",
"@hcengineering/view": "^0.6.9",
"@hcengineering/task": "^0.6.13",
"@hcengineering/setting": "^0.6.11",
"@hcengineering/model-task": "^0.6.0",
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/model-tracker": "^0.6.0",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/model-calendar": "^0.6.0",
"@hcengineering/model-tags": "^0.6.0",
"@anticrm/skillset": "^0.6.0"
"@hcengineering/task": "^0.6.13",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/workbench": "^0.6.9"
}
}
+49 -24
View File
@@ -56,7 +56,7 @@ import {
} from '@hcengineering/recruit'
import setting from '@hcengineering/setting'
import { type KeyBinding, type ViewOptionModel, type ViewOptionsModel } from '@hcengineering/view'
import activity from '@hcengineering/activity'
import recruit from './plugin'
import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
import { TOpinion, TReview } from './review-model'
@@ -85,7 +85,7 @@ export class TVacancy extends TProject implements Vacancy {
@Prop(TypeRef(contact.class.Organization), recruit.string.Company, { icon: contact.icon.Company })
company?: Ref<Organization>
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(notification.class.ChatMessage), notification.string.Comments)
comments?: number
@Prop(TypeString(), recruit.string.Vacancy)
@@ -197,6 +197,18 @@ export class TApplicantMatch extends TAttachedDoc implements ApplicantMatch {
export function createModel (builder: Builder): void {
builder.createModel(TVacancy, TCandidates, TCandidate, TApplicant, TReview, TOpinion, TVacancyList, TApplicantMatch)
builder.mixin(recruit.class.Vacancy, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(recruit.class.Applicant, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(recruit.class.Review, core.class.Class, notification.mixin.ActivityDoc, {})
builder.mixin(recruit.mixin.Candidate, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(recruit.class.Vacancy, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: recruit.class.Applicant,
@@ -832,7 +844,7 @@ export function createModel (builder: Builder): void {
key: 'title',
props: { kind: 'list', size: 'small', shouldShowName: false }
},
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
'comments',
{ key: '', displayProps: { grow: true } },
{
key: '$lookup.channels',
@@ -882,7 +894,7 @@ export function createModel (builder: Builder): void {
label: recruit.string.Applications,
props: { kind: 'list', size: 'small', shouldShowName: false }
},
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
'comments',
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
@@ -938,7 +950,7 @@ export function createModel (builder: Builder): void {
label: recruit.string.Applications
},
'description',
{ key: 'comments', displayProps: { key: 'comments', suffix: true } },
'comemnts',
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
@@ -1073,6 +1085,10 @@ export function createModel (builder: Builder): void {
titleProvider: recruit.function.AppTitleProvider
})
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ObjectIdentifier, {
provider: recruit.function.AppIdentifierProvider
})
builder.mixin(recruit.class.Review, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: recruit.function.RevTitleProvider
})
@@ -1569,39 +1585,33 @@ export function createModel (builder: Builder): void {
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: recruit.class.Vacancy,
components: {
input: chunter.component.CommentInput
}
objectClass: recruit.class.Vacancy,
label: chunter.string.LeftComment
},
recruit.ids.VacancyActivityExtension
recruit.ids.VacancyChatMessageViewlet
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: recruit.class.Applicant,
components: {
input: chunter.component.CommentInput
}
objectClass: recruit.class.Applicant,
label: chunter.string.LeftComment
},
recruit.ids.ApplicantActivityExtension
recruit.ids.ApplicantChatMessageViewlet
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: recruit.class.Review,
components: {
input: chunter.component.CommentInput
}
objectClass: recruit.class.Review,
label: chunter.string.LeftComment
},
recruit.ids.ReviewActivityExtension
recruit.ids.ReviewChatMessageViewlet
)
// Allow to use fuzzy search for mixins
@@ -1615,7 +1625,7 @@ export function createModel (builder: Builder): void {
propagate: [recruit.class.Applicant],
propagateClasses: [
tags.class.TagReference,
chunter.class.Comment,
notification.class.ChatMessage,
attachment.class.Attachment,
contact.class.Channel
]
@@ -1668,6 +1678,21 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: recruit.class.Applicant,
action: 'update',
config: {
status: {
iconPresenter: task.component.StateIconPresenter
}
}
},
recruit.ids.NotificationApplicantUpdated
)
createAction(
builder,
{
+10 -5
View File
@@ -14,7 +14,12 @@
//
import type { Client, Doc, Ref } from '@hcengineering/core'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import {
type ChatMessageViewlet,
type DocUpdateMessageViewlet,
type NotificationGroup,
type NotificationType
} from '@hcengineering/notification'
import type { IntlString, Resource, Status } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import { recruitId } from '@hcengineering/recruit'
@@ -22,7 +27,6 @@ import recruit from '@hcengineering/recruit-resources/src/plugin'
import { type ProjectType } from '@hcengineering/task'
import type { AnyComponent, Location } from '@hcengineering/ui/src/types'
import type { Action, ActionCategory, ViewAction, ViewQueryAction, Viewlet } from '@hcengineering/view'
import { type ActivityExtension } from '@hcengineering/activity'
export default mergeIds(recruitId, recruit, {
action: {
@@ -84,9 +88,10 @@ export default mergeIds(recruitId, recruit, {
AssigneeNotification: '' as Ref<NotificationType>,
ApplicationCreateNotification: '' as Ref<NotificationType>,
ReviewCreateNotification: '' as Ref<NotificationType>,
VacancyActivityExtension: '' as Ref<ActivityExtension>,
ApplicantActivityExtension: '' as Ref<ActivityExtension>,
ReviewActivityExtension: '' as Ref<ActivityExtension>
NotificationApplicantUpdated: '' as Ref<DocUpdateMessageViewlet>,
ApplicantChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
VacancyChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
ReviewChatMessageViewlet: '' as Ref<ChatMessageViewlet>
},
component: {
CreateApplication: '' as AnyComponent,
+2 -2
View File
@@ -3,12 +3,12 @@ import { type Domain, IndexKind, type Ref } from '@hcengineering/core'
import { Collection, Index, Model, Prop, TypeMarkup, TypeRef, TypeString, UX } from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import calendar, { TEvent } from '@hcengineering/model-calendar'
import chunter from '@hcengineering/model-chunter'
import contact from '@hcengineering/model-contact'
import core, { TAttachedDoc } from '@hcengineering/model-core'
import task from '@hcengineering/model-task'
import { type Applicant, type Candidate, type Opinion, type Review } from '@hcengineering/recruit'
import recruit from './plugin'
import notification from '@hcengineering/notification'
@Model(recruit.class.Review, calendar.class.Event)
@UX(recruit.string.Review, recruit.icon.Review, 'RVE', 'number')
@@ -47,7 +47,7 @@ export class TOpinion extends TAttachedDoc implements Opinion {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(notification.class.ChatMessage), notification.string.Comments)
comments?: number
@Prop(TypeMarkup(), recruit.string.Description)
+9 -10
View File
@@ -25,19 +25,18 @@
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-chunter": "^0.6.0",
"@hcengineering/request": "^0.6.6",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/chunter": "^0.6.12",
"@hcengineering/request-resources": "^0.6.0"
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/request": "^0.6.6",
"@hcengineering/request-resources": "^0.6.0",
"@hcengineering/ui": "^0.6.11"
}
}
+2 -3
View File
@@ -14,7 +14,6 @@
//
import activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import type { PersonAccount } from '@hcengineering/contact'
import contact from '@hcengineering/contact'
import { type Domain, IndexKind, type Ref, type Tx } from '@hcengineering/core'
@@ -73,11 +72,11 @@ export class TRequest extends TAttachedDoc implements Request {
@ReadOnly()
rejected?: Ref<PersonAccount>
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(notification.class.ChatMessage), notification.string.Comments)
comments?: number
}
@Mixin(request.mixin.RequestDecisionComment, chunter.class.Comment)
@Mixin(request.mixin.RequestDecisionComment, notification.class.ChatMessage)
export class TRequestDecisionComment extends TComment implements RequestDecisionComment {}
@Mixin(request.mixin.RequestPresenter, core.class.Class)
+1 -1
View File
@@ -33,7 +33,7 @@ export function createModel (builder: Builder): void {
})
builder.mixin<Class<Doc>, ObjectDDParticipant>(
chunter.class.Comment,
notification.class.ChatMessage,
core.class.Class,
serverCore.mixin.ObjectDDParticipant,
{
+8 -7
View File
@@ -15,12 +15,12 @@
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0"
},
@@ -28,10 +28,11 @@
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/server-core": "^0.6.1"
"@hcengineering/server-notification-resources": "^0.6.0"
}
}
+19 -1
View File
@@ -32,6 +32,7 @@ import serverNotification, {
} from '@hcengineering/server-notification'
export { serverNotificationId } from '@hcengineering/server-notification'
export { notificationServerOperation } from './migration'
@Mixin(serverNotification.mixin.HTMLPresenter, core.class.Class)
export class THTMLPresenter extends TClass implements HTMLPresenter {
@@ -62,8 +63,25 @@ export function createModel (builder: Builder): void {
trigger: serverNotification.trigger.OnBacklinkCreate
})
// NOTE: temporarily disabled
// builder.createDoc(serverCore.class.Trigger, core.space.Model, {
// trigger: serverNotification.trigger.OnReactionChanged,
// txMatch: {
// collection: 'reactions',
// objectClass: notification.class.ActivityMessage,
// _class: core.class.TxCollectionCUD
// }
// })
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.CollaboratorDocHandler
trigger: serverNotification.trigger.NotificationMessagesHandler
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverNotification.trigger.OnChatMessageSent,
txMatch: {
objectClass: notification.class.ChatMessage
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
+210
View File
@@ -0,0 +1,210 @@
//
// 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 core, {
SortingOrder,
TxFactory,
toFindResult,
toIdMap,
type AttachedDoc,
type Class,
type Doc,
type Ref,
type TxCUD,
type TxCollectionCUD,
type TxCreateDoc,
TxProcessor
} from '@hcengineering/core'
import { type MigrateOperation, type MigrationClient, type MigrationIterator, tryMigrate } from '@hcengineering/model'
import notification, { type DocUpdateMessage } from '@hcengineering/notification'
import { getAllObjectTransactions, type DocObjectCache, serverNotificationId } from '@hcengineering/server-notification'
import { generateDocUpdateMessages, type NotificationControl } from '@hcengineering/server-notification-resources'
function getNotificationControl (client: MigrationClient): NotificationControl {
const txFactory = new TxFactory(core.account.System, false)
return {
txFactory,
modelDb: client.model,
hierarchy: client.hierarchy,
findAll: async (_class, query, options) =>
toFindResult(await client.find(client.hierarchy.getDomain(_class), query, options))
}
}
async function generateDocUpdateMessageByTx (
tx: TxCUD<Doc>,
control: NotificationControl,
client: MigrationClient,
objectCache?: DocObjectCache
): Promise<void> {
const createCollectionCUDTxes = await generateDocUpdateMessages(tx, control, undefined, undefined, objectCache)
for (const collectionTx of createCollectionCUDTxes) {
const createTx = collectionTx.tx as TxCreateDoc<DocUpdateMessage>
const domain = client.hierarchy.getDomain(createTx.objectClass)
await client.create<DocUpdateMessage>(domain, {
_id: createTx.objectId,
_class: createTx.objectClass,
space: createTx.objectSpace,
modifiedOn: createTx.modifiedOn,
modifiedBy: createTx.modifiedBy,
createdOn: createTx.modifiedOn,
createdBy: createTx.createdBy,
...createTx.attributes
})
}
}
async function createDocUpdateMessages (client: MigrationClient): Promise<void> {
const activityDocs = await client.model.findAll(notification.mixin.ActivityDoc, {
_id: { $nin: [notification.class.DocUpdateMessage, notification.class.ChatMessage] }
})
const activityDocClasses = activityDocs.map(({ _id }) => _id)
const notificationControl = getNotificationControl(client)
const txClient: Pick<NotificationControl, 'hierarchy' | 'findAll'> = {
hierarchy: notificationControl.hierarchy,
findAll: notificationControl.findAll
}
let processed = 0
async function generateFor (_class: Ref<Class<Doc>>, documents: MigrationIterator<Doc>): Promise<void> {
while (true) {
const docs = await documents.next(50)
if (docs == null || docs.length === 0) {
break
}
const allTransactions = await getAllObjectTransactions(
txClient,
_class,
docs.map((it) => it._id)
)
// We need to find parent collection objects if missing
const byClass = new Map<Ref<Class<Doc>>, Set<Ref<Doc>>>()
for (const vv of allTransactions.values()) {
for (const v of vv) {
try {
const _cl = client.hierarchy.getBaseClass(v.objectClass)
const s = byClass.get(_cl) ?? new Set()
s.add(v.objectId)
byClass.set(_cl, s)
} catch {
console.log('class not found:', v.objectClass)
continue
}
if (v._class === core.class.TxCollectionCUD) {
try {
const vcol = v as TxCollectionCUD<Doc, AttachedDoc>
const _cl = client.hierarchy.getBaseClass(vcol.tx.objectClass)
const s = byClass.get(_cl) ?? new Set()
s.add(vcol.tx.objectId)
byClass.set(_cl, s)
} catch {
console.log('class not found:', (v as TxCollectionCUD<Doc, AttachedDoc>).tx.objectClass)
}
}
}
}
const docIds: Map<Ref<Doc>, Doc | null> = toIdMap(docs)
for (const [_class, classDocs] of byClass.entries()) {
const ids: Ref<Doc>[] = Array.from(classDocs.values()).filter((it) => !docIds.has(it))
if (ids.length > 0) {
for (const di of ids) {
docIds.set(di, null)
}
const edocs = await txClient.findAll(_class, { _id: { $in: ids } })
for (const ed of edocs) {
docIds.set(ed._id, ed)
}
}
}
for (const d of docs) {
processed += 1
if (processed % 1000 === 0) {
console.log('processed', processed)
}
const transactions = allTransactions.get(d._id) ?? []
for (const tx of transactions) {
const innerTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
if (!client.hierarchy.hasClass(innerTx.objectClass)) {
console.log('class not found:', innerTx.objectClass)
continue
}
try {
await generateDocUpdateMessageByTx(tx, notificationControl, client, {
docs: docIds,
transactions: allTransactions
})
} catch (e: any) {
console.error('error processing:', d._id, e.stack)
}
}
}
}
await documents.close()
}
for (const activityClass of activityDocClasses) {
console.log('processing', activityClass)
if (client.hierarchy.isMixin(activityClass)) {
// Skip mixins
continue
}
const domain = client.hierarchy.getDomain(activityClass)
await generateFor(
activityClass,
await client.traverse<Doc>(
domain,
{
_class: activityClass,
space: { $ne: core.space.Model }
},
{
sort: {
createdOn: SortingOrder.Ascending
}
}
)
)
}
console.log('process-finished', processed)
}
export const notificationServerOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, serverNotificationId, [
{
state: 'notifications',
func: createDocUpdateMessages
}
])
},
async upgrade (): Promise<void> {}
}
+4 -4
View File
@@ -27,11 +27,11 @@
"dependencies": {
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-recruit": "^0.6.0",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/openai": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/model-chunter": "^0.6.0",
"@hcengineering/model-recruit": "^0.6.0"
"@hcengineering/platform": "^0.6.9",
"@hcengineering/server-core": "^0.6.1"
}
}
+2 -2
View File
@@ -22,8 +22,8 @@ import core, { DOMAIN_CONFIGURATION } from '@hcengineering/core'
import openai, { type OpenAIConfiguration } from '@hcengineering/openai/src/plugin'
import serverCore from '@hcengineering/server-core'
import chunter from '@hcengineering/model-chunter'
import recruit from '@hcengineering/model-recruit'
import notification from '@hcengineering/notification'
export { openAIId } from '@hcengineering/openai/src/plugin'
@@ -49,7 +49,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: openai.trigger.AsyncOnGPTRequest,
txMatch: {
objectClass: { $in: [chunter.class.Comment, recruit.class.ApplicantMatch] },
objectClass: { $in: [notification.class.ChatMessage, recruit.class.ApplicantMatch] },
_class: core.class.TxCreateDoc
}
})
+13
View File
@@ -328,6 +328,19 @@ export function createModel (builder: Builder): void {
setting.ids.TxIntegrationDisable
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: setting.class.Integration,
icon: setting.icon.Integrations,
label: setting.string.IntegrationWith,
action: 'update',
hideIfRemoved: true
},
setting.ids.UpdateIntegrationNotificationViewlet
)
builder.mixin(core.class.TypeString, core.class.Class, view.mixin.ObjectEditor, {
editor: setting.component.StringTypeEditor
})
+7 -2
View File
@@ -21,7 +21,11 @@ import setting from '@hcengineering/setting-resources/src/plugin'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ActionCategory, type ViewAction } from '@hcengineering/view'
import { type TemplateFieldFunc } from '@hcengineering/templates'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import {
type DocUpdateMessageViewlet,
type NotificationGroup,
type NotificationType
} from '@hcengineering/notification'
export default mergeIds(settingId, setting, {
activity: {
@@ -32,7 +36,8 @@ export default mergeIds(settingId, setting, {
EnumSetting: '' as Ref<Doc>,
Configure: '' as Ref<Doc>,
SettingNotificationGroup: '' as Ref<NotificationGroup>,
IntegrationDisabledNotification: '' as Ref<NotificationType>
IntegrationDisabledNotification: '' as Ref<NotificationType>,
UpdateIntegrationNotificationViewlet: '' as Ref<DocUpdateMessageViewlet>
},
component: {
EnumSetting: '' as AnyComponent,
+9 -9
View File
@@ -15,25 +15,25 @@
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/model": "^0.6.7",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/tags": "^0.6.12",
"@hcengineering/tags-resources": "^0.6.0",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/view": "^0.6.9",
"@hcengineering/model-view": "^0.6.0"
"@hcengineering/view": "^0.6.9"
}
}
+9 -10
View File
@@ -25,24 +25,23 @@
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/model-contact": "^0.6.1",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/model-tags": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/model-contact": "^0.6.1",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/tags": "^0.6.12",
"@hcengineering/task": "^0.6.13",
"@hcengineering/task-resources": "^0.6.0",
"@hcengineering/model-chunter": "^0.6.0",
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/tags": "^0.6.12",
"@hcengineering/model-tags": "^0.6.0"
"@hcengineering/workbench": "^0.6.9"
}
}
+2 -2
View File
@@ -42,7 +42,6 @@ import {
UX
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter'
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@hcengineering/model-core'
import view, { createAction, template, actionTemplates as viewTemplates } from '@hcengineering/model-view'
import { type IntlString } from '@hcengineering/platform'
@@ -61,6 +60,7 @@ import {
import type { AnyComponent } from '@hcengineering/ui/src/types'
import { type ViewAction } from '@hcengineering/view'
import task from './plugin'
import notification from '@hcengineering/notification'
export { taskId } from '@hcengineering/task'
export { createProjectType, createSequence, taskOperation } from './migration'
@@ -95,7 +95,7 @@ export class TTask extends TAttachedDoc implements Task {
@Prop(Collection(tags.class.TagReference, task.string.TaskLabels), task.string.TaskLabels)
labels?: number
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(notification.class.ChatMessage), notification.string.Comments)
comments?: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
+2 -1
View File
@@ -59,7 +59,8 @@ export default mergeIds(taskId, task, {
StateRefPresenter: '' as AnyComponent,
StatusSelector: '' as AnyComponent,
TemplatesIcon: '' as AnyComponent,
TypesView: '' as AnyComponent
TypesView: '' as AnyComponent,
StateIconPresenter: '' as AnyComponent
},
space: {
TasksPublic: '' as Ref<Space>
+27
View File
@@ -133,6 +133,19 @@ export function createModel (builder: Builder): void {
telegram.ids.TxMessage
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: telegram.class.Message,
action: 'create',
icon: contact.icon.Telegram,
component: telegram.notification.NotificationMessageCreated,
label: telegram.string.SharedMessages
},
telegram.ids.NotificationMessageCreated
)
builder.createDoc(
contact.class.ChannelProvider,
core.space.Model,
@@ -177,6 +190,20 @@ export function createModel (builder: Builder): void {
telegram.ids.TxSharedCreate
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: telegram.class.SharedMessages,
action: 'create',
icon: contact.icon.Telegram,
component: telegram.component.SharedMessages,
label: telegram.string.SharedMessages,
hideIfRemoved: true
},
telegram.ids.NotificationMessageShared
)
builder.createDoc(
notification.class.NotificationGroup,
core.space.Model,
+4 -2
View File
@@ -21,7 +21,7 @@ import telegram from '@hcengineering/telegram-resources/src/plugin'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import type { TxViewlet } from '@hcengineering/activity'
import { type TemplateFieldFunc } from '@hcengineering/templates'
import { type NotificationGroup } from '@hcengineering/notification'
import { type DocUpdateMessageViewlet, type NotificationGroup } from '@hcengineering/notification'
export default mergeIds(telegramId, telegram, {
string: {
@@ -42,7 +42,9 @@ export default mergeIds(telegramId, telegram, {
TxMessage: '' as Ref<TxViewlet>,
TxSharedCreate: '' as Ref<TxViewlet>,
NewMessageNotificationViewlet: '' as Ref<TxViewlet>,
NotificationGroup: '' as Ref<NotificationGroup>
NotificationGroup: '' as Ref<NotificationGroup>,
NotificationMessageShared: '' as Ref<DocUpdateMessageViewlet>,
NotificationMessageCreated: '' as Ref<DocUpdateMessageViewlet>
},
function: {
GetCurrentEmployeeTG: '' as Resource<TemplateFieldFunc>,
+12 -13
View File
@@ -26,27 +26,26 @@
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/model-contact": "^0.6.1",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/model-task": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/model-contact": "^0.6.1",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/setting": "^0.6.11",
"@hcengineering/tags": "^0.6.12",
"@hcengineering/task": "^0.6.13",
"@hcengineering/tracker": "^0.6.13",
"@hcengineering/tracker-resources": "^0.6.0",
"@hcengineering/model-task": "^0.6.0",
"@hcengineering/model-chunter": "^0.6.0",
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/setting": "^0.6.11",
"@hcengineering/task": "^0.6.13",
"@hcengineering/tags": "^0.6.12"
"@hcengineering/workbench": "^0.6.9"
}
}
+114 -25
View File
@@ -18,7 +18,6 @@ import { type Builder } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import task from '@hcengineering/model-task'
import view from '@hcengineering/model-view'
import chunter from '@hcengineering/model-chunter'
import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification'
import setting from '@hcengineering/setting'
@@ -259,6 +258,10 @@ function defineFilters (builder: Builder): void {
component: tracker.component.MilestoneFilter
})
builder.mixin(tracker.class.Milestone, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: tracker.function.MilestoneTitleProvider
})
//
// Project
//
@@ -280,6 +283,10 @@ function defineFilters (builder: Builder): void {
filters: []
})
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: tracker.function.ComponentTitleProvider
})
//
// Type Milestone Status
//
@@ -438,6 +445,20 @@ export function createModel (builder: Builder): void {
TTypeRemainingTime
)
builder.mixin(tracker.class.Project, core.class.Class, notification.mixin.ActivityDoc, {})
builder.mixin(tracker.class.Issue, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(tracker.class.Milestone, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(tracker.class.Component, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
builder.mixin(tracker.class.IssueTemplate, core.class.Class, notification.mixin.ActivityDoc, {
ignoreCollections: ['comments']
})
defineViewlets(builder)
defineStatusCategories(builder)
@@ -495,6 +516,82 @@ export function createModel (builder: Builder): void {
tracker.ids.TxIssueCreated
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: tracker.class.Issue,
action: 'update',
icon: tracker.icon.Issue,
config: {
status: {
iconPresenter: tracker.component.IssueStatusIcon
},
priority: {
iconPresenter: tracker.component.PriorityIconPresenter
},
estimation: {
icon: tracker.icon.Estimation
}
}
},
tracker.ids.NotificationIssueUpdated
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: tracker.class.Issue,
action: 'create',
icon: tracker.icon.Issue,
valueAttr: 'title'
},
tracker.ids.NotificationIssueCreated
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: tracker.class.Issue,
action: 'remove',
icon: tracker.icon.Issue,
valueAttr: 'title'
},
tracker.ids.NotificationIssueRemoved
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: tracker.class.Milestone,
action: 'update',
config: {
status: {
iconPresenter: tracker.component.MilestoneStatusIcon
}
}
},
tracker.ids.NotificationMilestoneUpdated
)
builder.createDoc(
notification.class.DocUpdateMessageViewlet,
core.space.Model,
{
objectClass: tracker.class.IssueTemplate,
action: 'update',
config: {
priority: {
iconPresenter: tracker.component.PriorityIconPresenter
}
}
},
tracker.ids.NotificationIssueTemplateUpdated
)
defineApplication(builder, { myIssuesId, allIssuesId, issuesId, componentsId, milestonesId, templatesId })
defineActions(builder, issuesId, componentsId, myIssuesId)
@@ -528,51 +625,43 @@ export function createModel (builder: Builder): void {
})
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: tracker.class.Issue,
components: {
input: chunter.component.CommentInput
}
objectClass: tracker.class.Issue,
label: notification.string.LeftComment
},
tracker.ids.IssueActivityExtension
tracker.ids.IssueChatMessageViewlet
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: tracker.class.IssueTemplate,
components: {
input: chunter.component.CommentInput
}
objectClass: tracker.class.IssueTemplate,
label: notification.string.LeftComment
},
tracker.ids.IssueTemplateActivityExtension
tracker.ids.IssueTemplateChatMessageViewlet
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: tracker.class.Component,
components: {
input: chunter.component.CommentInput
}
objectClass: tracker.class.Component,
label: notification.string.LeftComment
},
tracker.ids.ComponentActivityExtension
tracker.ids.ComponentChatMessageViewlet
)
builder.createDoc(
activity.class.ActivityExtension,
notification.class.ChatMessageViewlet,
core.space.Model,
{
ofClass: tracker.class.Milestone,
components: {
input: chunter.component.CommentInput
}
objectClass: tracker.class.Milestone,
label: notification.string.LeftComment
},
tracker.ids.MilestoneActivityExtension
tracker.ids.MilestoneChatMessageViewlet
)
builder.createDoc(
+17 -7
View File
@@ -22,8 +22,13 @@ import tracker from '@hcengineering/tracker-resources/src/plugin'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ViewAction, type Viewlet } from '@hcengineering/view'
import { type Application } from '@hcengineering/workbench'
import { type ActivityExtension, type TxViewlet } from '@hcengineering/activity'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import { type TxViewlet } from '@hcengineering/activity'
import {
type ChatMessageViewlet,
type DocUpdateMessageViewlet,
type NotificationGroup,
type NotificationType
} from '@hcengineering/notification'
export default mergeIds(trackerId, tracker, {
string: {
@@ -75,11 +80,16 @@ export default mergeIds(trackerId, tracker, {
TxIssueCreated: '' as Ref<TxViewlet>,
TrackerNotificationGroup: '' as Ref<NotificationGroup>,
AssigneeNotification: '' as Ref<NotificationType>,
IssueActivityExtension: '' as Ref<ActivityExtension>,
IssueTemplateActivityExtension: '' as Ref<ActivityExtension>,
ComponentActivityExtension: '' as Ref<ActivityExtension>,
MilestoneActivityExtension: '' as Ref<ActivityExtension>,
BaseProjectType: '' as Ref<ProjectType>
BaseProjectType: '' as Ref<ProjectType>,
NotificationIssueUpdated: '' as Ref<DocUpdateMessageViewlet>,
NotificationIssueCreated: '' as Ref<DocUpdateMessageViewlet>,
NotificationIssueRemoved: '' as Ref<DocUpdateMessageViewlet>,
NotificationMilestoneUpdated: '' as Ref<DocUpdateMessageViewlet>,
NotificationIssueTemplateUpdated: '' as Ref<DocUpdateMessageViewlet>,
IssueChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
IssueTemplateChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
ComponentChatMessageViewlet: '' as Ref<ChatMessageViewlet>,
MilestoneChatMessageViewlet: '' as Ref<ChatMessageViewlet>
},
completion: {
IssueQuery: '' as Resource<ObjectSearchFactory>,
+5 -5
View File
@@ -41,7 +41,6 @@ import {
UX
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import chunter from '@hcengineering/model-chunter'
import core, { TAttachedDoc, TDoc, TStatus, TType } from '@hcengineering/model-core'
import task, { TTask, TProject as TTaskProject } from '@hcengineering/model-task'
import { type IntlString } from '@hcengineering/platform'
@@ -65,6 +64,7 @@ import {
type TimeSpendReport
} from '@hcengineering/tracker'
import tracker from './plugin'
import notification from '@hcengineering/notification'
export const DOMAIN_TRACKER = 'tracker' as Domain
@@ -252,7 +252,7 @@ export class TIssue extends TTask implements Issue {
*/
@Model(tracker.class.IssueTemplate, core.class.Doc, DOMAIN_TRACKER)
@UX(tracker.string.IssueTemplate, tracker.icon.Issue, 'PROCESS')
@UX(tracker.string.IssueTemplate, tracker.icon.IssueTemplates, 'PROCESS')
export class TIssueTemplate extends TDoc implements IssueTemplate {
@Prop(TypeString(), tracker.string.Title)
@Index(IndexKind.FullText)
@@ -288,7 +288,7 @@ export class TIssueTemplate extends TDoc implements IssueTemplate {
@Prop(ArrOf(TypeRef(tracker.class.IssueTemplate)), tracker.string.IssueTemplate)
children!: IssueTemplateChild[]
@Prop(Collection(chunter.class.Comment), tracker.string.Comments)
@Prop(Collection(notification.class.ChatMessage), tracker.string.Comments)
comments!: number
@Prop(Collection(attachment.class.Attachment), tracker.string.Attachments)
@@ -336,7 +336,7 @@ export class TComponent extends TDoc implements Component {
@Prop(TypeRef(contact.mixin.Employee), tracker.string.ComponentLead)
lead!: Ref<Employee> | null
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(notification.class.ChatMessage), notification.string.Comments)
comments!: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
@@ -362,7 +362,7 @@ export class TMilestone extends TDoc implements Milestone {
@Index(IndexKind.Indexed)
status!: MilestoneStatus
@Prop(Collection(chunter.class.Comment), chunter.string.Comments)
@Prop(Collection(notification.class.ChatMessage), notification.string.Comments)
comments!: number
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
+21 -3
View File
@@ -84,8 +84,11 @@ import {
type ViewOptionsModel,
type Viewlet,
type ViewletDescriptor,
type ViewletPreference
type ViewletPreference,
type NotificationAttributePresenter,
type ObjectIdentifier
} from '@hcengineering/view'
import view from './plugin'
export { viewId } from '@hcengineering/view'
@@ -124,6 +127,9 @@ export function classPresenter (
builder.mixin(_class, core.class.Class, view.mixin.ActivityAttributePresenter, {
presenter: activity
})
builder.mixin(_class, core.class.Class, view.mixin.NotificationAttributePresenter, {
presenter: activity
})
}
}
@@ -204,6 +210,11 @@ export class TActivityAttributePresenter extends TClass implements ActivityAttri
presenter!: AnyComponent
}
@Mixin(view.mixin.NotificationAttributePresenter, core.class.Class)
export class TNotificationAttributePresenter extends TClass implements NotificationAttributePresenter {
presenter!: AnyComponent
}
@Mixin(view.mixin.SpacePresenter, core.class.Class)
export class TSpacePresenter extends TClass implements SpacePresenter {
presenter!: AnyComponent
@@ -258,7 +269,12 @@ export class TObjectFactory extends TClass implements ObjectFactory {
@Mixin(view.mixin.ObjectTitle, core.class.Class)
export class TObjectTitle extends TClass implements ObjectTitle {
titleProvider!: Resource<<T extends Doc>(client: Client, ref: Ref<T>) => Promise<string>>
titleProvider!: Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<string>>
}
@Mixin(view.mixin.ObjectIdentifier, core.class.Class)
export class TObjectIdentifier extends TClass implements ObjectIdentifier {
provider!: Resource<<T extends Doc>(client: Client, ref: Ref<T>, doc?: T) => Promise<string>>
}
@Mixin(view.mixin.ListHeaderExtra, core.class.Class)
@@ -408,6 +424,7 @@ export function createModel (builder: Builder): void {
TAttributePresenter,
TAttributeFilterPresenter,
TActivityAttributePresenter,
TNotificationAttributePresenter,
TListItemPresenter,
TCollectionEditor,
TCollectionPresenter,
@@ -436,7 +453,8 @@ export function createModel (builder: Builder): void {
TFilteredView,
TAllValuesFunc,
TAggregation,
TGroupping
TGroupping,
TObjectIdentifier
)
classPresenter(
+15
View File
@@ -0,0 +1,15 @@
export function groupByArray<T, K> (array: T[], keyProvider: (item: T) => K): Map<K, T[]> {
const result = new Map<K, T[]>()
array.forEach((item) => {
const key = keyProvider(item)
if (!result.has(key)) {
result.set(key, [item])
} else {
result.get(key)?.push(item)
}
})
return result
}
+6
View File
@@ -151,6 +151,12 @@ export class Hierarchy {
return data
}
hasClass (_class: Ref<Class<Obj>>): boolean {
const data = this.classifiers.get(_class)
return !(data === undefined || this.isInterface(data))
}
getClassOrInterface (_class: Ref<Class<Obj>>): Class<Obj> {
const data = this.classifiers.get(_class)
if (data === undefined) {
+2
View File
@@ -30,3 +30,5 @@ export * from './utils'
export * from './backup'
export * from './status'
export * from './typeof'
export * from './common'
export * from './time'
+8
View File
@@ -175,6 +175,14 @@ export abstract class MemDb extends TxProcessor implements Storage {
return toFindResult(res, total)
}
async findOne<T extends Doc>(
_class: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
): Promise<WithLookup<T> | undefined> {
return (await this.findAll(_class, query, { ...options, limit: 1 }))[0]
}
/**
* Only in model find without lookups and sorting.
*/
+22
View File
@@ -0,0 +1,22 @@
function isToday (time: number): boolean {
const current = new Date()
const target = new Date(time)
return (
current.getDate() === target.getDate() &&
current.getMonth() === target.getMonth() &&
current.getFullYear() === target.getFullYear()
)
}
export function getDisplayTime (time: number): string {
let options: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric' }
if (!isToday(time)) {
options = {
month: 'numeric',
day: 'numeric',
...options
}
}
return new Date(time).toLocaleString('default', options)
}
+22 -6
View File
@@ -1,21 +1,21 @@
import core, {
Client,
Doc,
DOMAIN_MIGRATION,
Data,
Doc,
DocumentQuery,
Domain,
FindOptions,
Hierarchy,
IncOptions,
MigrationState,
ModelDb,
ObjQueryType,
PushOptions,
Ref,
UnsetOptions,
MigrationState,
generateId,
TxOperations,
Data
UnsetOptions,
generateId
} from '@hcengineering/core'
import { ModelLogger } from './utils'
@@ -48,6 +48,14 @@ export type MigrationDocumentQuery<T extends Doc> = {
[key: string]: any
}
/**
* @public
*/
export interface MigrationIterator<T extends Doc> {
next: (count: number) => Promise<T[] | null>
close: () => Promise<void>
}
/**
* @public
* Client to perform model upgrades
@@ -62,6 +70,13 @@ export interface MigrationClient {
options?: Omit<FindOptions<T>, 'lookup'>
) => Promise<T[]>
// Traverse documents
traverse: <T extends Doc>(
domain: Domain,
query: MigrationDocumentQuery<T>,
options?: Omit<FindOptions<T>, 'lookup'>
) => Promise<MigrationIterator<T>>
// Allow to raw update documents inside domain.
update: <T extends Doc>(
domain: Domain,
@@ -77,8 +92,9 @@ export interface MigrationClient {
// Move documents per domain
move: <T extends Doc>(sourceDomain: Domain, query: DocumentQuery<T>, targetDomain: Domain) => Promise<MigrationResult>
create: <T extends Doc>(domain: Domain, doc: T) => Promise<void>
create: <T extends Doc>(domain: Domain, doc: T | T[]) => Promise<void>
delete: <T extends Doc>(domain: Domain, _id: Ref<T>) => Promise<void>
deleteMany: <T extends Doc>(domain: Domain, ids: Ref<T>[]) => Promise<void>
hierarchy: Hierarchy
model: ModelDb
@@ -35,15 +35,20 @@
export let kind: 'default' | 'no-border' | 'link' | 'regular' = 'default'
export let size: 'small' | 'medium' | 'large' = 'small'
export let noShift: boolean = false
export let shouldShowAvatar: boolean = true
export let accent: boolean = false
export let inline: boolean = false
const dispatch = createEventDispatcher()
type TEdits = 'day' | 'month' | 'year' | 'hour' | 'min'
interface IEdits {
id: TEdits
value: number
el?: HTMLElement
}
const editsType: TEdits[] = ['day', 'month', 'year', 'hour', 'min']
const getIndex = (id: TEdits): number => editsType.indexOf(id)
const today = new Date()
@@ -297,155 +302,13 @@
adaptValue()
</script>
<button
bind:this={datePresenter}
class="datetime-button {kind} {size}"
class:notSelected={!value}
class:editable
class:edit
on:click={() => {
if (editable && !opened && withDate) openPopup()
else if (editable && !opened && mode === DateRangeMode.TIME) edit = true
}}
>
{#if edit}
{#if withDate}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
bind:this={edits[0].el}
class="digit"
tabindex="0"
on:keydown={(ev) => {
keyDown(ev, edits[0].id)
}}
on:focus={() => {
focused(edits[0].id)
}}
on:blur={(ev) => {
unfocus(ev, edits[0].id)
}}
>
{#if edits[0].value > -1}
{edits[0].value.toString().padStart(2, '0')}
{:else}<Label label={ui.string.DD} />{/if}
</span>
<span class="separator">.</span>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
bind:this={edits[1].el}
class="digit"
tabindex="0"
on:keydown={(ev) => {
keyDown(ev, edits[1].id)
}}
on:focus={() => {
focused(edits[1].id)
}}
on:blur={(ev) => {
unfocus(ev, edits[1].id)
}}
>
{#if edits[1].value > -1}
{edits[1].value.toString().padStart(2, '0')}
{:else}<Label label={ui.string.MM} />{/if}
</span>
<span class="separator">.</span>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<span
bind:this={edits[2].el}
class="digit"
tabindex="0"
on:keydown={(ev) => {
keyDown(ev, edits[2].id)
}}
on:focus={() => {
focused(edits[2].id)
}}
on:blur={(ev) => {
unfocus(ev, edits[2].id)
}}
>
{#if edits[2].value > -1}
{edits[2].value.toString().padStart(4, '0')}
{:else}<Label label={ui.string.YYYY} />{/if}
</span>
{/if}
{#if withTime}
{#if mode === DateRangeMode.DATETIME}
<div class="time-divider" />
{/if}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<span
bind:this={edits[3].el}
class="digit"
tabindex="0"
on:keydown={(ev) => {
keyDown(ev, edits[3].id)
}}
on:focus={() => {
focused(edits[3].id)
}}
on:blur={(ev) => {
unfocus(ev, edits[3].id)
}}
>
{#if edits[3].value > -1}
{edits[3].value.toString().padStart(2, '0')}
{:else}<Label label={ui.string.HH} />{/if}
</span>
<span class="separator">:</span>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
bind:this={edits[4].el}
class="digit"
tabindex="0"
on:keydown={(ev) => {
keyDown(ev, edits[4].id)
}}
on:focus={() => {
focused(edits[4].id)
}}
on:blur={(ev) => {
unfocus(ev, edits[4].id)
}}
>
{#if edits[4].value > -1}
{edits[4].value.toString().padStart(2, '0')}
{:else}<Label label={ui.string.MM} />{/if}
</span>
{/if}
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
bind:this={closeBtn}
class="close-btn"
tabindex="0"
on:click={() => {
selected = defaultSelected
startTyping = true
value = null
setEmptyEdits()
const newFocusElement = edits[mode === DateRangeMode.TIME ? 2 : 0].el
if (newFocusElement) newFocusElement.focus()
}}
on:blur={(ev) => {
unfocus(ev, closeBtn)
}}
>
<Icon icon={IconClose} size={'x-small'} />
{#if inline}
<span class:fs-bold={accent}>
{#if shouldShowAvatar}
<div class="btn-icon {iconModifier}">
<Icon icon={iconModifier === 'overdue' && !shouldIgnoreOverdue ? DPCalendarOver : DPCalendar} size={'full'} />
</div>
{/if}
{:else}
<div class="btn-icon {iconModifier}">
<Icon icon={iconModifier === 'overdue' && !shouldIgnoreOverdue ? DPCalendarOver : DPCalendar} size={'full'} />
</div>
{#if value !== undefined && value !== null && value.toString() !== ''}
{#if withDate}
{new Date(value).getDate()}
@@ -467,8 +330,181 @@
<Label label={labelNull} />
</div>
{/if}
{/if}
</button>
</span>
{:else}
<button
bind:this={datePresenter}
class="datetime-button {kind} {size}"
class:notSelected={!value}
class:editable
class:edit
on:click={() => {
if (editable && !opened && withDate) openPopup()
else if (editable && !opened && mode === DateRangeMode.TIME) edit = true
}}
>
{#if edit}
{#if withDate}
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
bind:this={edits[0].el}
class="digit"
tabindex="0"
on:keydown={(ev) => {
keyDown(ev, edits[0].id)
}}
on:focus={() => {
focused(edits[0].id)
}}
on:blur={(ev) => {
unfocus(ev, edits[0].id)
}}
>
{#if edits[0].value > -1}
{edits[0].value.toString().padStart(2, '0')}
{:else}<Label label={ui.string.DD} />{/if}
</span>
<span class="separator">.</span>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
bind:this={edits[1].el}
class="digit"
tabindex="0"
on:keydown={(ev) => {
keyDown(ev, edits[1].id)
}}
on:focus={() => {
focused(edits[1].id)
}}
on:blur={(ev) => {
unfocus(ev, edits[1].id)
}}
>
{#if edits[1].value > -1}
{edits[1].value.toString().padStart(2, '0')}
{:else}<Label label={ui.string.MM} />{/if}
</span>
<span class="separator">.</span>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<span
bind:this={edits[2].el}
class="digit"
tabindex="0"
on:keydown={(ev) => {
keyDown(ev, edits[2].id)
}}
on:focus={() => {
focused(edits[2].id)
}}
on:blur={(ev) => {
unfocus(ev, edits[2].id)
}}
>
{#if edits[2].value > -1}
{edits[2].value.toString().padStart(4, '0')}
{:else}<Label label={ui.string.YYYY} />{/if}
</span>
{/if}
{#if withTime}
{#if mode === DateRangeMode.DATETIME}
<div class="time-divider" />
{/if}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<span
bind:this={edits[3].el}
class="digit"
tabindex="0"
on:keydown={(ev) => {
keyDown(ev, edits[3].id)
}}
on:focus={() => {
focused(edits[3].id)
}}
on:blur={(ev) => {
unfocus(ev, edits[3].id)
}}
>
{#if edits[3].value > -1}
{edits[3].value.toString().padStart(2, '0')}
{:else}<Label label={ui.string.HH} />{/if}
</span>
<span class="separator">:</span>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
bind:this={edits[4].el}
class="digit"
tabindex="0"
on:keydown={(ev) => {
keyDown(ev, edits[4].id)
}}
on:focus={() => {
focused(edits[4].id)
}}
on:blur={(ev) => {
unfocus(ev, edits[4].id)
}}
>
{#if edits[4].value > -1}
{edits[4].value.toString().padStart(2, '0')}
{:else}<Label label={ui.string.MM} />{/if}
</span>
{/if}
{#if value}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
bind:this={closeBtn}
class="close-btn"
tabindex="0"
on:click={() => {
selected = defaultSelected
startTyping = true
value = null
setEmptyEdits()
const newFocusElement = edits[mode === DateRangeMode.TIME ? 2 : 0].el
if (newFocusElement) newFocusElement.focus()
}}
on:blur={(ev) => {
unfocus(ev, closeBtn)
}}
>
<Icon icon={IconClose} size={'x-small'} />
</div>
{/if}
{:else}
<div class="btn-icon {iconModifier}">
<Icon icon={iconModifier === 'overdue' && !shouldIgnoreOverdue ? DPCalendarOver : DPCalendar} size={'full'} />
</div>
{#if value !== undefined && value !== null && value.toString() !== ''}
{#if withDate}
{new Date(value).getDate()}
{getMonthName(new Date(value), 'short')}
{#if new Date(value).getFullYear() !== today.getFullYear()}
{new Date(value).getFullYear()}
{/if}
{/if}
{#if withTime}
{#if withDate}
<div class="time-divider" />
{/if}
{new Date(value).getHours().toString().padStart(2, '0')}
<span class="separator">:</span>
{new Date(value).getMinutes().toString().padStart(2, '0')}
{/if}
{:else}
<div class="overflow-label">
<Label label={labelNull} />
</div>
{/if}
{/if}
</button>
{/if}
<style lang="scss">
.datetime-button {
@@ -490,18 +526,23 @@
transition-duration: 0.15s;
cursor: pointer;
&.noPadding {
padding: 0;
}
&.small {
height: 1.5rem;
}
&.medium {
height: 2rem;
}
&.large {
height: 2.25rem;
}
.btn-icon {
margin-right: 0.375rem;
width: 0.875rem;
height: 0.875rem;
transition: color 0.15s;
@@ -510,12 +551,15 @@
&.normal {
color: var(--theme-content-color);
}
&.warning {
color: var(--theme-warning-color);
}
&.overdue {
color: var(--theme-error-color);
}
&.critical {
color: var(--theme-error-color);
}
@@ -529,6 +573,7 @@
color: var(--theme-caption-color);
}
}
&.no-border {
font-weight: 400;
color: var(--theme-content-color);
@@ -544,6 +589,7 @@
color: var(--theme-caption-color);
}
}
&:disabled {
color: var(--theme-trans-color);
background-color: var(--theme-button-disabled);
@@ -552,8 +598,10 @@
.btn-icon {
color: var(--theme-trans-color);
}
&:hover {
color: var(--theme-trans-color);
.btn-icon {
color: var(--theme-trans-color);
}
@@ -565,27 +613,34 @@
&:hover {
background-color: var(--theme-button-hovered);
.btn-icon {
&.normal {
color: var(--caption-color);
}
&.warning {
color: var(--theme-warning-color);
}
&.overdue {
color: var(--theme-error-color);
}
&.critical {
color: var(--theme-error-color);
}
}
.time-divider {
background-color: var(--theme-divider-color);
}
}
&:focus-within {
background-color: var(--theme-button-focused);
border-color: var(--primary-edit-border-color);
&:hover {
background-color: var(--theme-button-hovered);
}
@@ -596,6 +651,7 @@
padding: 0 0.125rem;
background-color: transparent;
border-color: var(--primary-edit-border-color);
&:hover {
background-color: transparent;
}
@@ -606,13 +662,16 @@
padding: 0 0.875rem;
width: 100%;
color: var(--theme-caption-color);
&:hover {
background-color: var(--theme-bg-color);
border-color: var(--theme-divider-color);
.btn-icon {
color: var(--theme-content-color);
}
}
&.edit {
padding: 0 0.5rem;
}
@@ -627,13 +686,16 @@
.btn-icon {
color: var(--theme-content-color);
}
&:hover {
background-color: var(--theme-button-hovered);
border-color: var(--theme-divider-color);
.btn-icon {
color: var(--theme-content-color);
}
}
// &.edit {
// padding: 0 0.5rem;
// }
@@ -670,6 +732,7 @@
&:focus {
background-color: var(--primary-bg-color);
}
&::after {
position: absolute;
top: 0;
@@ -680,6 +743,7 @@
cursor: pointer;
}
}
.time-divider {
flex-shrink: 0;
margin: 0 0.25rem;
@@ -688,15 +752,18 @@
height: 0.75rem;
background-color: var(--theme-divider-color);
}
.separator {
margin: 0 0.1rem;
}
&.notSelected {
color: var(--theme-dark-color);
.btn-icon {
color: var(--theme-darker-color);
}
&:hover,
&:hover .btn-icon {
color: var(--theme-content-color);
+10 -14
View File
@@ -1,21 +1,17 @@
{
"string": {
"Delete": "Delete",
"Edit": "Edit",
"Edited": "edited",
"DocCreated": "created {_class}",
"DocAdded": "added {_class}",
"DocDeleted": "deleted {_class}",
"Activity": "Activity",
"Changed": "changed",
"To": "to",
"Unset": "unset",
"CollectionUpdated": "Update {collection}",
"Added": "added",
"Removed": "removed",
"From": "from",
"All": "All",
"Attributes": "Attributes",
"NewestFirst": "Newest first"
"Changed": "changed",
"CollectionUpdated": "Update {collection}",
"DocAdded": "added {_class}",
"DocCreated": "created {_class}",
"DocDeleted": "deleted {_class}",
"Edited": "edited",
"From": "from",
"Removed": "removed",
"To": "to",
"Unset": "unset"
}
}
+10 -14
View File
@@ -1,21 +1,17 @@
{
"string": {
"Delete": "Удалить",
"Edit": "Редактировать",
"Edited": "отредактировал(а)",
"DocCreated": "создал(а) {_class}",
"DocAdded": "добавил(а) {_class}",
"DocDeleted": "удалил(а) {_class}",
"Activity": "Активность",
"Changed": "изменил(а)",
"To": "на",
"Unset": "сбросил",
"CollectionUpdated": "Обновлена {collection}",
"Added": "добавила(а)",
"Removed": "удалил(а)",
"From": "из",
"All": "Все",
"Attributes": "Атрибуты",
"NewestFirst": "Сначала новые"
"Changed": "изменил(а)",
"CollectionUpdated": "Обновлена {collection}",
"DocAdded": "добавил(а) {_class}",
"DocCreated": "создал(а) {_class}",
"DocDeleted": "удалил(а) {_class}",
"Edited": "отредактировал(а)",
"From": "из",
"Removed": "удалил(а)",
"To": "на",
"Unset": "сбросил"
}
}
+15 -18
View File
@@ -13,39 +13,36 @@
"build:watch": "tsc --noEmit --outDir ./dist_cache"
},
"devDependencies": {
"svelte-loader": "^3.1.9",
"sass": "^1.53.0",
"svelte-preprocess": "^5.1.0",
"@hcengineering/platform-rig": "^0.6.0",
"@types/jest": "^29.5.5",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-svelte": "^2.34.0",
"prettier-plugin-svelte": "^3.1.0",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"svelte-check": "^3.6.0",
"typescript": "^5.2.2",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0",
"sass": "^1.53.0",
"svelte-check": "^3.6.0",
"svelte-eslint-parser": "^0.33.1",
"svelte-loader": "^3.1.9",
"svelte-preprocess": "^5.1.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"svelte-eslint-parser": "^0.33.1"
"typescript": "^5.2.2"
},
"dependencies": {
"@hcengineering/core": "^0.6.28",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/activity": "^0.6.0",
"svelte": "^4.2.5",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/attachment": "^0.6.9"
"svelte": "^4.2.5"
}
}
-425
View File
@@ -1,24 +1,16 @@
import { type DisplayTx } from '@hcengineering/activity'
import core, {
type AnyAttribute,
type AttachedDoc,
type Attribute,
type Class,
type Client,
type Collection,
type Doc,
type Hierarchy,
type Ref,
SortingOrder,
type Tx,
type TxCollectionCUD,
type TxCreateDoc,
type TxCUD,
type TxMixin,
TxProcessor,
type TxUpdateDoc
} from '@hcengineering/core'
import { createQuery, type LiveQuery } from '@hcengineering/presentation'
/**
* @public
@@ -32,415 +24,6 @@ export function activityKey (objectClass: Ref<Class<Doc>>, txClass: Ref<Class<Tx
return objectClass + ':' + txClass
}
function isEqualOps (op1: any, op2: any): boolean {
if (typeof op1 === 'string' && typeof op2 === 'string') {
return op1 === op2
}
if (typeof op1 !== typeof op2) {
return false
}
const o1 = Object.keys(op1).sort().join('-')
const o2 = Object.keys(op2).sort().join('-')
return o1 === o2
}
/**
* @public
*/
export type DisplayTxListener = (objectId: Ref<Doc>, txes: DisplayTx[]) => void
// Use 5 minutes to combine similar transactions.
const combineThreshold = 5 * 60 * 1000
const createCombineThreshold = 10 * 1000
/**
* Define activity.
*
* Allow to recieve a list of transactions and notify client about it.
*/
export interface Activity {
update: (
objectId: Ref<Doc>,
objectClass: Ref<Class<Doc>>,
listener: DisplayTxListener,
sort: SortingOrder,
editable: Map<Ref<Class<Doc>>, boolean>
) => boolean
}
class ActivityImpl implements Activity {
private readonly ownTxQuery: LiveQuery
private readonly attachedTxQuery: LiveQuery
private readonly attachedChangeTxQuery: LiveQuery
private readonly hiddenAttributes: Set<string>
private prevObjectId: Ref<Doc> | undefined
private prevObjectClass: Ref<Class<Doc>> | undefined
private prevSort: SortingOrder | undefined
private editable: Map<Ref<Class<Doc>>, boolean> | undefined
private ownTxes: Array<TxCUD<Doc>> = []
private attachedTxes: Array<TxCollectionCUD<Doc, AttachedDoc>> = []
private attacheChangedTxes: Array<TxCollectionCUD<Doc, AttachedDoc>> = []
private readonly hierarchy: Hierarchy
constructor (
readonly client: Client,
attributes: Map<string, AnyAttribute>
) {
this.hierarchy = client.getHierarchy()
this.hiddenAttributes = new Set(
[...attributes.entries()].filter(([, value]) => value.hidden === true).map(([key]) => key)
)
this.ownTxQuery = createQuery()
this.attachedTxQuery = createQuery()
this.attachedChangeTxQuery = createQuery()
}
private notify (objectId: Ref<Doc>, listener: DisplayTxListener, sort: SortingOrder): void {
if (this.editable != null) {
this.combineTransactions(objectId, this.ownTxes, this.attachedTxes, this.attacheChangedTxes, this.editable).then(
(result) => {
const sorted = result.sort((a, b) => (a.tx.modifiedOn - b.tx.modifiedOn) * sort)
listener(objectId, sorted)
},
(err) => {
console.error(err)
}
)
}
}
update (
objectId: Ref<Doc>,
objectClass: Ref<Class<Doc>>,
listener: DisplayTxListener,
sort: SortingOrder,
editable: Map<Ref<Class<Doc>>, boolean>
): boolean {
this.editable = editable
if (objectId === this.prevObjectId && objectClass === this.prevObjectClass && sort === this.prevSort) {
return false
}
this.prevObjectClass = objectClass
this.prevObjectId = objectId
this.prevSort = sort
let isAttached = false
isAttached = this.hierarchy.isDerived(objectClass, core.class.AttachedDoc)
this.ownTxQuery.query<TxCUD<Doc>>(
isAttached ? core.class.TxCollectionCUD : core.class.TxCUD,
isAttached
? { 'tx.objectId': objectId as Ref<AttachedDoc> }
: {
objectId,
_class: {
$in: [core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc, core.class.TxMixin]
}
},
(result) => {
this.ownTxes = result
this.notify(objectId, listener, sort)
},
{ sort: { modifiedOn: SortingOrder.Ascending } }
)
this.attachedTxQuery.query<TxCollectionCUD<Doc, AttachedDoc>>(
core.class.TxCollectionCUD,
{
objectId,
'tx._class': { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc] }
},
(result) => {
this.attachedTxes = result
this.notify(objectId, listener, sort)
},
{ sort: { modifiedOn: SortingOrder.Ascending } }
)
this.attachedChangeTxQuery.query<TxCollectionCUD<Doc, AttachedDoc>>(
core.class.TxCollectionCUD,
{
'tx.operations.attachedTo': objectId,
'tx._class': core.class.TxUpdateDoc
},
(result) => {
this.attacheChangedTxes = result
this.notify(objectId, listener, sort)
},
{ sort: { modifiedOn: SortingOrder.Ascending } }
)
// In case editable is changed
this.notify(objectId, listener, sort)
return true
}
async combineTransactions (
_id: Ref<Doc>,
ownTxes: Array<TxCUD<Doc>>,
attachedTxes: Array<TxCollectionCUD<Doc, AttachedDoc>>,
attachedChangeTxes: Array<TxCollectionCUD<Doc, AttachedDoc>>,
editable: Map<Ref<Class<Doc>>, boolean>
): Promise<DisplayTx[]> {
const parents = new Map<Ref<Doc>, DisplayTx>()
let results: DisplayTx[] = []
for (const tx of ownTxes) {
if (!this.filterUpdateTx(tx)) continue
const [result] = this.createDisplayTx(tx, parents, true)
// Combine previous update transaction for same field and if same operation and time treshold is ok
results = this.integrateTxWithResults(results, result, editable)
this.updateRemovedState(result, results)
}
for (let tx of Array.from(attachedTxes)
.concat(attachedChangeTxes)
.sort((a, b) => a.modifiedOn - b.modifiedOn)) {
if (!this.filterUpdateTx(tx)) continue
const changeAttached = this.isChangeAttachedTx(tx)
if (changeAttached || this.isDisplayTxRequired(tx)) {
if (changeAttached) {
tx = await this.createFakeTx(_id, tx)
}
const [result, isUpdated, isMixin] = this.createDisplayTx(tx, parents, false)
if (!(isUpdated || isMixin)) {
// Combine previous update transaction for same field and if same operation and time treshold is ok
results = this.integrateTxWithResults(results, result, editable)
this.updateRemovedState(result, results)
}
}
}
return results
}
private async createFakeTx (
_id: Ref<Doc>,
cltx: TxCollectionCUD<Doc, AttachedDoc>
): Promise<TxCollectionCUD<Doc, AttachedDoc>> {
if (_id === cltx.objectId) {
cltx.tx._class = core.class.TxRemoveDoc
} else {
const createTx = await this.client.findOne(core.class.TxCollectionCUD, {
'tx.objectId': cltx.tx.objectId,
'tx._class': core.class.TxCreateDoc
})
if (createTx !== undefined) {
cltx.tx = createTx.tx
cltx.tx.modifiedBy = cltx.modifiedBy
cltx.tx.modifiedOn = cltx.modifiedOn
}
}
return cltx
}
private isChangeAttachedTx (cltx: TxCollectionCUD<Doc, AttachedDoc>): boolean {
const tx = TxProcessor.extractTx(cltx)
if (this.hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) {
const utx = tx as TxUpdateDoc<AttachedDoc>
return utx.operations.attachedTo !== undefined
}
return false
}
private updateRemovedState (result: DisplayTx, results: DisplayTx[]): void {
if (result.removed) {
// We need to mark all transactions for same object as removed as well.
for (const t of results) {
if (t.tx.objectId === result.tx.objectId) {
t.removed = true
}
}
}
}
private isDisplayTxRequired (cltx: TxCollectionCUD<Doc, AttachedDoc>): boolean {
// Check if collection attribute is hidden
if (this.hiddenAttributes.has(cltx.collection)) {
return false
}
const tx = TxProcessor.extractTx(cltx)
if (
[core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc, core.class.TxMixin].includes(tx._class)
) {
return true
}
return false
}
private readonly getUpdateTx = (tx: TxCUD<Doc>): TxUpdateDoc<Doc> | undefined => {
if (this.hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
const cltx = tx as TxCollectionCUD<Doc, AttachedDoc>
tx = TxProcessor.extractTx(cltx) as TxCUD<Doc>
}
if (tx._class !== core.class.TxUpdateDoc) {
return undefined
}
return tx as TxUpdateDoc<Doc>
}
filterUpdateTx (tx: TxCUD<Doc>): boolean {
const utx = this.getUpdateTx(tx)
if (utx === undefined) {
return true
}
const ops = Object.keys(utx.operations)
if (ops.length > 1) {
return true
}
return !this.hiddenAttributes.has(ops[0])
}
createDisplayTx (tx: TxCUD<Doc>, parents: Map<Ref<Doc>, DisplayTx>, isOwnTx: boolean): [DisplayTx, boolean, boolean] {
let collectionAttribute: Attribute<Collection<AttachedDoc>> | undefined
const originTx = tx
if (this.hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
const cltx = tx as TxCollectionCUD<Doc, AttachedDoc>
tx = TxProcessor.extractTx(cltx) as TxCUD<Doc>
// Check mixin classes for desired attribute
for (const cl of this.hierarchy.getDescendants(cltx.objectClass)) {
try {
collectionAttribute = this.hierarchy.findAttribute(cl, cltx.collection) as Attribute<Collection<AttachedDoc>>
if (collectionAttribute !== undefined) {
break
}
} catch (err: any) {
// Ignore
}
}
}
let firstTx = parents.get(tx.objectId)
const result: DisplayTx = newDisplayTx(tx, this.hierarchy, isOwnTx, originTx)
result.collectionAttribute = collectionAttribute
result.doc = firstTx?.doc ?? result.doc
result.prevDoc = this.hierarchy.clone(result.doc)
firstTx = firstTx ?? result
parents.set(tx.objectId, firstTx)
// If we have updates also apply them all.
const isUpdated = this.checkUpdateState(result, firstTx)
const isMixin = this.checkMixinState(result, firstTx)
this.checkRemoveState(tx, firstTx, result)
return [result, isUpdated, isMixin]
}
private checkRemoveState (tx: TxCUD<Doc>, firstTx: DisplayTx, result: DisplayTx): void {
if (this.hierarchy.isDerived(tx._class, core.class.TxRemoveDoc)) {
firstTx.removed = true
result.removed = true
}
}
checkUpdateState (result: DisplayTx, firstTx: DisplayTx): boolean {
if (this.hierarchy.isDerived(result.tx._class, core.class.TxUpdateDoc) && result.doc !== undefined) {
firstTx.doc = TxProcessor.updateDoc2Doc(result.doc, result.tx as TxUpdateDoc<Doc>)
firstTx.updated = true
result.updated = true
return true
}
return false
}
checkMixinState (result: DisplayTx, firstTx: DisplayTx): boolean {
if (this.hierarchy.isDerived(result.tx._class, core.class.TxMixin) && result.doc !== undefined) {
const mix = result.tx as TxMixin<Doc, Doc>
firstTx.doc = TxProcessor.updateMixin4Doc(result.doc, mix)
firstTx.mixin = true
result.mixin = true
return true
}
return false
}
integrateTxWithResults (
results: DisplayTx[],
result: DisplayTx,
editable: Map<Ref<Class<Doc>>, boolean>
): DisplayTx[] {
const curUpdate: any = getCombineOpFromTx(result)
if (curUpdate === undefined || (result.doc !== undefined && editable.get(result.doc._class) === true)) {
results.push(result)
return results
}
const newResults = results.filter((prevTx) => {
const prevUpdate: any = getCombineOpFromTx(prevTx)
if (this.isInitTx(prevTx, result)) {
result = prevTx
return false
}
// If same tx or same collection
if (this.isSameKindTx(prevTx, result) || prevUpdate === curUpdate) {
if (result.tx.modifiedOn - prevTx.tx.modifiedOn < combineThreshold && isEqualOps(prevUpdate, curUpdate)) {
// we have same keys,
// Remember previous transactions
if (result.txDocIds === undefined) {
result.txDocIds = new Set(prevTx.txDocIds)
}
if (prevTx.doc?._id !== undefined) {
result.txDocIds?.add(prevTx.doc._id)
}
if (result.doc?._id !== undefined) {
result.txDocIds?.add(result.doc._id)
}
result.txes.push(...prevTx.txes, prevTx)
return false
}
}
return true
})
newResults.push(result)
return newResults
}
isInitTx (prevTx: DisplayTx, result: DisplayTx): boolean {
if (prevTx.createTx !== undefined) {
if (
prevTx.tx.modifiedBy === result.tx.modifiedBy &&
(result.tx.objectId === prevTx.createTx.objectId ||
(result.doc as AttachedDoc)?.attachedTo === prevTx.createTx.objectId)
) {
return result.tx.modifiedOn - prevTx.createTx.modifiedOn < createCombineThreshold
}
}
return false
}
isSameKindTx (prevTx: DisplayTx, result: DisplayTx): boolean {
return (
prevTx.tx.objectId === result.tx.objectId && // Same document id
prevTx.tx._class === result.tx._class && // Same transaction class
prevTx.tx.modifiedBy === result.tx.modifiedBy // Same user
)
}
}
function getCombineOpFromTx (result: DisplayTx): any {
let curUpdate: any
if (result.tx._class === core.class.TxUpdateDoc) {
curUpdate = (result.tx as unknown as TxUpdateDoc<Doc>).operations
}
if (result.tx._class === core.class.TxMixin) {
curUpdate = (result.tx as unknown as TxMixin<Doc, Doc>).attributes
}
if (curUpdate === undefined && result.collectionAttribute !== undefined) {
curUpdate = result.collectionAttribute.attributeOf + '.' + result.collectionAttribute.name
}
return curUpdate
}
export function newDisplayTx (
tx: TxCUD<Doc>,
hierarchy: Hierarchy,
@@ -462,11 +45,3 @@ export function newDisplayTx (
originTx
}
}
/**
* Construct an new activity, to listend for displayed transactions in UI.
* @param client
*/
export function newActivity (client: Client, attributes: Map<string, AnyAttribute>): Activity {
return new ActivityImpl(client, attributes)
}
@@ -13,161 +13,83 @@
// limitations under the License.
-->
<script lang="ts">
import activity, { ActivityExtension, DisplayTx, TxViewlet } from '@hcengineering/activity'
import core, { Class, Doc, Ref, SortingOrder } from '@hcengineering/core'
import notification, { DocUpdates, DocUpdateTx, Writable } from '@hcengineering/notification'
import activity from '@hcengineering/activity'
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
import notification, { DisplayActivityMessage, ActivityMessage } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Grid, Label, Lazy, Spinner } from '@hcengineering/ui'
import { ActivityKey, activityKey, newActivity } from '../activity'
import { filterCollectionTxes, getExtensions } from '../utils'
import { createQuery } from '@hcengineering/presentation'
import { Component, Grid, Label, Lazy, Spinner } from '@hcengineering/ui'
import ActivityFilter from './ActivityFilter.svelte'
import TxView from './TxView.svelte'
import ActivityExtensionComponent from './ActivityExtensionComponent.svelte'
export let object: Doc
export let showCommenInput: boolean = true
export let transparent: boolean = false
export let shouldScroll: boolean = false
export let focusIndex: number = -1
export let boundary: HTMLElement | undefined = undefined
let extensions: ActivityExtension[] = []
const activityMessagesQuery = createQuery()
$: if (object) {
getExtensions(client, object._class).then((res?: ActivityExtension[]) => {
extensions = res || []
})
}
let filteredMessages: DisplayActivityMessage[] = []
let activityMessages: ActivityMessage[] = []
let isLoading = false
getResource(notification.function.GetNotificationClient).then((res) => {
updatesStore = res().docUpdatesStore
})
let updatesStore: Writable<Map<Ref<Doc>, DocUpdates>> | undefined
let isNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
$: updates = $updatesStore?.get(object._id)
$: newTxes = updates?.txes ?? []
async function updateActivityMessages (objectId: Ref<Doc>, order: SortingOrder): Promise<void> {
isLoading = true
const combineMessagesFn = await getResource(notification.function.CombineActivityMessages)
let txes: DisplayTx[] = []
const client = getClient()
const attrs = client.getHierarchy().getAllAttributes(object._class)
const activityQuery = newActivity(client, attrs)
let viewlets = new Map<ActivityKey, TxViewlet[]>()
let allViewlets: TxViewlet[] = []
let editableMap: Map<Ref<Class<Doc>>, boolean> | undefined = undefined
const descriptors = createQuery()
$: descriptors.query(activity.class.TxViewlet, {}, (result) => {
allViewlets = result
editableMap = new Map(
allViewlets
.filter((tx) => tx.txClass === core.class.TxCreateDoc)
.map((it) => [it.objectClass, it.editable ?? false])
)
})
$: viewlets = buildViewletsMap(allViewlets)
function buildViewletsMap (allViewlets: TxViewlet[]): Map<ActivityKey, TxViewlet[]> {
const viewlets = new Map()
for (const res of allViewlets) {
const key = activityKey(res.objectClass, res.txClass)
const arr = viewlets.get(key) ?? []
arr.push(res)
viewlets.set(key, arr)
}
return viewlets
}
let loading = false
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
function updateTxes (
objectId: Ref<Doc>,
objectClass: Ref<Class<Doc>>,
editableMap: Map<Ref<Class<Doc>>, boolean> | undefined,
activityOrder: boolean
): void {
loading = true
const res = activityQuery.update(
objectId,
objectClass,
(_id, result) => {
if (_id === objectId) {
txes = filterCollectionTxes(result)
if (txes.length > 0) {
loading = false
}
}
const res = activityMessagesQuery.query(
notification.class.ActivityMessage,
{ attachedTo: objectId },
(result: ActivityMessage[]) => {
activityMessages = combineMessagesFn(result, order)
isLoading = false
},
activityOrder ? SortingOrder.Descending : SortingOrder.Ascending,
editableMap ?? new Map()
{
sort: {
createdOn: SortingOrder.Ascending
}
}
)
if (!res) {
loading = false
isLoading = false
}
}
$: updateTxes(object._id, object._class, editableMap, activityOrderNewestFirst)
let filtered: DisplayTx[] = []
let newTxIndexes: number[] = []
$: newTxIndexes = getNewTxes(filtered, newTxes)
function getNewTxes (filtered: DisplayTx[], newTxes: DocUpdateTx[]): number[] {
const res: number[] = []
for (let i = 0; i < filtered.length; i++) {
if (isNew(filtered[i], newTxes)) {
res.push(i)
}
}
return res
}
function isNew (tx: DisplayTx | undefined, newTxes: DocUpdateTx[]): boolean {
if (tx === undefined) return false
const index = newTxes.findIndex((p) => p._id === tx.originTx._id && p.isNew)
return index !== -1
}
$: scrollIndex = shouldScroll ? newTxIndexes[0] ?? filtered.length - 1 : -1
$: updateActivityMessages(object._id, isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending)
</script>
<div class="antiSection-header high mt-9" class:invisible={transparent}>
<span class="antiSection-header__title flex-row-center">
<Label label={activity.string.Activity} />
{#if loading}
{#if isLoading}
<div class="ml-1">
<Spinner size={'small'} />
<Spinner size="small" />
</div>
{/if}
</span>
<ActivityFilter
{txes}
messages={activityMessages}
{object}
on:update={(e) => {
filtered = e.detail
filteredMessages = e.detail
}}
bind:activityOrderNewestFirst
bind:isNewestFirst
/>
</div>
<div class="p-activity select-text" id={activity.string.Activity}>
{#if filtered}
<Grid column={1} rowGap={0.75}>
{#each filtered as tx, i}
{#if filteredMessages.length}
<Grid column={1} rowGap={0}>
{#each filteredMessages as message}
<Lazy>
<TxView
{tx}
{viewlets}
isNew={newTxIndexes.includes(i)}
isNextNew={newTxIndexes.includes(i + 1)}
shouldScroll={i === scrollIndex}
{boundary}
<Component
is={notification.component.ActivityMessagePresenter}
props={{
value: message,
boundary
}}
/>
</Lazy>
{/each}
@@ -176,7 +98,7 @@
</div>
{#if showCommenInput}
<div class="ref-input">
<ActivityExtensionComponent {extensions} kind="input" props={{ object, focusIndex, boundary }} />
<Component is={notification.component.ChatMessageInput} props={{ object, boundary, focusIndex }} />
</div>
{/if}
@@ -186,14 +108,18 @@
margin-top: 1.75rem;
padding-bottom: 2.5rem;
}
.p-activity {
margin-top: 1.75rem;
}
.invisible {
display: none;
}
:global(.grid .msgactivity-container.showIcon:last-child::after) {
content: none;
} // Remove the line in the last Activity message
}
// Remove the line in the last Activity message
</style>
@@ -13,32 +13,36 @@
// limitations under the License.
-->
<script lang="ts">
import activity, { ActivityFilter, DisplayTx } from '@hcengineering/activity'
import { Class, Doc, Ref } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getClient, hasResource } from '@hcengineering/presentation'
import { ActionIcon, AnyComponent, Icon, Label, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import activityPlg from '../plugin'
import { Doc, Ref, SortingOrder } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { ActionIcon, eventToHTMLElement, Icon, Label, showPopup } from '@hcengineering/ui'
import notification, { ActivityMessage, ActivityMessagesFilter } from '@hcengineering/notification'
import activity from '../plugin'
import FilterPopup from './FilterPopup.svelte'
import IconClose from './icons/Close.svelte'
import IconFilter from './icons/Filter.svelte'
export let txes: DisplayTx[]
export let messages: ActivityMessage[]
export let object: Doc
export let isNewestFirst = false
let filtered: DisplayTx[]
let filtered: ActivityMessage[]
const client = getClient()
let filters: ActivityFilter[] = []
let filters: ActivityMessagesFilter[] = []
const saved = localStorage.getItem('activity-filter')
export let activityOrderNewestFirst = false
let selectedFiltersRefs: Ref<Doc>[] | 'All' =
saved !== null && saved !== undefined ? (JSON.parse(saved) as Ref<Doc>[] | 'All') : 'All'
let selectedFilters: ActivityFilter[] = []
let selectedFilters: ActivityMessagesFilter[] = []
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs))
$: localStorage.setItem('activity-newest-first', JSON.stringify(activityOrderNewestFirst))
client.findAll(activity.class.ActivityFilter, {}).then((res) => {
$: localStorage.setItem('activity-newest-first', JSON.stringify(isNewestFirst))
client.findAll(notification.class.ActivityMessagesFilter, {}).then((res) => {
filters = res
if (saved !== null && saved !== undefined) {
const temp: Ref<Doc>[] | 'All' = JSON.parse(saved)
if (temp !== 'All' && Array.isArray(temp)) {
@@ -52,20 +56,7 @@
}
})
function getAdditionalComponent (_class: Ref<Class<Doc>>): AnyComponent | undefined {
const hierarchy = client.getHierarchy()
const mixin = hierarchy.classHierarchyMixin(_class, activity.mixin.ExtraActivityComponent, (m) =>
hasResource(m.component)
)
if (mixin !== undefined) {
return mixin.component
}
}
let extraComponent = getAdditionalComponent(object._class)
$: extraComponent = getAdditionalComponent(object._class)
const handleOptions = (ev: MouseEvent) => {
const handleOptions = (ev: MouseEvent): void => {
showPopup(
FilterPopup,
{ selectedFiltersRefs, filters },
@@ -74,7 +65,7 @@
(res) => {
if (res === undefined) return
if (res.action === 'toggle') {
activityOrderNewestFirst = res.value
isNewestFirst = res.value
return
}
const selected = res.value as Ref<Doc>[]
@@ -87,33 +78,45 @@
const dispatch = createEventDispatcher()
async function updateFilterActions (
txes: DisplayTx[],
filters: ActivityFilter[],
selected: Ref<Doc>[] | 'All'
messages: ActivityMessage[],
filters: ActivityMessagesFilter[],
selected: Ref<Doc>[] | 'All',
sortOrder: SortingOrder
): Promise<void> {
const txesSorted = txes.sort((tx) => ((tx.doc as any)?.pinned ? -1 : 1))
const sortMessagesFn = await getResource(notification.function.SortActivityMessages)
const sortedMessages = sortMessagesFn(messages, sortOrder).sort(({ isPinned }) =>
isPinned && sortOrder === SortingOrder.Ascending ? -1 : 1
)
if (selected === 'All') {
filtered = txesSorted
filtered = sortedMessages
dispatch('update', filtered)
} else {
selectedFilters = filters.filter((filter) => selected.includes(filter._id))
const filterActions: ((tx: DisplayTx, _class?: Ref<Doc>) => boolean)[] = []
const filterActions: ((message: ActivityMessage, _class?: Ref<Doc>) => boolean)[] = []
for (const filter of selectedFilters) {
const fltr = await getResource(filter.filter)
filterActions.push(fltr)
}
filtered = txesSorted.filter((it) => filterActions.some((f) => f(it, object._class)))
filtered = messages.filter((message) => filterActions.some((f) => f(message, object._class)))
dispatch('update', filtered)
}
}
$: updateFilterActions(txes, filters, selectedFiltersRefs)
$: updateFilterActions(
messages,
filters,
selectedFiltersRefs,
isNewestFirst ? SortingOrder.Descending : SortingOrder.Ascending
)
</script>
<div class="w-4 min-w-4 max-w-4" />
{#if selectedFiltersRefs === 'All'}
<div class="antiSection-header__tag highlight">
<Label label={activityPlg.string.All} />
<Label label={activity.string.All} />
</div>
{:else}
{#each selectedFilters as filter}
@@ -137,10 +140,3 @@
{/if}
<div class="w-4 min-w-4 max-w-4" />
<ActionIcon icon={IconFilter} size={'medium'} action={handleOptions} />
{#if extraComponent}
{#await getResource(extraComponent) then comp}
{#if comp}
<svelte:component this={comp} {filtered} {object} on:update />
{/if}
{/await}
{/if}
@@ -17,14 +17,17 @@
import { IntlString } from '@hcengineering/platform'
import { Label, resizeObserver, CheckBox, MiniToggle } from '@hcengineering/ui'
import { Doc, Ref } from '@hcengineering/core'
import { ActivityFilter } from '@hcengineering/activity'
import notification, { ActivityMessagesFilter } from '@hcengineering/notification'
import activity from '../plugin'
export let selectedFiltersRefs: Ref<Doc>[] | 'All' = 'All'
export let filters: ActivityFilter[] = []
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
export let filters: ActivityMessagesFilter[] = []
const dispatch = createEventDispatcher()
let activityOrderNewestFirst = JSON.parse(localStorage.getItem('activity-newest-first') ?? 'false')
interface ActionMenu {
label: IntlString
checked: boolean
@@ -129,7 +132,7 @@
<div class="ml-3 mt-2 mb-2 mr-3">
<MiniToggle
bind:on={activityOrderNewestFirst}
label={activity.string.NewestFirst}
label={notification.string.NewestFirst}
on:change={() => {
dispatch('update', { action: 'toggle', value: activityOrderNewestFirst })
}}
@@ -1,550 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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 { tick } from 'svelte'
import type { ActivityExtension, DisplayTx, TxViewlet } from '@hcengineering/activity'
import attachment from '@hcengineering/attachment'
import contact, { Person, PersonAccount, getName } from '@hcengineering/contact'
import core, { AnyAttribute, Doc, Ref, TxCUD, getCurrentAccount } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import ui, {
ActionIcon,
AnyComponent,
Component,
Icon,
IconActivityEdit,
IconEdit,
IconMoreH,
Label,
ShowMore,
TimeSince,
showPopup
} from '@hcengineering/ui'
import type { AttributeModel } from '@hcengineering/view'
import { Menu, ObjectPresenter } from '@hcengineering/view-resources'
import TxViewTx from './TxViewTx.svelte'
import ActivityExtensionComponent from './ActivityExtensionComponent.svelte'
import { ActivityKey } from '../activity'
import activity from '../plugin'
import { TxDisplayViewlet, getPrevValue, getValue, updateViewlet, getExtensions } from '../utils'
export let tx: DisplayTx
export let viewlets: Map<ActivityKey, TxViewlet[]>
export let showIcon: boolean = true
export let isNew: boolean = false
export let isNextNew: boolean = false
export let contentHidden: boolean = false
export let shouldScroll: boolean = false
export let boundary: HTMLElement | undefined = undefined
// export let showDocument = false
let ptx: DisplayTx | undefined
let viewlet: TxDisplayViewlet | undefined
let props: any
let account: PersonAccount | undefined
let person: Person | undefined
let model: AttributeModel[] = []
let modelIcon: Asset | undefined = undefined
let iconComponent: AnyComponent | undefined = undefined
let extensions: ActivityExtension[] = []
let edit: boolean = false
let showDiff: boolean = false
const currentAccount = getCurrentAccount() as PersonAccount
$: if (tx.doc?._class) {
getExtensions(client, tx.doc._class).then((res?: ActivityExtension[]) => {
extensions = res || []
})
}
$: if (tx.tx._id !== ptx?.tx._id) {
if (tx.tx.modifiedBy !== account?._id) {
account = undefined
person = undefined
}
viewlet = undefined
props = undefined
model = []
ptx = tx
}
const client = getClient()
const query = createQuery()
const employeeQuery = createQuery()
function getProps (props: any, edit: boolean): any {
return { ...props, edit, attr: tx.collectionAttribute, boundary }
}
$: updateViewlet(client, viewlets, tx).then((result) => {
if (result.id === tx.tx._id) {
viewlet = result.viewlet
model = result.model
modelIcon = result.modelIcon
iconComponent = result.iconComponent
props = getProps(result.props, edit)
if (shouldScroll) {
tick().then(scrollIntoView)
}
}
})
$: query.query(
contact.class.PersonAccount,
{ _id: tx.tx.modifiedBy as Ref<PersonAccount> },
(res) => {
;[account] = res
},
{ limit: 1 }
)
$: account &&
employeeQuery.query(
contact.class.Person,
{ _id: account.person },
(res) => {
;[person] = res
},
{ limit: 1 }
)
const showMenu = async (ev: MouseEvent): Promise<void> => {
showPopup(
Menu,
{
object: tx.doc as Doc,
actions: [
{
label: activity.string.Edit,
icon: IconEdit,
action: () => {
edit = true
props = getProps(props, edit)
}
}
]
},
ev.target as HTMLElement
)
}
const onCancelEdit = () => {
edit = false
props = getProps(props, edit)
}
function isMessageType (attr?: AnyAttribute): boolean {
return attr?.type._class === core.class.TypeMarkup
}
function isAttachment (tx: TxCUD<Doc>): boolean {
return tx.objectClass === attachment.class.Attachment && tx._class === core.class.TxCreateDoc
}
function isMention (extensions: ActivityExtension[]): boolean {
return extensions.some(({ isMention }) => !!isMention)
}
async function updateMessageType (model: AttributeModel[], tx: DisplayTx): Promise<boolean> {
for (const m of model) {
if (isMessageType(m.attribute)) {
return true
}
const val = await getValue(client, m, tx)
if (val.added.length > 1 || val.removed.length > 1) {
return true
}
}
return false
}
let hasMessageType = false
$: updateMessageType(model, tx).then((res) => {
hasMessageType = res
})
$: isComment = viewlet && viewlet?.editable
$: isAttached = isAttachment(tx.tx)
$: isMentioned = isMention(extensions)
$: withAvatar = isComment || isMentioned || isAttached
$: isEmphasized = viewlet?.display === 'emphasized' || model.every((m) => isMessageType(m.attribute))
$: isColumn = isComment || isEmphasized || hasMessageType
let htmlElement: HTMLDivElement
function scrollIntoView () {
htmlElement?.scrollIntoView({ behavior: 'auto', block: 'start' })
shouldScroll = false
}
</script>
{#if (viewlet !== undefined && !((viewlet?.hideOnRemove ?? false) && tx.removed)) || model.length > 0}
<div
class="msgactivity-container"
bind:this={htmlElement}
class:showIcon
class:withAvatar
class:isNew
class:isNextNew
>
{#if showIcon}
{#if withAvatar}
<div class="msgactivity-avatar">
<Component
is={contact.component.Avatar}
props={{ avatar: person?.avatar, size: 'medium', name: person?.name }}
/>
</div>
{:else}
<div class="msgactivity-icon">
{#if iconComponent}
<Component is={iconComponent} {props} />
{:else if viewlet}
<Icon icon={viewlet.icon} size="small" />
{:else if viewlet === undefined && model.length > 0}
<Icon icon={modelIcon !== undefined ? modelIcon : IconActivityEdit} size="small" />
{:else}
<Icon icon={IconActivityEdit} size="small" />
{/if}
</div>
{/if}
{/if}
<div class="msgactivity-content" class:content={isColumn} class:comment={withAvatar}>
<div class="msgactivity-content__header">
<div class="msgactivity-content__title labels-row">
<span class={withAvatar ? 'bold' : 'strong'}>
{#if person}
{getName(client.getHierarchy(), person)}
{:else}
<Label label={core.string.System} />
{/if}
</span>
{#if viewlet && viewlet?.editable}
{#if viewlet.label}
<span class="lower"><Label label={viewlet.label} params={viewlet.labelParams ?? {}} /></span>
{/if}
{#if tx.updated}
<span class="lower"><Label label={activity.string.Edited} /></span>
{/if}
<span class="time ml-1"><TimeSince value={tx.tx.modifiedOn} /></span>
{:else if viewlet && viewlet.label}
<span class="lower whitespace-nowrap">
<Label label={viewlet.label} params={viewlet.labelParams ?? {}} />
</span>
{/if}
{#if viewlet && viewlet.labelComponent}
<Component is={viewlet.labelComponent} {props} />
{/if}
{#if viewlet === undefined && model.length > 0 && tx.updateTx}
{#each model as m}
{#await getValue(client, m, tx) then value}
{#if value.added.length}
<span class="lower"><Label label={activity.string.Added} /></span>
<span class="lower"><Label label={activity.string.To} /></span>
<span class="lower"><Label label={m.label} /></span>
{#each value.added as cvalue}
{#if value.isObjectAdded}
<ObjectPresenter value={cvalue} accent />
{:else}
<svelte:component this={m.presenter} value={cvalue} accent />
{/if}
{/each}
{:else if value.removed.length}
<span class="lower"><Label label={activity.string.Removed} /></span>
<span class="lower"><Label label={activity.string.From} /></span>
<span class="lower"><Label label={m.label} /></span>
{#each value.removed as cvalue}
{#if value.isObjectRemoved}
<ObjectPresenter value={cvalue} accent />
{:else}
<svelte:component this={m.presenter} value={cvalue} accent />
{/if}
{/each}
{:else if value.set === null || value.set === undefined || value.set === ''}
<span class="lower"><Label label={activity.string.Unset} /></span>
<span class="lower"><Label label={m.label} /></span>
{:else}
<span class="lower"><Label label={activity.string.Changed} /></span>
<span class="lower"><Label label={m.label} /></span>
{#if !hasMessageType}
<span class="lower"><Label label={activity.string.To} /></span>
<span class="strong overflow-label">
{#if value.isObjectSet}
<ObjectPresenter value={value.set} accent />
{:else}
<svelte:component this={m.presenter} value={value.set} accent />
{/if}
</span>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="show-diff" on:click={() => (showDiff = !showDiff)}>
<Label label={showDiff ? ui.string.ShowLess : ui.string.ShowMore} />
</span>
{/if}
{/if}
{/await}
{/each}
{:else if viewlet === undefined && model.length > 0 && tx.mixinTx}
{#each model as m}
{#await getValue(client, m, tx) then value}
{#if value.set === null || value.set === ''}
<span class="lower"><Label label={activity.string.Unset} /></span>
<span class="lower"><Label label={m.label} /></span>
{:else}
<span class="lower"><Label label={activity.string.Changed} /></span>
<span class="lower"><Label label={m.label} /></span>
<span class="lower"><Label label={activity.string.To} /></span>
{#if !hasMessageType}
<div class="strong overflow-label">
{#if value.isObjectSet}
<ObjectPresenter value={value.set} accent />
{:else}
<svelte:component this={m.presenter} value={value.set} accent />
{/if}
</div>
{/if}
{/if}
{/await}
{/each}
{:else if viewlet && viewlet.display === 'inline' && viewlet.component}
{#if tx.collectionAttribute !== undefined && (tx.txDocIds?.size ?? 0) > 1}
<TxViewTx {tx} {onCancelEdit} {edit} {viewlet} />
{:else if typeof viewlet.component === 'string'}
<Component is={viewlet.component} {props} on:close={onCancelEdit} />
{:else}
<svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} />
{/if}
{/if}
</div>
{#if isComment}
<div class="buttons-group">
<ActivityExtensionComponent {extensions} kind="action" props={{ object: tx.doc }} />
<!-- <Like /> -->
{#if account?.person === currentAccount?.person}
<ActionIcon icon={IconMoreH} size={'small'} action={showMenu} />
{/if}
</div>
{:else if hasMessageType || isColumn}
<span class="time top ml-4"><TimeSince value={tx.tx.modifiedOn} /></span>
{/if}
</div>
{#if !isColumn}
<span class="time top ml-4"><TimeSince value={tx.tx.modifiedOn} /></span>
{/if}
{#if viewlet && viewlet.display !== 'inline'}
<div class="activity-content content" class:indent={isAttached} class:contentHidden>
<ShowMore ignore={edit}>
{#if tx.collectionAttribute !== undefined && (tx.txDocIds?.size ?? 0) > 1}
<div class="flex-row-center flex-grow flex-wrap m--1 gap-around-2 clear-mins">
<TxViewTx {tx} {onCancelEdit} {edit} {viewlet} presentersOnly={isAttached} />
</div>
{:else if typeof viewlet.component === 'string'}
<Component is={viewlet.component} {props} on:close={onCancelEdit} />
{:else}
<svelte:component this={viewlet.component} {...props} on:close={onCancelEdit} />
{/if}
</ShowMore>
</div>
{:else if hasMessageType && model.length > 0 && (tx.updateTx || tx.mixinTx)}
{#await getValue(client, model[0], tx) then value}
{@const prevValue = getPrevValue(client, model[0], tx)}
<div class="activity-content content" class:indent={isAttached} class:contentHidden>
<ShowMore ignore={edit || prevValue !== undefined}>
{#if value.isObjectSet}
<ObjectPresenter value={value.set} />
{:else if showDiff}
<svelte:component this={model[0].presenter} value={value.set} {prevValue} showOnlyDiff />
{/if}
</ShowMore>
</div>
{/await}
{/if}
<ActivityExtensionComponent {extensions} kind="footer" props={{ object: tx.doc }} />
</div>
</div>
{/if}
<style lang="scss">
.msgactivity-container {
position: relative;
display: flex;
justify-content: space-between;
min-width: 0;
.msgactivity-icon,
.msgactivity-avatar {
display: flex;
justify-content: center;
align-items: center;
margin-right: 1rem;
width: 2.25rem;
min-width: 2.25rem;
color: var(--theme-darker-color);
}
.msgactivity-icon {
height: 1.75rem;
}
.msgactivity-avatar {
height: 2.25rem;
// background-color: var(--theme-darker-color);
border: 1px dashed var(--divider-trans-color);
border-radius: 50%;
}
.msgactivity-content {
display: flex;
flex-grow: 1;
margin-right: 1rem;
min-width: 0;
color: var(--theme-dark-color);
.msgactivity-content__header {
display: flex;
justify-content: space-between;
align-items: center;
flex-grow: 1;
min-width: 0;
}
.msgactivity-content__title {
display: inline-flex;
align-items: center;
flex-grow: 1;
min-width: 0;
}
&.content {
flex-direction: column;
padding-bottom: 0.25rem;
}
&:not(.comment) {
.msgactivity-content__header {
min-height: 1.75rem;
}
}
&:not(.content) {
align-items: center;
.msgactivity-content__header {
justify-content: space-between;
}
}
}
}
.showIcon {
&::after,
&::before {
position: absolute;
left: 1.125rem;
width: 1px;
background-color: var(--divider-trans-color);
z-index: 1;
}
&.isNew::before {
background-color: var(--highlight-red);
}
&.isNextNew {
&::after {
background-color: var(--highlight-red);
}
}
&::before {
top: -0.75rem;
height: 0.75rem;
}
&.withAvatar::after {
content: '';
top: 2.25rem;
bottom: 0;
}
&:not(.withAvatar)::after {
content: '';
top: 1.75rem;
bottom: 0;
}
}
:global(.msgactivity-container + .msgactivity-container::before) {
content: '';
}
.time {
font-size: 0.75rem;
color: var(--theme-trans-color);
&.top {
align-self: flex-start;
}
}
.activity-content {
visibility: visible;
min-width: 0;
max-height: max-content;
opacity: 1;
position: relative;
transition-property: max-height, opacity;
transition-timing-function: ease-in-out;
transition-duration: 0.15s;
&.contentHidden {
visibility: hidden;
padding: 0;
margin-top: -0.5rem;
max-height: 0;
opacity: 0;
}
&.indent {
margin-top: 0.5rem;
}
}
.show-diff {
color: var(--theme-content-color);
cursor: pointer;
&:hover {
color: var(--theme-caption-color);
}
&:active {
color: var(--theme-content-color);
}
}
</style>
@@ -1,46 +0,0 @@
<script lang="ts">
import core, { Class, Doc, Ref } from '@hcengineering/core'
import { Component, IconAdd, IconDelete } from '@hcengineering/ui'
import { getDTxProps, TxDisplayViewlet } from '../utils'
import { DisplayTx } from '@hcengineering/activity'
export let tx: DisplayTx
export let viewlet: TxDisplayViewlet
export let edit: boolean
export let presentersOnly: boolean = false
export let onCancelEdit: () => void
function filterTx (dtx: DisplayTx[], _class: Ref<Class<Doc>>): DisplayTx[] {
return dtx.filter((it) => it.tx._class === _class)
}
function getProps (ctx: DisplayTx, edit: boolean): any {
if (viewlet?.pseudo) {
return { value: ctx.doc }
}
const dprops = getDTxProps(ctx)
return { ...dprops, edit }
}
</script>
{#each filterTx([...tx.txes, tx], core.class.TxCreateDoc) as ctx, i}
{#if i === 0 && !presentersOnly}
<IconAdd size={'x-small'} fill={'var(--theme-trans-color)'} />
{/if}
{#if typeof viewlet?.component === 'string'}
<Component is={viewlet?.component} props={getProps(ctx, edit)} disabled on:close={onCancelEdit} />
{:else}
<svelte:component this={viewlet?.component} {...getProps(ctx, edit)} disabled on:close={onCancelEdit} />
{/if}
<div class="antiHSpacer" />
{/each}
{#each filterTx([...tx.txes, tx], core.class.TxRemoveDoc) as ctx, i}
{#if i === 0 && !presentersOnly}
<IconDelete size={'x-small'} fill={'var(--theme-trans-color)'} />
{/if}
{#if typeof viewlet?.component === 'string'}
<Component is={viewlet?.component} props={getProps(ctx, edit)} disabled on:close={onCancelEdit} />
{:else}
<svelte:component this={viewlet?.component} {...getProps(ctx, edit)} disabled on:close={onCancelEdit} />
{/if}
<div class="antiHSpacer" />
{/each}
+1 -13
View File
@@ -13,25 +13,13 @@
// limitations under the License.
//
import { type Doc, type Ref } from '@hcengineering/core'
import { type Resources } from '@hcengineering/platform'
import type { DisplayTx } from '@hcengineering/activity'
import Activity from './components/Activity.svelte'
import TxView from './components/TxView.svelte'
export { TxView }
import Activity from './components/Activity.svelte'
export * from './activity'
export * from './utils'
export function attributeFilter (tx: DisplayTx, _class?: Ref<Doc>): boolean {
return tx.tx.objectClass === _class
}
export default async (): Promise<Resources> => ({
filter: {
AttributeFilter: attributeFilter
},
component: {
Activity
}
+4 -4
View File
@@ -19,10 +19,10 @@ import { mergeIds } from '@hcengineering/platform'
export default mergeIds(activityId, activity, {
string: {
DocCreated: '' as IntlString,
DocAdded: '' as IntlString,
DocDeleted: '' as IntlString,
All: '' as IntlString,
CollectionUpdated: '' as IntlString,
All: '' as IntlString
DocAdded: '' as IntlString,
DocCreated: '' as IntlString,
DocDeleted: '' as IntlString
}
})
+3 -42
View File
@@ -1,4 +1,5 @@
import type { ActivityExtension, DisplayTx, TxViewlet } from '@hcengineering/activity'
import { get } from 'svelte/store'
import type { DisplayTx, TxViewlet } from '@hcengineering/activity'
import core, {
type AttachedDoc,
type Class,
@@ -15,7 +16,6 @@ import core, {
type TxOperations,
TxProcessor,
type TxUpdateDoc,
getObjectValue,
matchQuery
} from '@hcengineering/core'
import { type Asset, type IntlString, getResource, translate } from '@hcengineering/platform'
@@ -23,7 +23,7 @@ import { getAttributePresenterClass } from '@hcengineering/presentation'
import { type AnyComponent, type AnySvelteComponent, ErrorPresenter, themeStore } from '@hcengineering/ui'
import view, { type AttributeModel, type BuildModelKey, type BuildModelOptions } from '@hcengineering/view'
import { getObjectPresenter } from '@hcengineering/view-resources'
import { get } from 'svelte/store'
import { type ActivityKey, activityKey } from './activity'
import activity from './plugin'
@@ -391,42 +391,3 @@ export async function getValue (client: TxOperations, m: AttributeModel, tx: Dis
}
return value
}
export function getPrevValue (client: TxOperations, m: AttributeModel, tx: DisplayTx): any {
if (tx.txes.length > 0 && tx.txes[0].prevDoc !== undefined) {
return getObjectValue(m.key, tx.txes[0].prevDoc)
} else if (tx.prevDoc !== undefined) {
return getObjectValue(m.key, tx.prevDoc)
}
return undefined
}
export function filterCollectionTxes (txes: DisplayTx[]): DisplayTx[] {
return txes.map(filterCollectionTx).filter(Boolean) as DisplayTx[]
}
function filterCollectionTx (tx: DisplayTx): DisplayTx | undefined {
if (tx.collectionAttribute === undefined) return tx
const txes = tx.txes.reduceRight(
(txes, ctx) => {
const filtredTxes = txes.filter(
({ tx: { _class }, doc }) => doc?._id !== ctx.doc?._id || _class === core.class.TxUpdateDoc
)
return ctx.tx._class === core.class.TxUpdateDoc || filtredTxes.length === txes.length
? [ctx, ...txes]
: filtredTxes
},
[tx]
)
const txDocIds = txes.map(({ doc }) => doc?._id).filter(Boolean) as Array<Ref<Doc>>
const ctx = txes.pop()
if (ctx !== undefined) {
ctx.txes = txes
ctx.txDocIds = new Set(txDocIds)
}
return ctx
}
export async function getExtensions (client: Client, ofClass: Ref<Class<Doc>>): Promise<ActivityExtension[]> {
return await client.findAll(activity.class.ActivityExtension, { ofClass })
}
+7 -6
View File
@@ -14,23 +14,24 @@
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@types/jest": "^29.5.5",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"typescript": "^5.2.2",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"prettier-plugin-svelte": "^3.1.0"
"typescript": "^5.2.2"
},
"dependencies": {
"@hcengineering/platform": "^0.6.9",
"@hcengineering/core": "^0.6.28",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.11"
}
}
+8 -48
View File
@@ -27,10 +27,11 @@ import type {
TxMixin,
TxUpdateDoc
} from '@hcengineering/core'
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
// TODO: remove TxViewlet
/**
* Define an display for all transaction kinds for particular class.
* @public
@@ -61,6 +62,7 @@ export interface TxViewlet extends Doc {
hideOnRemove?: boolean
}
// TODO: remove DisplayTx
/**
* Transaction being displayed.
* @public
@@ -96,69 +98,27 @@ export interface DisplayTx {
originTx: TxCUD<Doc>
}
/**
* @public
*/
export interface ActivityFilter extends Doc {
label: IntlString
filter: Resource<(tx: DisplayTx, _class?: Ref<Doc>) => boolean>
}
/**
* @public
*/
export interface ExtraActivityComponent extends Class<Doc> {
component: AnyComponent
}
/**
* @public
*/
export const activityId = 'activity' as Plugin
/**
* @public
*/
export type ActivityExtensionKind = 'footer' | 'action' | 'input'
/**
* @public
*/
export interface ActivityExtension extends Doc {
ofClass: Ref<Class<Doc>>
components?: Partial<Record<ActivityExtensionKind, AnyComponent>>
isMention?: boolean
}
/**
* @public
*/
export interface TxMention extends Class<Doc> {}
export default plugin(activityId, {
icon: {
Activity: '' as Asset
},
string: {
Delete: '' as IntlString,
Edit: '' as IntlString,
Edited: '' as IntlString,
Activity: '' as IntlString,
Changed: '' as IntlString,
To: '' as IntlString,
Unset: '' as IntlString,
Added: '' as IntlString,
Changed: '' as IntlString,
Edited: '' as IntlString,
From: '' as IntlString,
Removed: '' as IntlString,
NewestFirst: '' as IntlString
},
mixin: {
ExtraActivityComponent: '' as Ref<Class<ExtraActivityComponent>>
To: '' as IntlString,
Unset: '' as IntlString
},
class: {
TxViewlet: '' as Ref<Class<TxViewlet>>,
ActivityFilter: '' as Ref<Class<ActivityFilter>>,
ActivityExtension: '' as Ref<Class<ActivityExtension>>
TxViewlet: '' as Ref<Class<TxViewlet>>
},
component: {
Activity: '' as AnyComponent
+20 -19
View File
@@ -14,43 +14,44 @@
"build:watch": "tsc --noEmit --outDir ./dist_cache"
},
"devDependencies": {
"svelte-loader": "^3.1.9",
"sass": "^1.53.0",
"svelte-preprocess": "^5.1.0",
"@hcengineering/platform-rig": "^0.6.0",
"@types/jest": "^29.5.5",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-svelte": "^2.34.0",
"prettier-plugin-svelte": "^3.1.0",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"svelte-check": "^3.6.0",
"typescript": "^5.2.2",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0",
"sass": "^1.53.0",
"svelte-check": "^3.6.0",
"svelte-eslint-parser": "^0.33.1",
"svelte-loader": "^3.1.9",
"svelte-preprocess": "^5.1.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"svelte-eslint-parser": "^0.33.1"
"typescript": "^5.2.2"
},
"dependencies": {
"@hcengineering/platform": "^0.6.9",
"svelte": "^4.2.5",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/attachment": "^0.6.9",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/login": "^0.6.8",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/panel": "^0.6.15",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/preference": "^0.6.9",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/panel": "^0.6.15",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/login": "^0.6.8",
"filesize": "^8.0.3",
"@hcengineering/preference": "^0.6.9",
"@hcengineering/activity": "^0.6.0"
"svelte": "^4.2.5"
},
"repository": "https://github.com/hcengineering/anticrm",
"publishConfig": {
@@ -0,0 +1,32 @@
<!--
// 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 { DocUpdateMessage } from '@hcengineering/notification'
import { Attachment } from '@hcengineering/attachment'
import AttachmentPresenter from '../AttachmentPresenter.svelte'
import RemovedAttachmentPresenter from '../RemovedAttachmentPresenter.svelte'
export let message: DocUpdateMessage
export let value: Attachment | undefined
</script>
{#if value}
{#if message.action === 'remove'}
<RemovedAttachmentPresenter {value} />
{:else}
<AttachmentPresenter {value} />
{/if}
{/if}
+13 -11
View File
@@ -14,17 +14,12 @@
//
import attachment, { type Attachment } from '@hcengineering/attachment'
import {
type ObjQueryType,
SortingOrder,
type SortingQuery,
type Markup,
type Ref,
type Doc
} from '@hcengineering/core'
import { type ObjQueryType, SortingOrder, type SortingQuery, type Markup } from '@hcengineering/core'
import { type IntlString, type Resources } from '@hcengineering/platform'
import preference from '@hcengineering/preference'
import { getClient } from '@hcengineering/presentation'
import notification, { type ActivityMessage, type DocUpdateMessage } from '@hcengineering/notification'
import TxAttachmentCreate from './components/activity/TxAttachmentCreate.svelte'
import AddAttachment from './components/AddAttachment.svelte'
import AttachmentDocList from './components/AttachmentDocList.svelte'
@@ -44,8 +39,8 @@ import AccordionEditor from './components/AccordionEditor.svelte'
import IconUploadDuo from './components/icons/UploadDuo.svelte'
import IconAttachment from './components/icons/Attachment.svelte'
import { deleteFile, uploadFile } from './utils'
import { type DisplayTx } from '@hcengineering/activity'
import AttachmentPreview from './components/AttachmentPreview.svelte'
import NotificationAttachmentChanged from './components/notification/NotificationAttachmentChanged.svelte'
export {
AddAttachment,
@@ -239,8 +234,12 @@ export async function DeleteAttachment (attach: Attachment): Promise<void> {
)
}
export function attachmentsFilter (tx: DisplayTx, _class?: Ref<Doc>): boolean {
return tx.tx.objectClass === attachment.class.Attachment
export function attachmentsFilter (message: ActivityMessage): boolean {
if (message._class === notification.class.DocUpdateMessage) {
return (message as DocUpdateMessage).objectClass === attachment.class.Attachment
}
return false
}
export default async (): Promise<Resources> => ({
@@ -255,6 +254,9 @@ export default async (): Promise<Resources> => ({
FileBrowser,
Photos
},
notification: {
NotificationAttachmentChanged
},
activity: {
TxAttachmentCreate
},
+9 -9
View File
@@ -30,18 +30,18 @@
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/platform": "^0.6.9",
"@hcengineering/core": "^0.6.28",
"@hcengineering/preference": "^0.6.9",
"@hcengineering/tags": "^0.6.12",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/chunter": "^0.6.12",
"@hcengineering/attachment": "^0.6.9",
"fast-equals": "^2.0.3",
"qs": "~6.11.0",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/gmail": "^0.6.15",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/preference": "^0.6.9",
"@hcengineering/recruit": "^0.6.21",
"@hcengineering/task": "^0.6.13"
"@hcengineering/tags": "^0.6.12",
"@hcengineering/task": "^0.6.13",
"fast-equals": "^2.0.3",
"qs": "~6.11.0"
},
"repository": "https://github.com/hcengineering/anticrm",
"publishConfig": {
+3 -3
View File
@@ -1,5 +1,4 @@
import attachment, { Attachment } from '@hcengineering/attachment'
import chunter, { Comment } from '@hcengineering/chunter'
import contact, { Channel, combineName, Contact, Employee, PersonAccount } from '@hcengineering/contact'
import core, {
Account,
@@ -41,6 +40,7 @@ import {
LoginInfo
} from './types'
import { convert, ConvertResult } from './utils'
import notification, { ChatMessage } from '@hcengineering/notification'
async function updateDoc (client: ApplyOperations, doc: Doc, raw: Doc | Data<Doc>, date: Timestamp): Promise<Doc> {
// We need to update fields if they are different.
@@ -785,9 +785,9 @@ async function downloadComments (
order: { ID: ops.direction }
})
for (const it of commentsData.result) {
const c: Comment & BitrixSyncDoc = {
const c: ChatMessage & BitrixSyncDoc = {
_id: generateId(),
_class: chunter.class.Comment,
_class: notification.class.ChatMessage,
message: processComment(it.COMMENT as string),
bitrixId: `${it.ID as string}`,
type: it.ENTITY_TYPE,
@@ -16,7 +16,6 @@
<script lang="ts">
import { AttachmentDroppable, AttachmentsPresenter } from '@hcengineering/attachment-resources'
import type { Card } from '@hcengineering/board'
import { CommentsPresenter } from '@hcengineering/chunter-resources'
import { Employee } from '@hcengineering/contact'
import type { Ref, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification'
@@ -41,6 +40,7 @@
import { hasDate, openCardPanel, updateCard, updateCardMembers } from '../utils/CardUtils'
import CheckListsPresenter from './presenters/ChecklistsPresenter.svelte'
import NotificationPresenter from './presenters/NotificationPresenter.svelte'
import { ChatMessagesPresenter } from '@hcengineering/notification-resources'
export let object: WithLookup<Card>
@@ -196,7 +196,7 @@
{/if}
{#if (object.comments ?? 0) > 0}
<div class="float-left">
<CommentsPresenter value={object.comments} {object} />
<ChatMessagesPresenter value={object.comments} {object} size="small" />
</div>
{/if}
{#if (object.todoItems ?? 0) > 0}
+4 -4
View File
@@ -17,7 +17,8 @@
"About": "About",
"Members": "Members",
"NoMembers": "No members",
"MentionedIn": "mentioned you in ",
"MentionedIn": "Mentioned you in ",
"Mentioned": "Mentioned",
"ContactInfo": "Contact Info",
"Content": "Content",
"Comment": "Comment",
@@ -67,8 +68,6 @@
"Messages": "Messages",
"NoResults": "No results",
"CopyLink": "Copy link",
"FilterComments": "Comments",
"FilterPinnedComments": "Pinned comments",
"FilterBacklinks": "Backlinks",
"DM": "Direct message",
"DMNotification": "Sent you a message",
@@ -80,6 +79,7 @@
"NoMessages": "There are no messages yet",
"DirectNotificationTitle": "{senderName}",
"DirectNotificationBody": "{message}",
"AddCommentPlaceholder": "Add comment..."
"AddCommentPlaceholder": "Add comment...",
"Reacted": "Reacted"
}
}
+4 -4
View File
@@ -17,7 +17,8 @@
"About": "Информация",
"Members": "Участники",
"NoMembers": "Нет участников",
"MentionedIn": "упомянул(а) вас в",
"MentionedIn": "Упомянул(а) вас в",
"Mentioned": "Упомянул(а)",
"ContactInfo": "Контактная информация",
"Content": "Содержимое",
"Comment": "Комментарий",
@@ -67,8 +68,6 @@
"Messages": "Сообщения",
"NoResults": "Нет результатов",
"CopyLink": "Копировать ссылку",
"FilterComments": "Комментарии",
"FilterPinnedComments": "Закрепленные комментарии",
"FilterBacklinks": "Упоминания",
"DM": "Личное сообщение",
"DMNotification": "Отправил сообщение",
@@ -80,6 +79,7 @@
"NoMessages": "Сообщений пока нет",
"DirectNotificationTitle": "{senderName}",
"DirectNotificationBody": "{message}",
"AddCommentPlaceholder": "Добавить комментарий..."
"AddCommentPlaceholder": "Добавить комментарий...",
"Reacted": "Отреагировал(а)"
}
}
+27 -27
View File
@@ -14,48 +14,48 @@
"build:watch": "tsc --noEmit --outDir ./dist_cache"
},
"devDependencies": {
"svelte-loader": "^3.1.9",
"sass": "^1.53.0",
"svelte-preprocess": "^5.1.0",
"@hcengineering/platform-rig": "^0.6.0",
"@types/jest": "^29.5.5",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-svelte": "^2.34.0",
"prettier-plugin-svelte": "^3.1.0",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"svelte-check": "^3.6.0",
"typescript": "^5.2.2",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0",
"sass": "^1.53.0",
"svelte-check": "^3.6.0",
"svelte-eslint-parser": "^0.33.1",
"svelte-loader": "^3.1.9",
"svelte-preprocess": "^5.1.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"svelte-eslint-parser": "^0.33.1"
"typescript": "^5.2.2"
},
"dependencies": {
"@hcengineering/core": "^0.6.28",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/chunter": "^0.6.12",
"@hcengineering/login": "^0.6.8",
"svelte": "^4.2.5",
"fast-equals": "^2.0.3",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/notification-resources": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/attachment": "^0.6.9",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/view": "^0.6.9",
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/chunter": "^0.6.12",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/core": "^0.6.28",
"@hcengineering/login": "^0.6.8",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/notification-resources": "^0.6.0",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/preference": "^0.6.9",
"@hcengineering/workbench-resources": "^0.6.1"
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/workbench": "^0.6.9",
"@hcengineering/workbench-resources": "^0.6.1",
"fast-equals": "^2.0.3",
"svelte": "^4.2.5"
}
}
@@ -16,9 +16,16 @@
import type { Backlink } from '@hcengineering/chunter'
import { MessageViewer } from '@hcengineering/presentation'
// export let tx: TxCreateDoc<Backlink>
export let value: Backlink
// export let edit: boolean = false
</script>
<MessageViewer message={value.message} />
<div class="root">
<MessageViewer message={value?.message} />
</div>
<style lang="scss">
.root {
display: flex;
flex-direction: column;
}
</style>
@@ -20,24 +20,19 @@
import { getObjectPresenter } from '@hcengineering/view-resources'
export let value: Backlink
export let inline = true
const client = getClient()
let presenter: AttributeModel | undefined
const docQuery = createQuery()
// const targetQuery = createQuery()
let doc: Doc | undefined
// let target: Doc | undefined
$: value.backlinkClass != null &&
docQuery.query(value.backlinkClass, { _id: value.backlinkId }, (r) => {
doc = r.shift()
})
// $: targetQuery.query(value.attachedToClass, { _id: value.attachedTo }, (r) => {
// target = r.shift()
// })
$: if (doc !== undefined) {
getObjectPresenter(client, doc._class, { key: '' }).then((p) => {
presenter = p
@@ -47,6 +42,6 @@
{#if presenter}
<span class="labels-row">
<svelte:component this={presenter.presenter} value={doc} inline />
<svelte:component this={presenter.presenter} value={doc} {inline} embedded shouldShowAvatar={false} />
</span>
{/if}
@@ -20,6 +20,8 @@
export let value: Channel
export let inline: boolean = false
export let shouldShowAvatar = true
const client = getClient()
$: icon = client.getHierarchy().getClass(value._class).icon
@@ -28,7 +30,7 @@
{#if value}
<NavLink app={chunterId} space={value._id}>
<div class="flex-presenter">
{#if !inline}
{#if !inline && shouldShowAvatar}
<div class="icon">
{#if icon}
<Icon {icon} size={'small'} />
@@ -1,65 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
//
// 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 { getName, Person } from '@hcengineering/contact'
import { Avatar } from '@hcengineering/contact-resources'
import { getClient } from '@hcengineering/presentation'
interface IMessage {
text: string
createDate: Date
}
export let user: Person
export let message: IMessage
const client = getClient()
</script>
<div class="flex-nowrap">
<div class="avatar"><Avatar size={'medium'} avatar={user.avatar} name={user.name} /></div>
<div class="flex-col-stretch message">
<div class="header">{getName(client.getHierarchy(), user)}<span>{message.createDate}</span></div>
<div class="text">{message.text}</div>
</div>
</div>
<style lang="scss">
.avatar {
margin-right: 1rem;
}
.message {
margin-right: 1.25rem;
.header {
margin-bottom: 0.25rem;
font-weight: 500;
font-size: 1rem;
line-height: 150%;
color: var(--caption-color);
span {
margin-left: 0.5rem;
font-weight: 400;
font-size: 0.875rem;
color: var(--dark-color);
}
}
.text {
line-height: 150%;
color: var(--content-color);
}
}
</style>
@@ -1,134 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 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 { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { Comment } from '@hcengineering/chunter'
import { AttachedData, Doc, generateId, Ref } from '@hcengineering/core'
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import chunter from '../plugin'
export let object: Doc
export let shouldSaveDraft: boolean = true
export let focusIndex: number = -1
export let boundary: HTMLElement | undefined = undefined
const client = getClient()
const _class = chunter.class.Comment
type CommentDraft = AttachedData<Comment> & { _id: Ref<Comment> }
const draftKey = `${object._id}_comment`
const draftController = new DraftController<CommentDraft>(draftKey)
const empty = {
message: '<p></p>',
attachments: 0
}
let commentInputBox: AttachmentRefInput
const draftComment = shouldSaveDraft ? $draftsStore[draftKey] : undefined
let comment: CommentDraft = draftComment ?? getDefault()
let _id: Ref<Comment> = comment._id
let inputContent: string = comment.message
const createdQuery = createQuery()
$: createdQuery.query(chunter.class.Comment, { _id }, (docs) => {
if (docs.length > 0) {
// Ouch we have got comment with same id created already.
comment = getDefault()
_id = comment._id
commentInputBox.removeDraft(false)
}
})
function objectChange (object: CommentDraft, empty: any) {
if (shouldSaveDraft) {
draftController.save(object, empty)
}
}
$: objectChange(comment, empty)
function getDefault (): CommentDraft {
return {
_id: generateId(),
...empty
}
}
async function onUpdate (event: CustomEvent) {
if (!shouldSaveDraft) {
return
}
const { message, attachments } = event.detail
if (comment) {
comment.message = message
comment.attachments = message
} else {
comment = {
_id,
message,
attachments
}
}
}
let loading = false
async function onMessage (event: CustomEvent) {
loading = true
const { message, attachments } = event.detail
try {
draftController.remove()
commentInputBox.removeDraft(false)
await client.addCollection<Doc, Comment>(
_class,
object.space,
object._id,
object._class,
'comments',
{ message, attachments },
_id
)
// Remove draft from Local Storage
comment = getDefault()
_id = comment._id
} catch (err) {
console.error(err)
} finally {
loading = false
}
}
</script>
<AttachmentRefInput
{focusIndex}
bind:this={commentInputBox}
bind:content={inputContent}
{_class}
space={object.space}
bind:objectId={_id}
{shouldSaveDraft}
{boundary}
on:message={onMessage}
on:update={onUpdate}
on:focus
on:blur
bind:loading
/>
@@ -1,95 +0,0 @@
<!--
// Copyright © 2023 Anticrm Platform Contributors.
//
// 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 contact, { PersonAccount } from '@hcengineering/contact'
import chunter, { Backlink, Comment } from '@hcengineering/chunter'
import view, { ObjectPanel } from '@hcengineering/view'
import { createQuery, getClient } from '@hcengineering/presentation'
import { AnyComponent, Component, Loading } from '@hcengineering/ui'
import { Class, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
export let _id: Ref<Comment> | undefined
export let embedded: boolean = true
const client = getClient()
const hierarchy = client.getHierarchy()
let comment: Comment | undefined
const commentQuery = createQuery()
let loadingComment = true
$: if (_id !== undefined) {
commentQuery.query(chunter.class.Comment, { _id }, (res) => {
;[comment] = res
loadingComment = false
})
} else {
commentQuery.unsubscribe()
loadingComment = false
}
let backlinks: Backlink[] = []
const backlinksQuery = createQuery()
let loadingBacklinks = true
const mePerson = (getCurrentAccount() as PersonAccount).person
let isMeMentioned = false
$: if (_id !== undefined) {
backlinksQuery.query(chunter.class.Backlink, { attachedDocId: _id }, (res) => {
backlinks = res
for (const backlink of backlinks) {
if (hierarchy.isDerived(backlink.attachedToClass, contact.class.Person) && mePerson === backlink.attachedTo) {
isMeMentioned = true
break
}
}
loadingBacklinks = false
})
} else {
backlinksQuery.unsubscribe()
loadingBacklinks = false
}
let attachedDocId: Ref<Doc> | undefined
let attachedDocClass: Ref<Class<Doc>> | undefined
$: loading = loadingComment || loadingBacklinks
let component: AnyComponent
$: getComponent(loading, comment)
async function getComponent (loading: boolean, comment?: Comment): Promise<void> {
if (comment == null || loading) {
return
}
let panelComponent: ObjectPanel | undefined
if (isMeMentioned) {
panelComponent = hierarchy.classHierarchyMixin(comment.attachedToClass, view.mixin.ObjectPanel)
}
component = panelComponent?.component ?? view.component.EditDoc
attachedDocId = comment.attachedTo
attachedDocClass = comment.attachedToClass
}
</script>
{#if loading}
<Loading />
{:else if component && attachedDocId && attachedDocClass}
<Component is={component} props={{ _id: attachedDocId, _class: attachedDocClass, embedded }} on:close />
{/if}
@@ -1,117 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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 { AttachmentDocList } from '@hcengineering/attachment-resources'
import type { Comment } from '@hcengineering/chunter'
import { Person, PersonAccount, getName } from '@hcengineering/contact'
import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref } from '@hcengineering/core'
import { MessageViewer, getClient } from '@hcengineering/presentation'
import { Icon, ShowMore, TimeSince } from '@hcengineering/ui'
import { LinkPresenter, ObjectPresenter } from '@hcengineering/view-resources'
import view from '@hcengineering/view'
export let value: Comment
export let inline: boolean = false
// export let disabled = false
export let inbox: boolean = false
const client = getClient()
async function getEmployee (
value: Comment,
employees: IdMap<Person>,
accounts: IdMap<PersonAccount>
): Promise<Person | undefined> {
const acc = accounts.get(value.modifiedBy as Ref<PersonAccount>)
if (acc !== undefined) {
const emp = employees.get(acc.person)
return emp
}
}
$: links = getLinks(value.message)
function getLinks (content: string): HTMLLinkElement[] {
const parser = new DOMParser()
const parent = parser.parseFromString(content, 'text/html').firstChild?.childNodes[1] as HTMLElement
return parseLinks(parent.childNodes)
}
function parseLinks (nodes: NodeListOf<ChildNode>): HTMLLinkElement[] {
const res: HTMLLinkElement[] = []
nodes.forEach((p) => {
if (p.nodeType !== Node.TEXT_NODE) {
if (p.nodeName === 'A') {
res.push(p as HTMLLinkElement)
}
res.push(...parseLinks(p.childNodes))
}
})
return res
}
</script>
{#if inline}
{#if !inbox}
<ObjectPresenter _class={value.attachedToClass} objectId={value.attachedTo} accent noUnderline />
{:else}
<MessageViewer message={value.message} />
{/if}
{:else}
<div class="flex-row-top">
{#await getEmployee(value, $personByIdStore, $personAccountByIdStore) then employee}
<div class="avatar">
<Avatar size={'medium'} avatar={employee?.avatar} name={employee?.name} />
</div>
<div class="flex-grow flex-col select-text">
<div class="header">
<div class="fs-title">
{#if employee}{getName(client.getHierarchy(), employee)}{/if}
</div>
<div class="content-dark-color ml-4 flex-row-center">
{#if value.pinned}
<Icon icon={view.icon.Pin} size={'small'} fill="#3265cb" />
{/if}
<div class="ml-1">
<TimeSince value={value.modifiedOn} />
</div>
</div>
</div>
<ShowMore fixed>
<MessageViewer message={value.message} />
<AttachmentDocList {value} />
{#each links as link}
<LinkPresenter {link} />
{/each}
</ShowMore>
</div>
{/await}
</div>
{/if}
<style lang="scss">
.avatar {
margin-right: 1rem;
min-width: 2.25rem;
}
.header {
display: inline-flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.25rem;
}
</style>
@@ -19,7 +19,7 @@
import type { ChunterMessage, ChunterMessageExtension, Message, Reaction } from '@hcengineering/chunter'
import { PersonAccount } from '@hcengineering/contact'
import { Avatar, personByIdStore, EmployeePresenter } from '@hcengineering/contact-resources'
import { getCurrentAccount, Mixin, Ref, WithLookup } from '@hcengineering/core'
import { getCurrentAccount, getDisplayTime, Mixin, Ref, WithLookup } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { getClient, MessageViewer } from '@hcengineering/presentation'
import ui, { ActionIcon, Button, EmojiPopup, IconMoreV, Label, showPopup, tooltip } from '@hcengineering/ui'
@@ -35,7 +35,7 @@
import { AddMessageToSaved, DeleteMessageFromSaved, UnpinMessage } from '../index'
import chunter from '../plugin'
import { getLinks, getTime, updateDocReactions } from '../utils'
import { getLinks, updateDocReactions } from '../utils'
export let message: WithLookup<ChunterMessage>
export let savedAttachmentsIds: Ref<Attachment>[]
@@ -188,9 +188,9 @@
{#if employee}
<EmployeePresenter value={employee} shouldShowAvatar={false} />
{/if}
<span class="text-sm">{getTime(message.createdOn ?? 0)}</span>
<span class="text-sm">{getDisplayTime(message.createdOn ?? 0)}</span>
{#if message.editedOn}
<span use:tooltip={{ label: ui.string.TimeTooltip, props: { value: getTime(message.editedOn) } }}>
<span use:tooltip={{ label: ui.string.TimeTooltip, props: { value: getDisplayTime(message.editedOn) } }}>
<Label label={chunter.string.Edited} />
</span>
{/if}
@@ -24,6 +24,7 @@
export let value: Message
export let inline: boolean = false
export let embedded = false
export let disabled = false
const client = getClient()
@@ -41,7 +42,7 @@
})
</script>
{#if inline}
{#if inline || embedded}
{#if presenter && doc}
<div class="flex-presenter">
{#if isThreadMessage}
@@ -53,7 +54,7 @@
</span>
&nbsp;
{/if}
<svelte:component this={presenter.presenter} value={doc} inline {disabled} />
<svelte:component this={presenter.presenter} value={doc} {inline} {disabled} shouldShowAvatarr={false} />
</div>
{/if}
{:else}
@@ -19,13 +19,13 @@
import { LinkPresenter } from '@hcengineering/view-resources'
import { AttachmentList } from '@hcengineering/attachment-resources'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Ref, WithLookup, getCurrentAccount } from '@hcengineering/core'
import { Ref, WithLookup, getCurrentAccount, getDisplayTime } from '@hcengineering/core'
import { Attachment } from '@hcengineering/attachment'
import { EmployeePresenter, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { PersonAccount } from '@hcengineering/contact'
import chunter from '../plugin'
import { getLinks, getTime } from '../utils'
import { getLinks } from '../utils'
export let value: WithLookup<ChunterMessage>
@@ -51,9 +51,9 @@
<Label label={chunter.string.You} />
{/if}
{/if}
<span>{getTime(value.createdOn ?? 0)}</span>
<span>{getDisplayTime(value.createdOn ?? 0)}</span>
{#if value.editedOn}
<span use:tooltip={{ label: ui.string.TimeTooltip, props: { value: getTime(value.editedOn) } }}>
<span use:tooltip={{ label: ui.string.TimeTooltip, props: { value: getDisplayTime(value.editedOn) } }}>
<Label label={getEmbeddedLabel('Edited')} />
</span>
{/if}
@@ -2,12 +2,11 @@
import chunter, { ChunterMessage } from '@hcengineering/chunter'
import { Person, PersonAccount, getName } from '@hcengineering/contact'
import { Avatar, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref, Space } from '@hcengineering/core'
import { getDisplayTime, IdMap, Ref, Space } from '@hcengineering/core'
import { MessageViewer, createQuery, getClient } from '@hcengineering/presentation'
import { IconClose } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { UnpinMessage } from '../index'
import { getTime } from '../utils'
export let space: Ref<Space>
@@ -70,7 +69,7 @@
</div>
</div>
<MessageViewer message={message.content} />
<span class="time">{getTime(message.createdOn ?? 0)}</span>
<span class="time">{getDisplayTime(message.createdOn ?? 0)}</span>
</div>
{/each}
</div>
@@ -13,15 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import { Reaction } from '@hcengineering/chunter'
import { Account, Ref } from '@hcengineering/core'
import { tooltip } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { Reaction } from '@hcengineering/chunter'
import { Account, Doc, Ref } from '@hcengineering/core'
import { EmojiPopup, IconAdd, showPopup, tooltip } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import ReactionsTooltip from './ReactionsTooltip.svelte'
import { updateDocReactions } from '../utils'
export let reactions: Reaction[] = []
export let object: Doc | undefined = undefined
const dispatch = createEventDispatcher()
const client = getClient()
let reactionsAccounts = new Map<string, Ref<Account>[]>()
$: {
@@ -33,25 +38,43 @@
})
reactionsAccounts = reactionsAccounts
}
function getClickHandler (emoji: string) {
return (e: CustomEvent) => {
e.stopPropagation()
e.preventDefault()
dispatch('click', emoji)
}
}
function openEmojiPalette (ev: Event) {
showPopup(EmojiPopup, {}, ev.target as HTMLElement, (emoji: string) => {
updateDocReactions(client, reactions, object, emoji)
})
}
</script>
<div class="container">
{#each [...reactionsAccounts] as [emoji, accounts]}
<div class="reaction over-underline">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex-row-center"
use:tooltip={{ component: ReactionsTooltip, props: { reactionAccounts: accounts } }}
on:click={() => {
dispatch('click', emoji)
}}
>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="item border-radius-1"
use:tooltip={{ component: ReactionsTooltip, props: { reactionAccounts: accounts } }}
on:click={getClickHandler(emoji)}
>
<div class="flex-row-center">
<div>{emoji}</div>
<div class="caption-color counter">{accounts.length}</div>
</div>
</div>
{/each}
{#if object && reactionsAccounts.size > 0}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="item flex-row-center border-radius-1" class:withoutBackground={true} on:click={openEmojiPalette}>
<IconAdd size="small" />
</div>
{/if}
</div>
<style lang="scss">
@@ -59,11 +82,32 @@
display: flex;
flex-wrap: wrap;
user-select: none;
column-gap: 1rem;
column-gap: 0.125rem;
row-gap: 0.25rem;
}
.counter {
margin-left: 0.25rem;
.counter {
font-size: 0.75rem;
color: var(--theme-dark-color);
margin-left: 0.25rem;
}
.item {
display: flex;
align-items: center;
justify-content: center;
width: 2.625rem;
height: 1.5rem;
background: var(--secondary-button-disabled);
border: none;
cursor: pointer;
&:hover {
border: 1px solid var(--theme-darker-color);
}
&.withoutBackground {
background: transparent;
}
}
</style>
@@ -20,12 +20,15 @@
import chunter from '../plugin'
import { updateDocReactions } from '../utils'
import { createEventDispatcher } from 'svelte'
export let object: Doc | undefined = undefined
const client = getClient()
const reactionsQuery = createQuery()
const dispatch = createEventDispatcher()
let reactions: Reaction[] = []
$: if (object) {
@@ -35,11 +38,15 @@
}
function openEmojiPalette (ev: Event) {
dispatch('open')
showPopup(
EmojiPopup,
{},
ev.target as HTMLElement,
(emoji: string) => updateDocReactions(client, reactions, object, emoji),
(emoji: string) => {
updateDocReactions(client, reactions, object, emoji)
dispatch('close')
},
() => {}
)
}
@@ -22,7 +22,7 @@
import { updateDocReactions } from '../utils'
import chunter from '../plugin'
export let object: Doc | undefined = undefined
export let object: Doc | undefined
const client = getClient()
const reactionsQuery = createQuery()
@@ -42,6 +42,6 @@
{#if reactions.length}
<div class="footer flex-col p-inline contrast mt-2">
<Reactions {reactions} on:click={handleClick} />
<Reactions {reactions} {object} on:click={handleClick} />
</div>
{/if}

Some files were not shown because too many files have changed in this diff Show More