mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-21 16:09:12 +03:00
UBERF-8582: Fix triggers (#7155)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
a220fac255
commit
e0ca033405
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -55,7 +55,8 @@
|
||||
// "VERSION": "0.6.289",
|
||||
"ELASTIC_INDEX_NAME": "local_storage_index",
|
||||
"UPLOAD_URL": "/files",
|
||||
"AI_BOT_URL": "http://localhost:4010"
|
||||
"AI_BOT_URL": "http://localhost:4010",
|
||||
"STATS_URL": "http://host.docker.internal:4900",
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"runtimeVersion": "20",
|
||||
|
@ -102,7 +102,7 @@ services:
|
||||
environment:
|
||||
- PORT=4900
|
||||
- SERVER_SECRET=secret
|
||||
restart: unless-stopped
|
||||
restart: unless-stopped
|
||||
workspace:
|
||||
image: hardcoreeng/workspace
|
||||
extra_hosts:
|
||||
@ -221,7 +221,6 @@ services:
|
||||
links:
|
||||
- mongodb
|
||||
- minio
|
||||
- rekoni
|
||||
- account
|
||||
- stats
|
||||
# - apm-server
|
||||
@ -261,7 +260,6 @@ services:
|
||||
links:
|
||||
- postgres
|
||||
- minio
|
||||
- rekoni
|
||||
- account
|
||||
- stats
|
||||
# - apm-server
|
||||
@ -296,10 +294,6 @@ services:
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 4004:4004
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1024M
|
||||
fulltext:
|
||||
image: hardcoreeng/fulltext
|
||||
extra_hosts:
|
||||
@ -319,10 +313,6 @@ services:
|
||||
- STATS_URL=http://host.docker.internal:4900
|
||||
- REKONI_URL=http://host.docker.internal:4004
|
||||
- ACCOUNTS_URL=http://host.docker.internal:3000
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 300M
|
||||
fulltext_pg:
|
||||
image: hardcoreeng/fulltext
|
||||
extra_hosts:
|
||||
@ -343,10 +333,6 @@ services:
|
||||
- STATS_URL=http://host.docker.internal:4900
|
||||
- REKONI_URL=http://host.docker.internal:4004
|
||||
- ACCOUNTS_URL=http://host.docker.internal:3000
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 300M
|
||||
print:
|
||||
image: hardcoreeng/print
|
||||
extra_hosts:
|
||||
@ -358,10 +344,6 @@ services:
|
||||
- SECRET=secret
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- STATS_URL=http://host.docker.internal:4900
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 300M
|
||||
sign:
|
||||
image: hardcoreeng/sign
|
||||
extra_hosts:
|
||||
@ -382,10 +364,6 @@ services:
|
||||
- SERVICE_ID=sign-service
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
- STATS_URL=http://host.docker.internal:4900
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 300M
|
||||
analytics:
|
||||
image: hardcoreeng/analytics-collector
|
||||
extra_hosts:
|
||||
@ -402,10 +380,6 @@ services:
|
||||
- ACCOUNTS_URL=http://host.docker.internal:3000
|
||||
- SUPPORT_WORKSPACE=support
|
||||
- STATS_URL=http://host.docker.internal:4900
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 300M
|
||||
aiBot:
|
||||
image: hardcoreeng/ai-bot
|
||||
ports:
|
||||
@ -426,10 +400,6 @@ services:
|
||||
- STATS_URL=http://host.docker.internal:4900
|
||||
# - LOVE_ENDPOINT=http://host.docker.internal:8096
|
||||
# - OPENAI_API_KEY=token
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 300M
|
||||
# telegram-bot:
|
||||
# image: hardcoreeng/telegram-bot
|
||||
# extra_hosts:
|
||||
@ -445,10 +415,6 @@ services:
|
||||
# - ACCOUNTS_URL=http://host.docker.internal:3000
|
||||
# - SERVICE_ID=telegram-bot-service
|
||||
# - STATS_URL=http://host.docker.internal:4900
|
||||
# deploy:
|
||||
# resources:
|
||||
# limits:
|
||||
# memory: 300M
|
||||
volumes:
|
||||
db:
|
||||
dbpg:
|
||||
|
@ -2,50 +2,51 @@
|
||||
// Copyright @ 2022-2023 Hardcore Engineering Inc.
|
||||
//
|
||||
|
||||
import attachment, { type Attachment } from '@hcengineering/attachment'
|
||||
import {
|
||||
clone,
|
||||
loadCollaborativeDoc,
|
||||
saveCollaborativeDoc,
|
||||
YAbstractType,
|
||||
YXmlElement,
|
||||
YXmlText
|
||||
} from '@hcengineering/collaboration'
|
||||
import {
|
||||
type ChangeControl,
|
||||
type ControlledDocument,
|
||||
createChangeControl,
|
||||
createDocumentTemplate,
|
||||
type DocumentCategory,
|
||||
documentsId,
|
||||
DocumentState
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import {
|
||||
type Class,
|
||||
type Data,
|
||||
type Ref,
|
||||
TxOperations,
|
||||
generateId,
|
||||
type Doc,
|
||||
DOMAIN_TX,
|
||||
generateId,
|
||||
makeCollaborativeDoc,
|
||||
MeasureMetricsContext,
|
||||
type Class,
|
||||
type Doc,
|
||||
SortingOrder
|
||||
type Ref,
|
||||
SortingOrder,
|
||||
toIdMap,
|
||||
TxOperations
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
createDefaultSpace,
|
||||
createOrUpdate,
|
||||
type MigrateUpdate,
|
||||
type MigrationDocumentQuery,
|
||||
tryMigrate,
|
||||
tryUpgrade,
|
||||
type MigrateOperation,
|
||||
type MigrateUpdate,
|
||||
type MigrationClient,
|
||||
type MigrationUpgradeClient
|
||||
type MigrationDocumentQuery,
|
||||
type MigrationUpgradeClient,
|
||||
tryMigrate,
|
||||
tryUpgrade
|
||||
} from '@hcengineering/model'
|
||||
import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
|
||||
import core from '@hcengineering/model-core'
|
||||
import tags from '@hcengineering/tags'
|
||||
import {
|
||||
type ChangeControl,
|
||||
type DocumentCategory,
|
||||
DocumentState,
|
||||
documentsId,
|
||||
createDocumentTemplate,
|
||||
type ControlledDocument,
|
||||
createChangeControl
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import {
|
||||
loadCollaborativeDoc,
|
||||
saveCollaborativeDoc,
|
||||
YXmlElement,
|
||||
YXmlText,
|
||||
YAbstractType,
|
||||
clone
|
||||
} from '@hcengineering/collaboration'
|
||||
import attachment, { type Attachment } from '@hcengineering/attachment'
|
||||
import { DOMAIN_ATTACHMENT } from '@hcengineering/model-attachment'
|
||||
|
||||
import documents, { DOMAIN_DOCUMENTS } from './index'
|
||||
|
||||
@ -210,6 +211,7 @@ async function createDocumentCategories (tx: TxOperations): Promise<void> {
|
||||
{ code: 'CM', title: 'Client Management' }
|
||||
]
|
||||
|
||||
const catsCache = toIdMap(await tx.findAll(documents.class.DocumentCategory, {}))
|
||||
const ops = tx.apply()
|
||||
for (const c of categories) {
|
||||
await createOrUpdate(
|
||||
@ -217,7 +219,8 @@ async function createDocumentCategories (tx: TxOperations): Promise<void> {
|
||||
documents.class.DocumentCategory,
|
||||
documents.space.QualityDocuments,
|
||||
{ ...c, attachments: 0 },
|
||||
((documents.category.DOC as string) + ' - ' + c.code) as Ref<DocumentCategory>
|
||||
((documents.category.DOC as string) + ' - ' + c.code) as Ref<DocumentCategory>,
|
||||
catsCache
|
||||
)
|
||||
}
|
||||
await ops.commit()
|
||||
|
@ -14,7 +14,15 @@
|
||||
//
|
||||
|
||||
import { getCategories } from '@anticrm/skillset'
|
||||
import core, { DOMAIN_TX, TxOperations, type Ref, type Space, type Status } from '@hcengineering/core'
|
||||
import core, {
|
||||
DOMAIN_TX,
|
||||
toIdMap,
|
||||
TxOperations,
|
||||
type Doc,
|
||||
type Ref,
|
||||
type Space,
|
||||
type Status
|
||||
} from '@hcengineering/core'
|
||||
import {
|
||||
createOrUpdate,
|
||||
migrateSpace,
|
||||
@ -26,7 +34,7 @@ import {
|
||||
type ModelLogger
|
||||
} from '@hcengineering/model'
|
||||
import tags, { type TagCategory } from '@hcengineering/model-tags'
|
||||
import task, { DOMAIN_TASK, createSequence, migrateDefaultStatusesBase } from '@hcengineering/model-task'
|
||||
import task, { createSequence, DOMAIN_TASK, migrateDefaultStatusesBase } from '@hcengineering/model-task'
|
||||
import { recruitId, type Applicant } from '@hcengineering/recruit'
|
||||
|
||||
import { DOMAIN_CALENDAR } from '@hcengineering/model-calendar'
|
||||
@ -194,10 +202,16 @@ async function createDefaults (client: MigrationUpgradeClient, tx: TxOperations)
|
||||
},
|
||||
recruit.category.Other
|
||||
)
|
||||
const ops = tx.apply()
|
||||
|
||||
for (const c of getCategories()) {
|
||||
const cats = getCategories().filter((it, idx, arr) => arr.findIndex((qt) => qt.id === it.id) === idx)
|
||||
|
||||
const existingCategories = toIdMap(
|
||||
await client.findAll<Doc>(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
|
||||
)
|
||||
for (const c of cats) {
|
||||
await createOrUpdate(
|
||||
tx,
|
||||
ops,
|
||||
tags.class.TagCategory,
|
||||
core.space.Workspace,
|
||||
{
|
||||
@ -207,9 +221,11 @@ async function createDefaults (client: MigrationUpgradeClient, tx: TxOperations)
|
||||
tags: c.skills,
|
||||
default: false
|
||||
},
|
||||
(recruit.category.Category + '.' + c.id) as Ref<TagCategory>
|
||||
(recruit.category.Category + '.' + c.id) as Ref<TagCategory>,
|
||||
existingCategories
|
||||
)
|
||||
}
|
||||
await ops.commit()
|
||||
|
||||
await createSequence(tx, recruit.class.Review)
|
||||
await createSequence(tx, recruit.class.Opinion)
|
||||
|
@ -44,14 +44,13 @@ export function createModel (builder: Builder): void {
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverActivity.trigger.ActivityMessagesHandler,
|
||||
txMatch: {
|
||||
'tx.objectClass': { $nin: [activity.class.ActivityMessage, notification.class.DocNotifyContext] }
|
||||
},
|
||||
arrays: true,
|
||||
isAsync: true
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverActivity.trigger.OnDocRemoved
|
||||
trigger: serverActivity.trigger.OnDocRemoved,
|
||||
arrays: true
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
|
@ -37,6 +37,7 @@ export function createModel (builder: Builder): void {
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverAiBot.trigger.OnMessageSend,
|
||||
arrays: true,
|
||||
isAsync: true
|
||||
})
|
||||
|
||||
|
@ -23,6 +23,7 @@ export { serverCollaborationId } from '@hcengineering/server-collaboration'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverCollaboration.trigger.OnDelete
|
||||
trigger: serverCollaboration.trigger.OnDelete,
|
||||
arrays: true
|
||||
})
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import { type Builder, Mixin, Model } from '@hcengineering/model'
|
||||
|
||||
import contact from '@hcengineering/contact'
|
||||
import core, { type Ref } from '@hcengineering/core'
|
||||
import { TClass, TDoc } from '@hcengineering/model-core'
|
||||
import { TNotificationType } from '@hcengineering/model-notification'
|
||||
@ -33,7 +34,6 @@ import serverNotification, {
|
||||
type TypeMatch,
|
||||
type TypeMatchFunc
|
||||
} from '@hcengineering/server-notification'
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
export { serverNotificationId } from '@hcengineering/server-notification'
|
||||
|
||||
@ -89,7 +89,8 @@ export function createModel (builder: Builder): void {
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverNotification.trigger.OnDocRemove
|
||||
trigger: serverNotification.trigger.OnDocRemove,
|
||||
arrays: true
|
||||
})
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
|
@ -25,7 +25,8 @@ export { serverRequestId } from '@hcengineering/server-request'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverRequest.trigger.OnRequest
|
||||
trigger: serverRequest.trigger.OnRequest,
|
||||
arrays: true
|
||||
})
|
||||
|
||||
builder.mixin(request.class.Request, core.class.Class, serverNotification.mixin.TextPresenter, {
|
||||
|
@ -23,7 +23,8 @@ export { serverTagsId } from '@hcengineering/server-tags'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverTags.trigger.onTagReference
|
||||
trigger: serverTags.trigger.onTagReference,
|
||||
arrays: true
|
||||
})
|
||||
|
||||
builder.mixin<Class<Doc>, ObjectDDParticipant>(
|
||||
|
@ -22,6 +22,7 @@ export { serverTaskId } from '@hcengineering/server-task'
|
||||
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverTask.trigger.OnStateUpdate
|
||||
trigger: serverTask.trigger.OnStateUpdate,
|
||||
arrays: true
|
||||
})
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ export function createModel (builder: Builder): void {
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverTime.trigger.OnTask,
|
||||
arrays: true,
|
||||
isAsync: true
|
||||
})
|
||||
|
||||
|
@ -15,10 +15,10 @@
|
||||
|
||||
import core, { type Doc } from '@hcengineering/core'
|
||||
import { type Builder, Mixin } from '@hcengineering/model'
|
||||
import serverCore, { type TriggerControl } from '@hcengineering/server-core'
|
||||
import serverView, { type ServerLinkIdProvider } from '@hcengineering/server-view'
|
||||
import { TClass } from '@hcengineering/model-core'
|
||||
import { type Resource } from '@hcengineering/platform'
|
||||
import serverCore, { type TriggerControl } from '@hcengineering/server-core'
|
||||
import serverView, { type ServerLinkIdProvider } from '@hcengineering/server-view'
|
||||
|
||||
export { serverViewId } from '@hcengineering/server-view'
|
||||
|
||||
@ -31,6 +31,10 @@ export function createModel (builder: Builder): void {
|
||||
builder.createModel(TServerLinkIdProvider)
|
||||
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverView.trigger.OnCustomAttributeRemove
|
||||
trigger: serverView.trigger.OnCustomAttributeRemove,
|
||||
txMatch: {
|
||||
_class: core.class.TxRemoveDoc,
|
||||
objectClass: core.class.Attribute
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Class, Data, Doc, DocumentUpdate, Ref, Space, TxOperations } from '@hcengineering/core'
|
||||
import { Class, Data, Doc, DocumentUpdate, Ref, Space, TxOperations, type IdMap } from '@hcengineering/core'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
function toUndef (value: any): any {
|
||||
@ -40,9 +40,10 @@ export async function createOrUpdate<T extends Doc> (
|
||||
_class: Ref<Class<T>>,
|
||||
space: Ref<Space>,
|
||||
data: Data<T>,
|
||||
_id: Ref<T>
|
||||
_id: Ref<T>,
|
||||
cache?: IdMap<Doc>
|
||||
): Promise<void> {
|
||||
const existingDoc = await client.findOne<Doc>(_class, { _id })
|
||||
const existingDoc = cache !== undefined ? cache.get(_id) : await client.findOne<Doc>(_class, { _id })
|
||||
if (existingDoc !== undefined) {
|
||||
const { _class: _oldClass, _id, space: _oldSpace, modifiedBy, modifiedOn, ...oldData } = existingDoc
|
||||
if (modifiedBy === client.txFactory.account) {
|
||||
|
@ -3,11 +3,11 @@
|
||||
//
|
||||
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service'
|
||||
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
|
||||
import { startFront } from '@hcengineering/front/src/starter'
|
||||
import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service'
|
||||
import { join } from 'path'
|
||||
import { initStatisticsContext } from '@hcengineering/server-core'
|
||||
import { join } from 'path'
|
||||
|
||||
configureAnalytics(process.env.SENTRY_DSN, {})
|
||||
Analytics.setTag('application', 'front')
|
||||
@ -45,5 +45,5 @@ startFront(metricsContext, {
|
||||
ANALYTICS_COLLECTOR_URL: process.env.ANALYTICS_COLLECTOR_URL,
|
||||
AI_URL: process.env.AI_URL,
|
||||
TELEGRAM_BOT_URL: process.env.TELEGRAM_BOT_URL,
|
||||
STATS_URL: process.env.STATS_URL
|
||||
STATS_URL: process.env.STATS_API ?? process.env.STATS_URL
|
||||
})
|
||||
|
@ -20,7 +20,7 @@ import { initStatisticsContext, type StorageConfiguration } from '@hcengineering
|
||||
import { join } from 'path'
|
||||
|
||||
import { createElasticAdapter } from '@hcengineering/elastic'
|
||||
import { createRekoniAdapter, createYDocAdapter, type FulltextDBConfiguration } from '@hcengineering/server-indexer'
|
||||
import { createRekoniAdapter, type FulltextDBConfiguration } from '@hcengineering/server-indexer'
|
||||
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
|
||||
import { readFileSync } from 'fs'
|
||||
import { startIndexer } from './server'
|
||||
@ -80,11 +80,6 @@ const config: FulltextDBConfiguration = {
|
||||
factory: createRekoniAdapter,
|
||||
contentType: '*',
|
||||
url: rekoniUrl
|
||||
},
|
||||
YDoc: {
|
||||
factory: createYDocAdapter,
|
||||
contentType: 'application/ydoc',
|
||||
url: ''
|
||||
}
|
||||
},
|
||||
defaultContentAdapter: 'Rekoni'
|
||||
|
@ -8,8 +8,7 @@
|
||||
"template": "@hcengineering/node-package",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"start": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret OPERATION_PROFILING=false MODEL_JSON=../../models/all/bundle/model.json node --inspect bundle/bundle.js",
|
||||
"start-u": "rush bundle --to @hcengineering/pod-server && ./bundle/ && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret MODEL_JSON=../../models/all/bundle/model.json node bundle/bundle.js",
|
||||
"start": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 DB_URL=mongodb://localhost:27017 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret OPERATION_PROFILING=false MODEL_JSON=../../models/all/bundle/model.json STATS_URL=http://host.docker.internal:4900 node --inspect bundle/bundle.js",
|
||||
"start-flame": "rush bundle --to @hcengineering/pod-server && cross-env NODE_ENV=production MODEL_VERSION=$(node ../../common/scripts/show_version.js) ACCOUNTS_URL=http://localhost:3000 REKONI_URL=http://localhost:4004 MONGO_URL=mongodb://localhost:27017 FRONT_URL=http://localhost:8087 UPLOAD_URL=/upload MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin METRICS_CONSOLE=true SERVER_SECRET=secret MODEL_JSON=../../models/all/bundle/model.json clinic flame --dest ./out -- node --nolazy -r ts-node/register --enable-source-maps src/__start.ts",
|
||||
"build": "compile",
|
||||
"_phase:bundle": "rushx bundle",
|
||||
|
@ -387,69 +387,72 @@ export async function generateDocUpdateMessages (
|
||||
return res
|
||||
}
|
||||
|
||||
async function ActivityMessagesHandler (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
if (
|
||||
control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage) ||
|
||||
control.hierarchy.isDerived(tx.objectClass, notification.class.DocNotifyContext) ||
|
||||
control.hierarchy.isDerived(tx.objectClass, notification.class.ActivityInboxNotification) ||
|
||||
control.hierarchy.isDerived(tx.objectClass, notification.class.BrowserNotification)
|
||||
) {
|
||||
return []
|
||||
}
|
||||
async function ActivityMessagesHandler (_txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
|
||||
const ltxes = _txes.filter(
|
||||
(it) =>
|
||||
!(
|
||||
control.hierarchy.isDerived(it.objectClass, activity.class.ActivityMessage) ||
|
||||
control.hierarchy.isDerived(it.objectClass, notification.class.DocNotifyContext) ||
|
||||
control.hierarchy.isDerived(it.objectClass, notification.class.ActivityInboxNotification) ||
|
||||
control.hierarchy.isDerived(it.objectClass, notification.class.BrowserNotification)
|
||||
)
|
||||
)
|
||||
|
||||
const cache: DocObjectCache = control.contextCache.get('ActivityMessagesHandler') ?? {
|
||||
docs: new Map(),
|
||||
transactions: new Map()
|
||||
}
|
||||
control.contextCache.set('ActivityMessagesHandler', cache)
|
||||
const result: Tx[] = []
|
||||
for (const tx of ltxes) {
|
||||
const txes =
|
||||
tx.space === core.space.DerivedTx
|
||||
? []
|
||||
: await control.ctx.with('generateDocUpdateMessages', {}, (ctx) =>
|
||||
generateDocUpdateMessages(ctx, tx, control, [], undefined, cache)
|
||||
)
|
||||
|
||||
const txes =
|
||||
tx.space === core.space.DerivedTx
|
||||
? []
|
||||
: await control.ctx.with(
|
||||
'generateDocUpdateMessages',
|
||||
{},
|
||||
async (ctx) => await generateDocUpdateMessages(ctx, tx, control, [], undefined, cache)
|
||||
)
|
||||
const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc<DocUpdateMessage>))
|
||||
|
||||
const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc<DocUpdateMessage>))
|
||||
|
||||
const notificationTxes = await control.ctx.with(
|
||||
'createCollaboratorNotifications',
|
||||
{},
|
||||
async (ctx) =>
|
||||
await createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map<Ref<Doc>, Doc>)
|
||||
)
|
||||
|
||||
const result = [...txes, ...notificationTxes]
|
||||
const notificationTxes = await control.ctx.with('createCollaboratorNotifications', {}, (ctx) =>
|
||||
createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map<Ref<Doc>, Doc>)
|
||||
)
|
||||
|
||||
result.push(...txes, ...notificationTxes)
|
||||
}
|
||||
if (result.length > 0) {
|
||||
await control.apply(control.ctx, result)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
async function OnDocRemoved (originTx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const tx = TxProcessor.extractTx(originTx) as TxCUD<Doc>
|
||||
async function OnDocRemoved (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
for (const originTx of txes) {
|
||||
const tx = TxProcessor.extractTx(originTx) as TxCUD<Doc>
|
||||
|
||||
if (tx._class !== core.class.TxRemoveDoc) {
|
||||
return []
|
||||
if (tx._class !== core.class.TxRemoveDoc) {
|
||||
continue
|
||||
}
|
||||
|
||||
const activityDocMixin = control.hierarchy.classHierarchyMixin(tx.objectClass, activity.mixin.ActivityDoc)
|
||||
|
||||
if (activityDocMixin === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
const messages = await control.findAll(
|
||||
control.ctx,
|
||||
activity.class.ActivityMessage,
|
||||
{ attachedTo: tx.objectId },
|
||||
{ projection: { _id: 1, _class: 1, space: 1 } }
|
||||
)
|
||||
|
||||
result.push(
|
||||
...messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id))
|
||||
)
|
||||
}
|
||||
|
||||
const activityDocMixin = control.hierarchy.classHierarchyMixin(tx.objectClass, activity.mixin.ActivityDoc)
|
||||
|
||||
if (activityDocMixin === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const messages = await control.findAll(
|
||||
control.ctx,
|
||||
activity.class.ActivityMessage,
|
||||
{ attachedTo: tx.objectId },
|
||||
{ projection: { _id: 1, _class: 1, space: 1 } }
|
||||
)
|
||||
|
||||
return messages.map((message) => control.txFactory.createTxRemoveDoc(message._class, message.space, message._id))
|
||||
return result
|
||||
}
|
||||
|
||||
async function ReactionNotificationContentProvider (
|
||||
|
@ -13,6 +13,15 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import aiBot, {
|
||||
aiBotAccountEmail,
|
||||
AIEventType,
|
||||
AIMessageEventRequest,
|
||||
AITransferEventRequest
|
||||
} from '@hcengineering/ai-bot'
|
||||
import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector'
|
||||
import chunter, { ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter'
|
||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||
import core, {
|
||||
AccountRole,
|
||||
AttachedDoc,
|
||||
@ -27,17 +36,8 @@ import core, {
|
||||
TxUpdateDoc,
|
||||
UserStatus
|
||||
} from '@hcengineering/core'
|
||||
import { TriggerControl } from '@hcengineering/server-core'
|
||||
import chunter, { ChatMessage, DirectMessage, ThreadMessage } from '@hcengineering/chunter'
|
||||
import aiBot, {
|
||||
aiBotAccountEmail,
|
||||
AIEventType,
|
||||
AIMessageEventRequest,
|
||||
AITransferEventRequest
|
||||
} from '@hcengineering/ai-bot'
|
||||
import contact, { PersonAccount } from '@hcengineering/contact'
|
||||
import { ActivityInboxNotification, MentionInboxNotification } from '@hcengineering/notification'
|
||||
import analyticsCollector, { OnboardingChannel } from '@hcengineering/analytics-collector'
|
||||
import { TriggerControl } from '@hcengineering/server-core'
|
||||
|
||||
import { createAccountRequest, getSupportWorkspaceId, sendAIEvents } from './utils'
|
||||
|
||||
@ -241,34 +241,38 @@ async function onSupportWorkspaceMessage (control: TriggerControl, message: Chat
|
||||
}
|
||||
|
||||
export async function OnMessageSend (
|
||||
originTx: TxCollectionCUD<Doc, AttachedDoc>,
|
||||
originTxs: TxCollectionCUD<Doc, AttachedDoc>[],
|
||||
control: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
const { hierarchy } = control
|
||||
const tx = TxProcessor.extractTx(originTx) as TxCreateDoc<ChatMessage>
|
||||
if (tx._class !== core.class.TxCreateDoc || !hierarchy.isDerived(tx.objectClass, chunter.class.ChatMessage)) {
|
||||
const txes = originTxs
|
||||
.map((it) => TxProcessor.extractTx(it) as TxCreateDoc<ChatMessage>)
|
||||
.filter(
|
||||
(it) =>
|
||||
it._class === core.class.TxCreateDoc &&
|
||||
hierarchy.isDerived(it.objectClass, chunter.class.ChatMessage) &&
|
||||
!(it.modifiedBy === aiBot.account.AIBot || it.modifiedBy === core.account.System)
|
||||
)
|
||||
if (txes.length === 0) {
|
||||
return []
|
||||
}
|
||||
for (const tx of txes) {
|
||||
const isThread = hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage)
|
||||
const message = TxProcessor.createDoc2Doc(tx)
|
||||
|
||||
if (tx.modifiedBy === aiBot.account.AIBot || tx.modifiedBy === core.account.System) {
|
||||
return []
|
||||
}
|
||||
const docClass = isThread ? (message as ThreadMessage).objectClass : message.attachedToClass
|
||||
|
||||
const isThread = hierarchy.isDerived(tx.objectClass, chunter.class.ThreadMessage)
|
||||
const message = TxProcessor.createDoc2Doc(tx)
|
||||
if (!hierarchy.isDerived(docClass, chunter.class.ChunterSpace)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const docClass = isThread ? (message as ThreadMessage).objectClass : message.attachedToClass
|
||||
if (docClass === chunter.class.DirectMessage) {
|
||||
await onBotDirectMessageSend(control, message)
|
||||
}
|
||||
|
||||
if (!hierarchy.isDerived(docClass, chunter.class.ChunterSpace)) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (docClass === chunter.class.DirectMessage) {
|
||||
await onBotDirectMessageSend(control, message)
|
||||
}
|
||||
|
||||
if (docClass === analyticsCollector.class.OnboardingChannel) {
|
||||
await onSupportWorkspaceMessage(control, message)
|
||||
if (docClass === analyticsCollector.class.OnboardingChannel) {
|
||||
await onSupportWorkspaceMessage(control, message)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
|
@ -35,7 +35,8 @@ import core, {
|
||||
TxProcessor,
|
||||
TxRemoveDoc,
|
||||
TxUpdateDoc,
|
||||
UserStatus
|
||||
UserStatus,
|
||||
type MeasureContext
|
||||
} from '@hcengineering/core'
|
||||
import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification'
|
||||
import { getMetadata, IntlString, translate } from '@hcengineering/platform'
|
||||
@ -151,7 +152,7 @@ async function OnThreadMessageCreated (originTx: TxCUD<Doc>, control: TriggerCon
|
||||
return [lastReplyTx, repliedPersonTx]
|
||||
}
|
||||
|
||||
async function OnChatMessageCreated (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
async function OnChatMessageCreated (ctx: MeasureContext, tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const hierarchy = control.hierarchy
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxCreateDoc<ChatMessage>
|
||||
|
||||
@ -163,9 +164,7 @@ async function OnChatMessageCreated (tx: TxCUD<Doc>, control: TriggerControl): P
|
||||
return []
|
||||
}
|
||||
|
||||
const targetDoc = (
|
||||
await control.findAll(control.ctx, message.attachedToClass, { _id: message.attachedTo }, { limit: 1 })
|
||||
)[0]
|
||||
const targetDoc = (await control.findAll(ctx, message.attachedToClass, { _id: message.attachedTo }, { limit: 1 }))[0]
|
||||
if (targetDoc === undefined) {
|
||||
return []
|
||||
}
|
||||
@ -190,7 +189,7 @@ async function OnChatMessageCreated (tx: TxCUD<Doc>, control: TriggerControl): P
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const collaborators = await getDocCollaborators(control.ctx, targetDoc, mixin, control)
|
||||
const collaborators = await getDocCollaborators(ctx, targetDoc, mixin, control)
|
||||
if (!collaborators.includes(message.modifiedBy)) {
|
||||
collaborators.push(message.modifiedBy)
|
||||
}
|
||||
@ -272,33 +271,19 @@ export async function ChunterTrigger (tx: TxCUD<Doc>, control: TriggerControl):
|
||||
actualTx._class === core.class.TxCreateDoc &&
|
||||
control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage)
|
||||
) {
|
||||
res.push(
|
||||
...(await control.ctx.with(
|
||||
'OnThreadMessageCreated',
|
||||
{},
|
||||
async (ctx) => await OnThreadMessageCreated(tx, control)
|
||||
))
|
||||
)
|
||||
res.push(...(await control.ctx.with('OnThreadMessageCreated', {}, (ctx) => OnThreadMessageCreated(tx, control))))
|
||||
}
|
||||
if (
|
||||
actualTx._class === core.class.TxRemoveDoc &&
|
||||
control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ThreadMessage)
|
||||
) {
|
||||
res.push(
|
||||
...(await control.ctx.with(
|
||||
'OnThreadMessageDeleted',
|
||||
{},
|
||||
async (ctx) => await OnThreadMessageDeleted(tx, control)
|
||||
))
|
||||
)
|
||||
res.push(...(await control.ctx.with('OnThreadMessageDeleted', {}, (ctx) => OnThreadMessageDeleted(tx, control))))
|
||||
}
|
||||
if (
|
||||
actualTx._class === core.class.TxCreateDoc &&
|
||||
control.hierarchy.isDerived(actualTx.objectClass, chunter.class.ChatMessage)
|
||||
) {
|
||||
res.push(
|
||||
...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control)))
|
||||
)
|
||||
res.push(...(await control.ctx.with('OnChatMessageCreated', {}, (ctx) => OnChatMessageCreated(ctx, tx, control))))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
@ -13,48 +13,46 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { removeCollaborativeDoc } from '@hcengineering/collaboration'
|
||||
import type { CollaborativeDoc, Doc, Tx, TxRemoveDoc } from '@hcengineering/core'
|
||||
import core, { TxProcessor } from '@hcengineering/core'
|
||||
import { removeCollaborativeDoc } from '@hcengineering/collaboration'
|
||||
import { type TriggerControl } from '@hcengineering/server-core'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnDelete (
|
||||
tx: Tx,
|
||||
txes: Tx[],
|
||||
{ hierarchy, storageAdapter, workspace, removedMap, ctx }: TriggerControl
|
||||
): Promise<Tx[]> {
|
||||
const rmTx = TxProcessor.extractTx(tx) as TxRemoveDoc<Doc>
|
||||
const ltxes = txes
|
||||
.map((it) => TxProcessor.extractTx(it))
|
||||
.filter((it) => it._class === core.class.TxRemoveDoc) as TxRemoveDoc<Doc>[]
|
||||
for (const rmTx of ltxes) {
|
||||
// Obtain document being deleted
|
||||
const doc = removedMap.get(rmTx.objectId)
|
||||
if (doc === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (rmTx._class !== core.class.TxRemoveDoc) {
|
||||
return []
|
||||
}
|
||||
// Ids of files to delete from storage
|
||||
const toDelete: CollaborativeDoc[] = []
|
||||
|
||||
// Obtain document being deleted
|
||||
const doc = removedMap.get(rmTx.objectId)
|
||||
if (doc === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Ids of files to delete from storage
|
||||
const toDelete: CollaborativeDoc[] = []
|
||||
|
||||
const attributes = hierarchy.getAllAttributes(rmTx.objectClass)
|
||||
for (const attribute of attributes.values()) {
|
||||
if (hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeDoc)) {
|
||||
const value = (doc as any)[attribute.name] as CollaborativeDoc
|
||||
if (value !== undefined) {
|
||||
toDelete.push(value)
|
||||
const attributes = hierarchy.getAllAttributes(rmTx.objectClass)
|
||||
for (const attribute of attributes.values()) {
|
||||
if (hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeDoc)) {
|
||||
const value = (doc as any)[attribute.name] as CollaborativeDoc
|
||||
if (value !== undefined) {
|
||||
toDelete.push(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This is not accurate way to delete collaborative document
|
||||
// Even though we are deleting it here, the document can be currently in use by someone else
|
||||
// and when editing session ends, the collborator service will recreate the document again
|
||||
await removeCollaborativeDoc(storageAdapter, workspace, toDelete, ctx)
|
||||
}
|
||||
|
||||
// TODO This is not accurate way to delete collaborative document
|
||||
// Even though we are deleting it here, the document can be currently in use by someone else
|
||||
// and when editing session ends, the collborator service will recreate the document again
|
||||
await removeCollaborativeDoc(storageAdapter, workspace, toDelete, ctx)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
|
@ -1001,9 +1001,10 @@ export async function createCollabDocInfo (
|
||||
}
|
||||
|
||||
const settings = await getNotificationProviderControl(ctx, control)
|
||||
const subscriptions = await control.findAll(ctx, notification.class.PushSubscription, {
|
||||
user: { $in: Array.from(targets) }
|
||||
})
|
||||
const subscriptions = (await control.queryFind(ctx, notification.class.PushSubscription, {})).filter((it) =>
|
||||
targets.has(it.user as Ref<PersonAccount>)
|
||||
)
|
||||
|
||||
for (const target of targets) {
|
||||
const info: ReceiverInfo | undefined = toReceiverInfo(control.hierarchy, usersInfo.get(target))
|
||||
|
||||
@ -1938,36 +1939,34 @@ async function OnEmployeeDeactivate (tx: TxCUD<Doc>, control: TriggerControl): P
|
||||
return res
|
||||
}
|
||||
|
||||
async function OnDocRemove (originTx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const tx = TxProcessor.extractTx(originTx) as TxRemoveDoc<Doc>
|
||||
|
||||
if (tx._class !== core.class.TxRemoveDoc) return []
|
||||
|
||||
async function OnDocRemove (txes: TxCUD<Doc>[], control: TriggerControl): Promise<Tx[]> {
|
||||
const ltxes = txes
|
||||
.map((it) => TxProcessor.extractTx(it))
|
||||
.filter((it) => it._class === core.class.TxRemoveDoc) as TxRemoveDoc<Doc>[]
|
||||
const res: Tx[] = []
|
||||
for (const tx of ltxes) {
|
||||
if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) {
|
||||
const message = control.removedMap.get(tx.objectId) as ActivityMessage | undefined
|
||||
|
||||
if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) {
|
||||
const message = control.removedMap.get(tx.objectId) as ActivityMessage | undefined
|
||||
|
||||
if (message !== undefined) {
|
||||
const txes = await OnActivityMessageRemove(message, control)
|
||||
res.push(...txes)
|
||||
}
|
||||
} else if (control.hierarchy.isDerived(tx.objectClass, notification.class.DocNotifyContext)) {
|
||||
const contextsCache: ContextsCache | undefined = control.cache.get(ContextsCacheKey)
|
||||
if (contextsCache !== undefined) {
|
||||
for (const [key, value] of contextsCache.contexts.entries()) {
|
||||
if (value === tx.objectId) {
|
||||
contextsCache.contexts.delete(key)
|
||||
if (message !== undefined) {
|
||||
const txes = await OnActivityMessageRemove(message, control)
|
||||
res.push(...txes)
|
||||
}
|
||||
} else if (control.hierarchy.isDerived(tx.objectClass, notification.class.DocNotifyContext)) {
|
||||
const contextsCache: ContextsCache | undefined = control.cache.get(ContextsCacheKey)
|
||||
if (contextsCache !== undefined) {
|
||||
for (const [key, value] of contextsCache.contexts.entries()) {
|
||||
if (value === tx.objectId) {
|
||||
contextsCache.contexts.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.push(...(await removeContextNotifications(control, [tx.objectId as Ref<DocNotifyContext>])))
|
||||
}
|
||||
|
||||
return await removeContextNotifications(control, [tx.objectId as Ref<DocNotifyContext>])
|
||||
res.push(...(await removeCollaboratorDoc(tx, control)))
|
||||
}
|
||||
|
||||
const txes = await removeCollaboratorDoc(tx, control)
|
||||
|
||||
res.push(...txes)
|
||||
return res
|
||||
}
|
||||
|
||||
|
@ -13,54 +13,50 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { DocUpdateMessage } from '@hcengineering/activity'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import core, {
|
||||
Doc,
|
||||
Ref,
|
||||
Tx,
|
||||
TxCUD,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
TxUpdateDoc,
|
||||
TxProcessor,
|
||||
Ref,
|
||||
TxUpdateDoc,
|
||||
type MeasureContext
|
||||
} from '@hcengineering/core'
|
||||
import request, { Request, RequestStatus } from '@hcengineering/request'
|
||||
import { getResource, translate } from '@hcengineering/platform'
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
import { pushDocUpdateMessages } from '@hcengineering/server-activity-resources'
|
||||
import { DocUpdateMessage } from '@hcengineering/activity'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { getResource, translate } from '@hcengineering/platform'
|
||||
import request, { Request, RequestStatus } from '@hcengineering/request'
|
||||
import { pushDocUpdateMessages } from '@hcengineering/server-activity-resources'
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
import {
|
||||
getNotificationTxes,
|
||||
getCollaborators,
|
||||
getNotificationProviderControl,
|
||||
getNotificationTxes,
|
||||
getTextPresenter,
|
||||
getUsersInfo,
|
||||
toReceiverInfo,
|
||||
getNotificationProviderControl
|
||||
toReceiverInfo
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnRequest (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
if (tx._class !== core.class.TxCollectionCUD) {
|
||||
return []
|
||||
}
|
||||
|
||||
export async function OnRequest (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
||||
const hierarchy = control.hierarchy
|
||||
const ptx = tx as TxCollectionCUD<Doc, Request>
|
||||
|
||||
if (!hierarchy.isDerived(ptx.tx.objectClass, request.class.Request)) {
|
||||
return []
|
||||
}
|
||||
const ltxes = (
|
||||
txes.filter((it) => it._class === core.class.TxCollectionCUD) as TxCollectionCUD<Doc, Request>[]
|
||||
).filter((it) => hierarchy.isDerived(it.tx.objectClass, request.class.Request))
|
||||
|
||||
let res: Tx[] = []
|
||||
|
||||
res = res.concat(await getRequestNotificationTx(control.ctx, ptx, control))
|
||||
for (const ptx of ltxes) {
|
||||
res = res.concat(await getRequestNotificationTx(control.ctx, ptx, control))
|
||||
|
||||
if (ptx.tx._class === core.class.TxUpdateDoc) {
|
||||
res = res.concat(await OnRequestUpdate(ptx, control))
|
||||
if (ptx.tx._class === core.class.TxUpdateDoc) {
|
||||
res = res.concat(await OnRequestUpdate(ptx, control))
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
@ -168,9 +164,10 @@ async function getRequestNotificationTx (
|
||||
}
|
||||
|
||||
const notificationControl = await getNotificationProviderControl(ctx, control)
|
||||
const subscriptions = await control.findAll(control.ctx, notification.class.PushSubscription, {
|
||||
user: { $in: collaborators }
|
||||
})
|
||||
const collaboratorsSet = new Set(collaborators)
|
||||
const subscriptions = (await control.queryFind(control.ctx, notification.class.PushSubscription, {})).filter((it) =>
|
||||
collaboratorsSet.has(it.user)
|
||||
)
|
||||
for (const target of collaborators) {
|
||||
const targetInfo = toReceiverInfo(control.hierarchy, usersInfo.get(target))
|
||||
if (targetInfo === undefined) continue
|
||||
|
@ -49,32 +49,37 @@ export async function TagElementRemove (
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function onTagReference (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
const isCreate = control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)
|
||||
const isRemove = control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)
|
||||
if (!isCreate && !isRemove) return []
|
||||
if (!control.hierarchy.isDerived((actualTx as TxCUD<Doc>).objectClass, tags.class.TagReference)) return []
|
||||
if (isCreate) {
|
||||
const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<TagReference>)
|
||||
const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, {
|
||||
$inc: { refCount: 1 }
|
||||
})
|
||||
return [res]
|
||||
}
|
||||
if (isRemove) {
|
||||
const ctx = actualTx as TxRemoveDoc<TagReference>
|
||||
const doc = control.removedMap.get(ctx.objectId) as TagReference
|
||||
if (doc !== undefined) {
|
||||
if (!control.removedMap.has(doc.tag)) {
|
||||
const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, {
|
||||
$inc: { refCount: -1 }
|
||||
export async function onTagReference (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
for (const tx of txes) {
|
||||
const actualTx = TxProcessor.extractTx(tx)
|
||||
const isCreate = control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)
|
||||
const isRemove = control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)
|
||||
if (!isCreate && !isRemove) return []
|
||||
if (!control.hierarchy.isDerived((actualTx as TxCUD<Doc>).objectClass, tags.class.TagReference)) return []
|
||||
if (isCreate) {
|
||||
const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<TagReference>)
|
||||
result.push(
|
||||
control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, {
|
||||
$inc: { refCount: 1 }
|
||||
})
|
||||
return [res]
|
||||
)
|
||||
}
|
||||
if (isRemove) {
|
||||
const ctx = actualTx as TxRemoveDoc<TagReference>
|
||||
const doc = control.removedMap.get(ctx.objectId) as TagReference
|
||||
if (doc !== undefined) {
|
||||
if (!control.removedMap.has(doc.tag)) {
|
||||
result.push(
|
||||
control.txFactory.createTxUpdateDoc(tags.class.TagElement, core.space.Workspace, doc.tag, {
|
||||
$inc: { refCount: -1 }
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
return result
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
|
@ -20,31 +20,34 @@ import task, { Task } from '@hcengineering/task'
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnStateUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||
if (!control.hierarchy.isDerived(actualTx.objectClass, task.class.Task)) return []
|
||||
if (actualTx._class === core.class.TxCreateDoc) {
|
||||
const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<Task>)
|
||||
const status = (await control.modelDb.findAll(core.class.Status, { _id: doc.status }))[0]
|
||||
if (status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won) {
|
||||
return [control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { isDone: true })]
|
||||
}
|
||||
} else if (actualTx._class === core.class.TxUpdateDoc) {
|
||||
const updateTx = actualTx as TxUpdateDoc<Task>
|
||||
if (updateTx.operations.status !== undefined) {
|
||||
const status = (await control.modelDb.findAll(core.class.Status, { _id: updateTx.operations.status }))[0]
|
||||
export async function OnStateUpdate (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
for (const tx of txes) {
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||
if (!control.hierarchy.isDerived(actualTx.objectClass, task.class.Task)) return []
|
||||
if (actualTx._class === core.class.TxCreateDoc) {
|
||||
const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<Task>)
|
||||
const status = control.modelDb.findAllSync(core.class.Status, { _id: doc.status })[0]
|
||||
if (status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won) {
|
||||
return [
|
||||
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
|
||||
isDone: true
|
||||
})
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
|
||||
isDone: false
|
||||
})
|
||||
]
|
||||
result.push(control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { isDone: true }))
|
||||
}
|
||||
} else if (actualTx._class === core.class.TxUpdateDoc) {
|
||||
const updateTx = actualTx as TxUpdateDoc<Task>
|
||||
if (updateTx.operations.status !== undefined) {
|
||||
const status = control.modelDb.findAllSync(core.class.Status, { _id: updateTx.operations.status })[0]
|
||||
if (status?.category === task.statusCategory.Lost || status?.category === task.statusCategory.Won) {
|
||||
result.push(
|
||||
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
|
||||
isDone: true
|
||||
})
|
||||
)
|
||||
} else {
|
||||
result.push(
|
||||
control.txFactory.createTxUpdateDoc(updateTx.objectClass, updateTx.objectSpace, updateTx.objectId, {
|
||||
isDone: false
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import contact, { Channel, ChannelProvider, Contact, Employee, formatName, PersonAccount } from '@hcengineering/contact'
|
||||
import {
|
||||
Account,
|
||||
@ -29,27 +31,25 @@ import {
|
||||
TxCreateDoc,
|
||||
TxProcessor
|
||||
} from '@hcengineering/core'
|
||||
import { TriggerControl } from '@hcengineering/server-core'
|
||||
import notification, {
|
||||
BaseNotificationType,
|
||||
InboxNotification,
|
||||
MentionInboxNotification,
|
||||
NotificationType
|
||||
} from '@hcengineering/notification'
|
||||
import telegram, { TelegramMessage, TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||
import setting, { Integration } from '@hcengineering/setting'
|
||||
import { NotificationProviderFunc, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
|
||||
import { getMetadata, getResource, translate } from '@hcengineering/platform'
|
||||
import serverTelegram from '@hcengineering/server-telegram'
|
||||
import { TriggerControl } from '@hcengineering/server-core'
|
||||
import { NotificationProviderFunc, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification'
|
||||
import {
|
||||
getTranslatedNotificationContent,
|
||||
getNotificationLink,
|
||||
getTextPresenter,
|
||||
getNotificationLink
|
||||
getTranslatedNotificationContent
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
import serverTelegram from '@hcengineering/server-telegram'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import setting, { Integration } from '@hcengineering/setting'
|
||||
import telegram, { TelegramMessage, TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||
import { markupToHTML } from '@hcengineering/text'
|
||||
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -245,6 +245,7 @@ function hasAttachments (doc: ActivityMessage | undefined, hierarchy: Hierarchy)
|
||||
return false
|
||||
}
|
||||
|
||||
const telegramNotificationKey = 'telegram.notification.reported'
|
||||
const SendTelegramNotifications: NotificationProviderFunc = async (
|
||||
control: TriggerControl,
|
||||
types: BaseNotificationType[],
|
||||
@ -261,7 +262,11 @@ const SendTelegramNotifications: NotificationProviderFunc = async (
|
||||
const botUrl = getMetadata(serverTelegram.metadata.BotUrl)
|
||||
|
||||
if (botUrl === undefined || botUrl === '') {
|
||||
control.ctx.error('Please provide telegram bot service url to enable telegram notifications.')
|
||||
const reported = control.cache.get(telegramNotificationKey)
|
||||
if (reported === undefined) {
|
||||
control.ctx.error('Please provide telegram bot service url to enable telegram notifications.')
|
||||
control.cache.set(telegramNotificationKey, true)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
|
@ -50,23 +50,26 @@ import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengine
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnTask (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||
const mixin = control.hierarchy.classHierarchyMixin<Class<Doc>, ToDoFactory>(
|
||||
actualTx.objectClass,
|
||||
serverTime.mixin.ToDoFactory
|
||||
)
|
||||
if (mixin !== undefined) {
|
||||
if (actualTx._class !== core.class.TxRemoveDoc) {
|
||||
const factory = await getResource(mixin.factory)
|
||||
return await factory(tx, control)
|
||||
} else {
|
||||
const todos = await control.findAll(control.ctx, time.class.ToDo, { attachedTo: actualTx.objectId })
|
||||
return todos.map((p) => control.txFactory.createTxRemoveDoc(p._class, p.space, p._id))
|
||||
export async function OnTask (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
for (const tx of txes) {
|
||||
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||
const mixin = control.hierarchy.classHierarchyMixin<Class<Doc>, ToDoFactory>(
|
||||
actualTx.objectClass,
|
||||
serverTime.mixin.ToDoFactory
|
||||
)
|
||||
if (mixin !== undefined) {
|
||||
if (actualTx._class !== core.class.TxRemoveDoc) {
|
||||
const factory = await getResource(mixin.factory)
|
||||
result.push(...(await factory(tx, control)))
|
||||
} else {
|
||||
const todos = await control.findAll(control.ctx, time.class.ToDo, { attachedTo: actualTx.objectId })
|
||||
result.push(...todos.map((p) => control.txFactory.createTxRemoveDoc(p._class, p.space, p._id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
return result
|
||||
}
|
||||
|
||||
export async function OnWorkSlotUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
|
@ -169,6 +169,23 @@ export class DbAdapterManagerImpl implements DBAdapterManager {
|
||||
}
|
||||
}
|
||||
|
||||
getAdapterName (domain: Domain): string {
|
||||
const adapterName = this.conf.domains[domain]
|
||||
return adapterName ?? this.conf.defaultAdapter
|
||||
}
|
||||
|
||||
getAdapterByName (name: string): DbAdapter {
|
||||
if (name === this.conf.defaultAdapter) {
|
||||
return this.defaultAdapter
|
||||
}
|
||||
const adapter = this.adapters.get(name) ?? this.defaultAdapter
|
||||
if (adapter === undefined) {
|
||||
throw new Error('adapter not provided: ' + name)
|
||||
}
|
||||
|
||||
return adapter
|
||||
}
|
||||
|
||||
public getAdapter (domain: Domain, requireExists: boolean): DbAdapter {
|
||||
const name = this.conf.domains[domain] ?? '#default'
|
||||
const adapter = this.adapters.get(name) ?? this.defaultAdapter
|
||||
|
@ -126,21 +126,28 @@ export function initStatisticsContext (
|
||||
workspaces: ops?.getUsers?.()
|
||||
}
|
||||
|
||||
void fetch(
|
||||
concatLink(statsUrl, '/api/v1/statistics') + `/?token=${encodeURIComponent(token)}&name=${serviceId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}
|
||||
).catch((err) => {
|
||||
try {
|
||||
void fetch(
|
||||
concatLink(statsUrl, '/api/v1/statistics') + `/?token=${encodeURIComponent(token)}&name=${serviceId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}
|
||||
).catch((err) => {
|
||||
errorToSend++
|
||||
if (errorToSend % 20 === 0) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
} catch (err: any) {
|
||||
errorToSend++
|
||||
if (errorToSend % 20 === 0) {
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, METRICS_UPDATE_INTERVAL)
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
import core, {
|
||||
TxFactory,
|
||||
TxProcessor,
|
||||
generateId,
|
||||
groupByArray,
|
||||
matchQuery,
|
||||
type Class,
|
||||
@ -134,7 +133,6 @@ export class Triggers {
|
||||
mode: 'sync' | 'async'
|
||||
): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
|
||||
for (const { query, trigger, arrays } of this.triggers) {
|
||||
if ((trigger.isAsync ? 'async' : 'sync') !== mode) {
|
||||
continue
|
||||
@ -150,19 +148,14 @@ export class Triggers {
|
||||
trigger.resource,
|
||||
{},
|
||||
async (ctx) => {
|
||||
if (mode === 'async') {
|
||||
ctx.id = generateId()
|
||||
}
|
||||
const tresult = await this.applyTrigger(ctx, ctrl, matches, { trigger, arrays })
|
||||
result.push(...tresult)
|
||||
if (ctx.onEnd !== undefined && mode === 'async') {
|
||||
await ctx.onEnd(ctx)
|
||||
}
|
||||
},
|
||||
{ count: matches.length, arrays }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -141,6 +141,9 @@ export type TxMiddlewareResult = TxResult
|
||||
export interface DBAdapterManager {
|
||||
getAdapter: (domain: Domain, requireExists: boolean) => DbAdapter
|
||||
|
||||
getAdapterName: (domain: Domain) => string
|
||||
getAdapterByName: (name: string, requireExists: boolean) => DbAdapter
|
||||
|
||||
getDefaultAdapter: () => DbAdapter
|
||||
|
||||
close: () => Promise<void>
|
||||
|
@ -18,7 +18,6 @@ import type { ContentTextAdapterConfiguration, FullTextAdapterFactory } from '@h
|
||||
export * from './fulltext'
|
||||
export * from './indexer'
|
||||
export * from './rekoni'
|
||||
export * from './ydoc'
|
||||
|
||||
export interface FulltextDBConfiguration {
|
||||
fulltextAdapter: {
|
||||
|
@ -63,7 +63,7 @@ import type {
|
||||
StorageAdapter
|
||||
} from '@hcengineering/server-core'
|
||||
import { RateLimiter, SessionDataImpl } from '@hcengineering/server-core'
|
||||
import { jsonToText, markupToJSON } from '@hcengineering/text'
|
||||
import { jsonToText, markupToJSON, pmNodeToText, yDocContentToNodes } from '@hcengineering/text'
|
||||
import { findSearchPresenter, updateDocWithPresenter } from '../mapper'
|
||||
import { type FullTextPipeline } from './types'
|
||||
import { createIndexedDoc, createStateDoc, getContent } from './utils'
|
||||
@ -786,12 +786,21 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
if (collaborativeDoc !== undefined && collaborativeDoc !== '') {
|
||||
const { documentId } = collaborativeDocParse(collaborativeDoc)
|
||||
|
||||
const docInfo: Blob | undefined = await this.storageAdapter?.stat(ctx, this.workspace, documentId)
|
||||
try {
|
||||
await this.handleBlob(ctx, docInfo, indexedDoc)
|
||||
const readable = await this.storageAdapter?.read(ctx, this.workspace, documentId)
|
||||
const nodes = yDocContentToNodes(Buffer.concat(readable as any))
|
||||
let textContent = nodes.map(pmNodeToText).join('\n')
|
||||
textContent = textContent
|
||||
.split(/ +|\t+|\f+/)
|
||||
.filter((it) => it)
|
||||
.join(' ')
|
||||
.split(/\n\n+/)
|
||||
.join('\n')
|
||||
|
||||
indexedDoc.fulltextSummary += '\n' + textContent
|
||||
} catch (err: any) {
|
||||
Analytics.handleError(err)
|
||||
ctx.error('failed to handle blob', { _id: docInfo?._id, workspace: this.workspace.name })
|
||||
ctx.error('failed to handle blob', { _id: documentId, workspace: this.workspace.name })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +0,0 @@
|
||||
import { type MeasureContext, type WorkspaceId } from '@hcengineering/core'
|
||||
import { type ContentTextAdapter } from '@hcengineering/server-core'
|
||||
import { pmNodeToText, yDocContentToNodes } from '@hcengineering/text'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { Readable } from 'node:stream'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function createYDocAdapter (_url: string): Promise<ContentTextAdapter> {
|
||||
return {
|
||||
content: async (
|
||||
ctx: MeasureContext,
|
||||
workspace: WorkspaceId,
|
||||
_name: string,
|
||||
_type: string,
|
||||
data: Readable | Buffer | string
|
||||
): Promise<string> => {
|
||||
const chunks: any[] = []
|
||||
|
||||
if (data instanceof Readable) {
|
||||
await new Promise((resolve) => {
|
||||
data.on('readable', () => {
|
||||
let chunk: any
|
||||
while ((chunk = data.read()) !== null) {
|
||||
const b = chunk as Buffer
|
||||
chunks.push(b)
|
||||
}
|
||||
})
|
||||
|
||||
data.on('end', () => {
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
} else if (data instanceof Buffer) {
|
||||
chunks.push(data)
|
||||
} else {
|
||||
console.warn('ydoc content adapter does not support string content')
|
||||
}
|
||||
|
||||
if (chunks.length > 0) {
|
||||
const nodes = yDocContentToNodes(Buffer.concat(chunks))
|
||||
return nodes.map(pmNodeToText).join('\n')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
@ -14,9 +14,10 @@
|
||||
//
|
||||
|
||||
import core, {
|
||||
Domain,
|
||||
groupByArray,
|
||||
TxProcessor,
|
||||
type Doc,
|
||||
type Domain,
|
||||
type MeasureContext,
|
||||
type SessionData,
|
||||
type Tx,
|
||||
@ -24,7 +25,13 @@ import core, {
|
||||
type TxResult
|
||||
} from '@hcengineering/core'
|
||||
import { PlatformError, unknownError } from '@hcengineering/platform'
|
||||
import type { DBAdapterManager, Middleware, PipelineContext, TxMiddlewareResult } from '@hcengineering/server-core'
|
||||
import type {
|
||||
DbAdapter,
|
||||
DBAdapterManager,
|
||||
Middleware,
|
||||
PipelineContext,
|
||||
TxMiddlewareResult
|
||||
} from '@hcengineering/server-core'
|
||||
import { BaseMiddleware } from '@hcengineering/server-core'
|
||||
|
||||
/**
|
||||
@ -66,32 +73,41 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
private async routeTx (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<TxResult[]> {
|
||||
const result: TxResult[] = []
|
||||
|
||||
const domainGroups = new Map<Domain, TxCUD<Doc>[]>()
|
||||
const adapterGroups = new Map<string, TxCUD<Doc>[]>()
|
||||
|
||||
const routeToAdapter = async (domain: Domain, txes: TxCUD<Doc>[]): Promise<void> => {
|
||||
const routeToAdapter = async (adapter: DbAdapter, txes: TxCUD<Doc>[]): Promise<void> => {
|
||||
if (txes.length > 0) {
|
||||
// Find all deleted documents
|
||||
|
||||
const adapter = this.adapterManager.getAdapter(domain, true)
|
||||
const toDelete = txes.filter((it) => it._class === core.class.TxRemoveDoc).map((it) => it.objectId)
|
||||
const toDelete = txes.filter((it) => it._class === core.class.TxRemoveDoc)
|
||||
|
||||
if (toDelete.length > 0) {
|
||||
const toDeleteDocs = await ctx.with(
|
||||
'adapter-load',
|
||||
{ domain },
|
||||
async () => await adapter.load(ctx, domain, toDelete),
|
||||
{ count: toDelete.length }
|
||||
)
|
||||
const deleteByDomain = groupByArray(toDelete, (it) => this.context.hierarchy.getDomain(it.objectClass))
|
||||
|
||||
for (const ddoc of toDeleteDocs) {
|
||||
ctx.contextData.removedMap.set(ddoc._id, ddoc)
|
||||
for (const [d, docs] of deleteByDomain.entries()) {
|
||||
const todel = await ctx.with(
|
||||
'adapter-load',
|
||||
{},
|
||||
() =>
|
||||
adapter.load(
|
||||
ctx,
|
||||
d,
|
||||
docs.map((it) => it._id)
|
||||
),
|
||||
{ count: toDelete.length }
|
||||
)
|
||||
|
||||
for (const ddoc of todel) {
|
||||
ctx.contextData.removedMap.set(ddoc._id, ddoc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const r = await ctx.with('adapter-tx', { domain }, (ctx) => adapter.tx(ctx, ...txes), {
|
||||
const classes = Array.from(new Set(txes.map((it) => it.objectClass)))
|
||||
const _classes = Array.from(new Set(txes.map((it) => it._class)))
|
||||
const r = await ctx.with('adapter-tx', {}, (ctx) => adapter.tx(ctx, ...txes), {
|
||||
txes: txes.length,
|
||||
classes: Array.from(new Set(txes.map((it) => it.objectClass))),
|
||||
_classes: Array.from(new Set(txes.map((it) => it._class)))
|
||||
classes,
|
||||
_classes
|
||||
})
|
||||
|
||||
if (Array.isArray(r)) {
|
||||
@ -102,6 +118,7 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
}
|
||||
}
|
||||
|
||||
const domains = new Set<Domain>()
|
||||
for (const tx of txes) {
|
||||
const txCUD = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||
if (!TxProcessor.isExtendsCUD(txCUD._class)) {
|
||||
@ -110,16 +127,26 @@ export class DomainTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
continue
|
||||
}
|
||||
const domain = this.context.hierarchy.getDomain(txCUD.objectClass)
|
||||
domains.add(domain)
|
||||
const adapterName = this.adapterManager.getAdapterName(domain)
|
||||
|
||||
let group = domainGroups.get(domain)
|
||||
let group = adapterGroups.get(adapterName)
|
||||
if (group === undefined) {
|
||||
group = []
|
||||
domainGroups.set(domain, group)
|
||||
adapterGroups.set(adapterName, group)
|
||||
}
|
||||
group.push(txCUD)
|
||||
}
|
||||
for (const [domain, txes] of domainGroups.entries()) {
|
||||
await routeToAdapter(domain, txes)
|
||||
|
||||
// We need to mark domains to set existing
|
||||
for (const d of domains) {
|
||||
// We need to mark adapter
|
||||
this.adapterManager.getAdapter(d, true)
|
||||
}
|
||||
|
||||
for (const [adapterName, txes] of adapterGroups.entries()) {
|
||||
const adapter = this.adapterManager.getAdapterByName(adapterName, true)
|
||||
await routeToAdapter(adapter, txes)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -89,9 +89,7 @@ export class LiveQueryMiddleware extends BaseMiddleware implements Middleware {
|
||||
}
|
||||
|
||||
async tx (ctx: MeasureContext, tx: Tx[]): Promise<TxMiddlewareResult> {
|
||||
for (const _tx of tx) {
|
||||
await this.liveQuery.tx(_tx)
|
||||
}
|
||||
await this.liveQuery.tx(...tx)
|
||||
return await this.provideTx(ctx, tx)
|
||||
}
|
||||
}
|
||||
|
@ -54,8 +54,9 @@ export class QueryJoiner {
|
||||
}
|
||||
if (q.result instanceof Promise) {
|
||||
q.result = await q.result
|
||||
q.callbacks--
|
||||
}
|
||||
q.callbacks--
|
||||
|
||||
this.removeFromQueue(q)
|
||||
|
||||
return q.result as FindResult<T>
|
||||
|
25
server/middleware/src/tests/queryJoiner.spec.ts
Normal file
25
server/middleware/src/tests/queryJoiner.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import core, { MeasureMetricsContext, toFindResult } from '@hcengineering/core'
|
||||
import type { SessionFindAll } from '@hcengineering/server-core'
|
||||
import { QueryJoiner } from '../queryJoin'
|
||||
|
||||
describe('test query joiner', () => {
|
||||
it('test find', async () => {
|
||||
let count = 0
|
||||
const findT: SessionFindAll = async (ctx, _class, query, options) => {
|
||||
await new Promise<void>((resolve) => {
|
||||
count++
|
||||
setTimeout(resolve, 100)
|
||||
})
|
||||
return toFindResult([])
|
||||
}
|
||||
const joiner = new QueryJoiner(findT)
|
||||
const ctx = new MeasureMetricsContext('test', {})
|
||||
|
||||
const p1 = joiner.findAll(ctx, core.class.Class, {})
|
||||
const p2 = joiner.findAll(ctx, core.class.Class, {})
|
||||
await Promise.all([p1, p2])
|
||||
expect(count).toBe(1)
|
||||
expect((joiner as any).queries.size).toBe(1)
|
||||
expect((joiner as any).queries.get(core.class.Class).length).toBe(0)
|
||||
})
|
||||
})
|
@ -51,6 +51,7 @@ import type {
|
||||
} from '@hcengineering/server-core'
|
||||
import serverCore, { BaseMiddleware, SessionDataImpl, SessionFindAll, Triggers } from '@hcengineering/server-core'
|
||||
import { filterBroadcastOnly } from './utils'
|
||||
import { QueryJoiner } from './queryJoin'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -106,13 +107,14 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
|
||||
}
|
||||
|
||||
private async processDerived (ctx: MeasureContext<SessionData>, txes: Tx[]): Promise<void> {
|
||||
const findAll: SessionFindAll = async (ctx, _class, query, options) => {
|
||||
const _findAll: SessionFindAll = async (ctx, _class, query, options) => {
|
||||
const _ctx: MeasureContext = (options as ServerFindOptions<Doc>)?.ctx ?? ctx
|
||||
delete (options as ServerFindOptions<Doc>)?.ctx
|
||||
if (_ctx.contextData !== undefined) {
|
||||
_ctx.contextData.isTriggerCtx = true
|
||||
}
|
||||
|
||||
// Use live query
|
||||
const results = await this.findAll(_ctx, _class, query, options)
|
||||
return toFindResult(
|
||||
results.map((v) => {
|
||||
@ -121,6 +123,12 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
|
||||
results.total
|
||||
)
|
||||
}
|
||||
const joiner = new QueryJoiner(_findAll)
|
||||
|
||||
const findAll: SessionFindAll = async (ctx, _class, query, options) => {
|
||||
return await joiner.findAll(ctx, _class, query, options)
|
||||
}
|
||||
|
||||
const removed = await ctx.with('process-remove', {}, (ctx) => this.processRemove(ctx, txes, findAll))
|
||||
const collections = await ctx.with('process-collection', {}, (ctx) => this.processCollection(ctx, txes, findAll))
|
||||
const moves = await ctx.with('process-move', {}, (ctx) => this.processMove(ctx, txes, findAll))
|
||||
@ -223,6 +231,10 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
|
||||
this.context.modelDb
|
||||
)
|
||||
ctx.contextData = asyncContextData
|
||||
|
||||
if (!((ctx as MeasureContext<SessionDataImpl>).contextData.isAsyncContext ?? false)) {
|
||||
ctx.id = generateId()
|
||||
}
|
||||
const aresult = await this.triggers.apply(
|
||||
ctx,
|
||||
txes,
|
||||
@ -236,16 +248,15 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
|
||||
)
|
||||
|
||||
if (aresult.length > 0) {
|
||||
await ctx.with('process-aync-result', {}, async (ctx) => {
|
||||
ctx.id = generateId()
|
||||
await ctx.with('process-async-result', {}, async (ctx) => {
|
||||
await this.processDerivedTxes(ctx, aresult)
|
||||
// We need to send all to recipients
|
||||
await this.context.head?.handleBroadcast(ctx)
|
||||
if (ctx.onEnd !== undefined) {
|
||||
await ctx.onEnd(ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (ctx.onEnd !== undefined) {
|
||||
await ctx.onEnd(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
private async processDerivedTxes (ctx: MeasureContext<SessionData>, derived: Tx[]): Promise<void> {
|
||||
|
@ -14,6 +14,7 @@ export { serverGithubId } from '@hcengineering/server-github'
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
|
||||
trigger: serverGithub.trigger.OnProjectChanges,
|
||||
arrays: true,
|
||||
isAsync: true
|
||||
})
|
||||
|
||||
|
@ -29,52 +29,60 @@ import tracker from '@hcengineering/tracker'
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function OnProjectChanges (tx: Tx, control: TriggerControl): Promise<Tx[]> {
|
||||
const ltx = TxProcessor.extractTx(tx)
|
||||
export async function OnProjectChanges (txes: Tx[], control: TriggerControl): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
const cache = new Map<string, any>()
|
||||
|
||||
if (ltx._class === core.class.TxMixin && (ltx as TxMixin<Doc, Doc>).mixin === github.mixin.GithubIssue) {
|
||||
const mix = ltx as TxMixin<Doc, Doc>
|
||||
// Do not spend time to wait for trigger processing
|
||||
await updateDocSyncInfo(control, tx, mix.objectSpace, mix)
|
||||
return []
|
||||
}
|
||||
const toApply: Tx[] = []
|
||||
for (const tx of txes) {
|
||||
const ltx = TxProcessor.extractTx(tx)
|
||||
|
||||
if (control.hierarchy.isDerived(ltx._class, core.class.TxCUD)) {
|
||||
const cud = ltx as TxCUD<Doc>
|
||||
|
||||
let space: Ref<Space> = cud.objectSpace
|
||||
|
||||
if (cud._class === core.class.TxUpdateDoc) {
|
||||
const upd = cud as TxUpdateDoc<Doc>
|
||||
if (upd.operations.space != null) {
|
||||
space = upd.operations.space
|
||||
}
|
||||
}
|
||||
|
||||
if (isDocSyncUpdateRequired(control.hierarchy, cud)) {
|
||||
if (ltx._class === core.class.TxMixin && (ltx as TxMixin<Doc, Doc>).mixin === github.mixin.GithubIssue) {
|
||||
const mix = ltx as TxMixin<Doc, Doc>
|
||||
// Do not spend time to wait for trigger processing
|
||||
await updateDocSyncInfo(control, tx, space, cud)
|
||||
await updateDocSyncInfo(control, tx, mix.objectSpace, mix, cache, toApply)
|
||||
continue
|
||||
}
|
||||
if (control.hierarchy.isDerived(cud.objectClass, time.class.ToDo)) {
|
||||
if (tx._class === core.class.TxCollectionCUD) {
|
||||
const coll = tx as TxCollectionCUD<Doc, AttachedDoc>
|
||||
if (control.hierarchy.isDerived(coll.objectClass, github.class.GithubPullRequest)) {
|
||||
// Ok we got todo change for pull request, let's mark it for sync.
|
||||
return [
|
||||
control.txFactory.createTxUpdateDoc<DocSyncInfo>(
|
||||
github.class.DocSyncInfo,
|
||||
coll.objectSpace,
|
||||
coll.objectId as Ref<DocSyncInfo>,
|
||||
{
|
||||
needSync: ''
|
||||
}
|
||||
|
||||
if (TxProcessor.isExtendsCUD(ltx._class)) {
|
||||
const cud = ltx as TxCUD<Doc>
|
||||
|
||||
let space: Ref<Space> = cud.objectSpace
|
||||
|
||||
if (cud._class === core.class.TxUpdateDoc) {
|
||||
const upd = cud as TxUpdateDoc<Doc>
|
||||
if (upd.operations.space != null) {
|
||||
space = upd.operations.space
|
||||
}
|
||||
}
|
||||
|
||||
if (isDocSyncUpdateRequired(control.hierarchy, cud)) {
|
||||
await updateDocSyncInfo(control, tx, space, cud, cache, toApply)
|
||||
}
|
||||
if (control.hierarchy.isDerived(cud.objectClass, time.class.ToDo)) {
|
||||
if (tx._class === core.class.TxCollectionCUD) {
|
||||
const coll = tx as TxCollectionCUD<Doc, AttachedDoc>
|
||||
if (control.hierarchy.isDerived(coll.objectClass, github.class.GithubPullRequest)) {
|
||||
// Ok we got todo change for pull request, let's mark it for sync.
|
||||
result.push(
|
||||
control.txFactory.createTxUpdateDoc<DocSyncInfo>(
|
||||
github.class.DocSyncInfo,
|
||||
coll.objectSpace,
|
||||
coll.objectId as Ref<DocSyncInfo>,
|
||||
{
|
||||
needSync: ''
|
||||
}
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
if (toApply.length > 0) {
|
||||
await control.apply(control.ctx, toApply)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
@ -108,14 +116,16 @@ async function updateDocSyncInfo (
|
||||
_class: Ref<Class<Tx>>
|
||||
objectId: Ref<Doc>
|
||||
objectClass: Ref<Class<Doc>>
|
||||
}
|
||||
},
|
||||
cache: Map<string, any>,
|
||||
toApply: Tx[]
|
||||
): Promise<void> {
|
||||
const checkTx = (tx: Tx): boolean =>
|
||||
control.hierarchy.isDerived(tx._class, core.class.TxCUD) &&
|
||||
(tx as TxCUD<Doc>).objectClass === github.class.DocSyncInfo &&
|
||||
(tx as TxCUD<Doc>).objectId === cud.objectId
|
||||
|
||||
const txes = [...control.txes, ...control.ctx.contextData.broadcast.txes]
|
||||
const txes = [...control.txes, ...control.ctx.contextData.broadcast.txes, ...toApply]
|
||||
// Check already captured Txes
|
||||
for (const i of txes) {
|
||||
if (checkTx(i)) {
|
||||
@ -131,20 +141,29 @@ async function updateDocSyncInfo (
|
||||
if (account === undefined) {
|
||||
return
|
||||
}
|
||||
const projects =
|
||||
(cache.get('projects') as GithubProject[]) ??
|
||||
(await control.queryFind(control.ctx, github.mixin.GithubProject, {}, { projection: { _id: 1 } }))
|
||||
cache.set('projects', projects)
|
||||
|
||||
const projects = await control.queryFind(control.ctx, github.mixin.GithubProject, {}, { projection: { _id: 1 } })
|
||||
if (projects.some((it) => it._id === (space as Ref<GithubProject>))) {
|
||||
const [sdoc] = await control.findAll(control.ctx, github.class.DocSyncInfo, {
|
||||
_id: cud.objectId as Ref<DocSyncInfo>
|
||||
})
|
||||
const sdoc =
|
||||
(cache.get(cud.objectId) as DocSyncInfo) ??
|
||||
(
|
||||
await control.findAll(control.ctx, github.class.DocSyncInfo, {
|
||||
_id: cud.objectId as Ref<DocSyncInfo>
|
||||
})
|
||||
).shift()
|
||||
|
||||
// We need to check if sync doc is already exists.
|
||||
if (sdoc === undefined) {
|
||||
// Created by non github integration
|
||||
// We need to create the doc sync info
|
||||
await createSyncDoc(control, cud, tx, space)
|
||||
createSyncDoc(control, cud, tx, space, toApply)
|
||||
} else {
|
||||
cache.set(cud.objectId, sdoc)
|
||||
// We need to create the doc sync info
|
||||
await updateSyncDoc(control, cud, space, sdoc)
|
||||
updateSyncDoc(control, cud, space, sdoc, toApply)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -160,7 +179,7 @@ function isDocSyncUpdateRequired (h: Hierarchy, coll: TxCUD<Doc>): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
async function updateSyncDoc (
|
||||
function updateSyncDoc (
|
||||
control: TriggerControl,
|
||||
cud: {
|
||||
_class: Ref<Class<Tx>>
|
||||
@ -168,8 +187,9 @@ async function updateSyncDoc (
|
||||
objectClass: Ref<Class<Doc>>
|
||||
},
|
||||
space: Ref<Space>,
|
||||
info: DocSyncInfo
|
||||
): Promise<void> {
|
||||
info: DocSyncInfo,
|
||||
toApply: Tx[]
|
||||
): void {
|
||||
const data: DocumentUpdate<DocSyncInfo> =
|
||||
cud._class === core.class.TxRemoveDoc
|
||||
? {
|
||||
@ -183,14 +203,14 @@ async function updateSyncDoc (
|
||||
data.externalVersion = '#' // We need to put this one to handle new documents.)
|
||||
data.space = space
|
||||
}
|
||||
await control.apply(control.ctx, [
|
||||
toApply.push(
|
||||
control.txFactory.createTxUpdateDoc<DocSyncInfo>(
|
||||
github.class.DocSyncInfo,
|
||||
info.space,
|
||||
cud.objectId as Ref<DocSyncInfo>,
|
||||
data
|
||||
)
|
||||
])
|
||||
)
|
||||
|
||||
control.ctx.contextData.broadcast.targets.github = (it) => {
|
||||
if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) {
|
||||
@ -201,7 +221,7 @@ async function updateSyncDoc (
|
||||
}
|
||||
}
|
||||
|
||||
async function createSyncDoc (
|
||||
function createSyncDoc (
|
||||
control: TriggerControl,
|
||||
cud: {
|
||||
_class: Ref<Class<Tx>>
|
||||
@ -209,8 +229,9 @@ async function createSyncDoc (
|
||||
objectClass: Ref<Class<Doc>>
|
||||
},
|
||||
tx: Tx,
|
||||
space: Ref<Space>
|
||||
): Promise<void> {
|
||||
space: Ref<Space>,
|
||||
toApply: Tx[]
|
||||
): void {
|
||||
const data: DocumentUpdate<DocSyncInfo> = {
|
||||
url: '',
|
||||
githubNumber: 0,
|
||||
@ -226,14 +247,14 @@ async function createSyncDoc (
|
||||
data.attachedTo = coll.objectId
|
||||
}
|
||||
|
||||
await control.apply(control.ctx, [
|
||||
toApply.push(
|
||||
control.txFactory.createTxCreateDoc<DocSyncInfo>(
|
||||
github.class.DocSyncInfo,
|
||||
space,
|
||||
data,
|
||||
cud.objectId as Ref<DocSyncInfo>
|
||||
)
|
||||
])
|
||||
)
|
||||
control.ctx.contextData.broadcast.targets.github = (it) => {
|
||||
if (control.hierarchy.isDerived(it._class, core.class.TxCUD)) {
|
||||
if ((it as TxCUD<Doc>).objectClass === github.class.DocSyncInfo) {
|
||||
|
@ -56,6 +56,7 @@ services:
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- MODEL_ENABLED=*
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
- STATS_URL=http://stats:4901
|
||||
workspace:
|
||||
image: hardcoreeng/workspace
|
||||
links:
|
||||
@ -72,6 +73,7 @@ services:
|
||||
- MODEL_ENABLED=*
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
- STATS_URL=http://stats:4901
|
||||
restart: unless-stopped
|
||||
front:
|
||||
image: hardcoreeng/front
|
||||
@ -100,6 +102,8 @@ services:
|
||||
- COLLABORATOR_URL=ws://localhost:3079
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- BRANDING_URL=http://localhost:8083/branding-test.json
|
||||
- STATS_URL=http://stats:4901
|
||||
- STATS_API=http://localhost:4901
|
||||
transactor:
|
||||
image: hardcoreeng/transactor
|
||||
pull_policy: never
|
||||
@ -128,6 +132,7 @@ services:
|
||||
- LAST_NAME_FIRST=true
|
||||
- BRANDING_PATH=/var/cfg/branding.json
|
||||
- FULLTEXT_URL=http://fulltext:4710
|
||||
- STATS_URL=http://stats:4901
|
||||
collaborator:
|
||||
image: hardcoreeng/collaborator
|
||||
links:
|
||||
@ -142,20 +147,18 @@ services:
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- FULLTEXT_URL=http://fulltext:4710
|
||||
- STATS_URL=http://stats:4901
|
||||
restart: unless-stopped
|
||||
rekoni:
|
||||
image: hardcoreeng/rekoni-service
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 4007:4004
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 1024M
|
||||
environment:
|
||||
- STATS_URL=http://stats:4901
|
||||
|
||||
fulltext:
|
||||
image: hardcoreeng/fulltext
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- elastic
|
||||
@ -172,4 +175,13 @@ services:
|
||||
- ELASTIC_INDEX_NAME=local_storage_index
|
||||
- STORAGE_CONFIG=${STORAGE_CONFIG}
|
||||
- REKONI_URL=http://rekoni:4007
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- ACCOUNTS_URL=http://account:3003
|
||||
- STATS_URL=http://stats:4901
|
||||
stats:
|
||||
image: hardcoreeng/stats
|
||||
ports:
|
||||
- 4901:4901
|
||||
environment:
|
||||
- PORT=4901
|
||||
- SERVER_SECRET=secret
|
||||
restart: unless-stopped
|
@ -23,6 +23,8 @@ fi
|
||||
# Create user record in accounts
|
||||
./tool.sh create-account user1 -f John -l Appleseed -p 1234
|
||||
./tool.sh create-account user2 -f Kainin -l Dirak -p 1234
|
||||
./tool.sh create-account super -f Super -l User -p 1234
|
||||
./tool.sh set-user-admin super true
|
||||
./tool.sh assign-workspace user1 sanity-ws
|
||||
./tool.sh assign-workspace user2 sanity-ws
|
||||
./tool.sh set-user-role user1 sanity-ws OWNER
|
||||
|
@ -253,29 +253,40 @@ test.describe('Inbox tests', () => {
|
||||
const linkText = await page.locator('.antiPopup .link').textContent()
|
||||
const page2 = await browser.newPage()
|
||||
try {
|
||||
const leftSideMenuPageSecond = new LeftSideMenuPage(page2)
|
||||
const inboxPageSecond = new InboxPage(page2)
|
||||
const channelPage2 = new ChannelPage(page2)
|
||||
const leftSideMenuPage2 = new LeftSideMenuPage(page2)
|
||||
const inboxPage2 = new InboxPage(page2)
|
||||
await leftSideMenuPage.clickOnCloseInvite()
|
||||
await page2.goto(linkText ?? '')
|
||||
const joinPage = new SignInJoinPage(page2)
|
||||
await joinPage.join(newUser2)
|
||||
|
||||
const joinPage2 = new SignInJoinPage(page2)
|
||||
await joinPage2.join(newUser2)
|
||||
|
||||
await leftSideMenuPage2.clickChunter()
|
||||
await channelPage2.clickChannel('general')
|
||||
|
||||
await leftSideMenuPage.clickChunter()
|
||||
await channelPage.clickChannel('general')
|
||||
await channelPage.sendMessage('Test message')
|
||||
|
||||
await leftSideMenuPage2.clickNotification()
|
||||
await inboxPage2.clickOnInboxFilter('Channels')
|
||||
|
||||
await leftSideMenuPage.clickTracker()
|
||||
|
||||
const newIssue = createNewIssueData(newUser2.firstName, newUser2.lastName)
|
||||
await prepareNewIssueWithOpenStep(page, newIssue, false)
|
||||
await issuesDetailsPage.checkIssue(newIssue)
|
||||
await leftSideMenuPageSecond.clickTracker()
|
||||
await leftSideMenuPageSecond.clickNotification()
|
||||
await inboxPageSecond.clickOnInboxFilter('Channels')
|
||||
await inboxPageSecond.checkIfInboxChatExists(newIssue.title, false)
|
||||
await inboxPageSecond.checkIfInboxChatExists('Test message', true)
|
||||
await inboxPageSecond.clickOnInboxFilter('Issues')
|
||||
await inboxPageSecond.checkIfIssueIsPresentInInbox(newIssue.title)
|
||||
await inboxPageSecond.checkIfInboxChatExists('Channel general', false)
|
||||
|
||||
await leftSideMenuPage2.clickTracker()
|
||||
await leftSideMenuPage2.clickNotification()
|
||||
|
||||
await inboxPage2.clickOnInboxFilter('Channels')
|
||||
await inboxPage2.checkIfInboxChatExists(newIssue.title, false)
|
||||
await inboxPage2.checkIfInboxChatExists('Test message', true)
|
||||
await inboxPage2.clickOnInboxFilter('Issues')
|
||||
await inboxPage2.checkIfIssueIsPresentInInbox(newIssue.title)
|
||||
await inboxPage2.checkIfInboxChatExists('Channel general', false)
|
||||
} finally {
|
||||
await page2.close()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user