mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-21 16:09:12 +03:00
UBERF-7532: Bulk operations for triggers (#6023)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
fa82ee1939
commit
4eac1927f0
@ -13,11 +13,11 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { addLocation } from '@hcengineering/platform'
|
import { devModelId } from '@hcengineering/devmodel'
|
||||||
|
import { PresentationClientHook } from '@hcengineering/devmodel-resources'
|
||||||
import login from '@hcengineering/login'
|
import login from '@hcengineering/login'
|
||||||
import { setMetadata } from '@hcengineering/platform'
|
import { addLocation, setMetadata } from '@hcengineering/platform'
|
||||||
import devmodel, { devModelId } from '@hcengineering/devmodel'
|
import presentation from '@hcengineering/presentation'
|
||||||
import client from '@hcengineering/client'
|
|
||||||
|
|
||||||
export function configurePlatformDevServer() {
|
export function configurePlatformDevServer() {
|
||||||
console.log('Use Endpoint override:', process.env.LOGIN_ENDPOINT)
|
console.log('Use Endpoint override:', process.env.LOGIN_ENDPOINT)
|
||||||
@ -28,6 +28,6 @@ export function configurePlatformDevServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function enableDevModel() {
|
function enableDevModel() {
|
||||||
setMetadata(client.metadata.ClientHook, devmodel.hook.Hook)
|
setMetadata(presentation.metadata.ClientHook, new PresentationClientHook())
|
||||||
addLocation(devModelId, () => import(/* webpackChunkName: "devmodel" */ '@hcengineering/devmodel-resources'))
|
addLocation(devModelId, () => import(/* webpackChunkName: "devmodel" */ '@hcengineering/devmodel-resources'))
|
||||||
}
|
}
|
||||||
|
@ -122,9 +122,6 @@ describe('client', () => {
|
|||||||
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
|
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
|
||||||
loadModel: async (last: Timestamp) => clone(txes),
|
loadModel: async (last: Timestamp) => clone(txes),
|
||||||
getAccount: async () => null as unknown as Account,
|
getAccount: async () => null as unknown as Account,
|
||||||
measure: async () => {
|
|
||||||
return async () => ({ time: 0, serverTime: 0 })
|
|
||||||
},
|
|
||||||
sendForceClose: async () => {}
|
sendForceClose: async () => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,6 @@ export async function connect (handler: (tx: Tx) => void): Promise<ClientConnect
|
|||||||
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
|
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
|
||||||
loadModel: async (last: Timestamp) => txes,
|
loadModel: async (last: Timestamp) => txes,
|
||||||
getAccount: async () => null as unknown as Account,
|
getAccount: async () => null as unknown as Account,
|
||||||
measure: async () => async () => ({ time: 0, serverTime: 0 }),
|
|
||||||
sendForceClose: async () => {}
|
sendForceClose: async () => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,17 +47,10 @@ export interface Client extends Storage, FulltextStorage {
|
|||||||
close: () => Promise<void>
|
close: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MeasureDoneOperation = () => Promise<{ time: number, serverTime: number }>
|
|
||||||
|
|
||||||
export interface MeasureClient extends Client {
|
|
||||||
// Will perform on server operation measure and will return a local client time and on server time
|
|
||||||
measure: (operationName: string) => Promise<MeasureDoneOperation>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface AccountClient extends MeasureClient {
|
export interface AccountClient extends Client {
|
||||||
getAccount: () => Promise<Account>
|
getAccount: () => Promise<Account>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,11 +90,9 @@ export interface ClientConnection extends Storage, FulltextStorage, BackupClient
|
|||||||
// If hash is passed, will return LoadModelResponse
|
// If hash is passed, will return LoadModelResponse
|
||||||
loadModel: (last: Timestamp, hash?: string) => Promise<Tx[] | LoadModelResponse>
|
loadModel: (last: Timestamp, hash?: string) => Promise<Tx[] | LoadModelResponse>
|
||||||
getAccount: () => Promise<Account>
|
getAccount: () => Promise<Account>
|
||||||
|
|
||||||
measure: (operationName: string) => Promise<MeasureDoneOperation>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ClientImpl implements AccountClient, BackupClient, MeasureClient {
|
class ClientImpl implements AccountClient, BackupClient {
|
||||||
notify?: (...tx: Tx[]) => void
|
notify?: (...tx: Tx[]) => void
|
||||||
hierarchy!: Hierarchy
|
hierarchy!: Hierarchy
|
||||||
model!: ModelDb
|
model!: ModelDb
|
||||||
@ -163,10 +154,6 @@ class ClientImpl implements AccountClient, BackupClient, MeasureClient {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
|
||||||
return await this.conn.measure(operationName)
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateFromRemote (...tx: Tx[]): Promise<void> {
|
async updateFromRemote (...tx: Tx[]): Promise<void> {
|
||||||
for (const t of tx) {
|
for (const t of tx) {
|
||||||
try {
|
try {
|
||||||
|
@ -312,8 +312,8 @@ export class TxOperations implements Omit<Client, 'notify'> {
|
|||||||
return this.removeDoc(doc._class, doc.space, doc._id)
|
return this.removeDoc(doc._class, doc.space, doc._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
apply (scope: string): ApplyOperations {
|
apply (scope: string, measure?: string): ApplyOperations {
|
||||||
return new ApplyOperations(this, scope)
|
return new ApplyOperations(this, scope, measure)
|
||||||
}
|
}
|
||||||
|
|
||||||
async diffUpdate<T extends Doc = Doc>(
|
async diffUpdate<T extends Doc = Doc>(
|
||||||
@ -423,6 +423,12 @@ export class TxOperations implements Omit<Client, 'notify'> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CommitResult {
|
||||||
|
result: boolean
|
||||||
|
time: number
|
||||||
|
serverTime: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
@ -436,7 +442,8 @@ export class ApplyOperations extends TxOperations {
|
|||||||
notMatches: DocumentClassQuery<Doc>[] = []
|
notMatches: DocumentClassQuery<Doc>[] = []
|
||||||
constructor (
|
constructor (
|
||||||
readonly ops: TxOperations,
|
readonly ops: TxOperations,
|
||||||
readonly scope: string
|
readonly scope: string,
|
||||||
|
readonly measureName?: string
|
||||||
) {
|
) {
|
||||||
const txClient: Client = {
|
const txClient: Client = {
|
||||||
getHierarchy: () => ops.client.getHierarchy(),
|
getHierarchy: () => ops.client.getHierarchy(),
|
||||||
@ -465,23 +472,28 @@ export class ApplyOperations extends TxOperations {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
async commit (notify: boolean = true, extraNotify: Ref<Class<Doc>>[] = []): Promise<boolean> {
|
async commit (notify: boolean = true, extraNotify: Ref<Class<Doc>>[] = []): Promise<CommitResult> {
|
||||||
if (this.txes.length > 0) {
|
if (this.txes.length > 0) {
|
||||||
return (
|
const st = Date.now()
|
||||||
await ((await this.ops.tx(
|
const result = await ((await this.ops.tx(
|
||||||
this.ops.txFactory.createTxApplyIf(
|
this.ops.txFactory.createTxApplyIf(
|
||||||
core.space.Tx,
|
core.space.Tx,
|
||||||
this.scope,
|
this.scope,
|
||||||
this.matches,
|
this.matches,
|
||||||
this.notMatches,
|
this.notMatches,
|
||||||
this.txes,
|
this.txes,
|
||||||
notify,
|
this.measureName,
|
||||||
extraNotify
|
notify,
|
||||||
)
|
extraNotify
|
||||||
)) as Promise<TxApplyResult>)
|
)
|
||||||
).success
|
)) as Promise<TxApplyResult>)
|
||||||
|
return {
|
||||||
|
result: result.success,
|
||||||
|
time: Date.now() - st,
|
||||||
|
serverTime: result.serverTime
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return { result: true, time: 0, serverTime: 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,14 +33,15 @@ export interface StorageIterator {
|
|||||||
close: (ctx: MeasureContext) => Promise<void>
|
close: (ctx: MeasureContext) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BroadcastTargets = Record<string, (tx: Tx) => string[] | undefined>
|
||||||
|
|
||||||
export interface SessionOperationContext {
|
export interface SessionOperationContext {
|
||||||
ctx: MeasureContext
|
ctx: MeasureContext
|
||||||
// A parts of derived data to deal with after operation will be complete
|
// A parts of derived data to deal with after operation will be complete
|
||||||
derived: {
|
derived: {
|
||||||
derived: Tx[]
|
txes: Tx[]
|
||||||
target?: string[]
|
targets: BroadcastTargets // A set of broadcast filters if required
|
||||||
}[]
|
}
|
||||||
|
|
||||||
with: <T>(
|
with: <T>(
|
||||||
name: string,
|
name: string,
|
||||||
params: ParamsType,
|
params: ParamsType,
|
||||||
|
@ -28,13 +28,13 @@ import type {
|
|||||||
Space,
|
Space,
|
||||||
Timestamp
|
Timestamp
|
||||||
} from './classes'
|
} from './classes'
|
||||||
|
import { clone } from './clone'
|
||||||
import core from './component'
|
import core from './component'
|
||||||
import { setObjectValue } from './objvalue'
|
import { setObjectValue } from './objvalue'
|
||||||
import { _getOperator } from './operator'
|
import { _getOperator } from './operator'
|
||||||
import { _toDoc } from './proxy'
|
import { _toDoc } from './proxy'
|
||||||
import type { DocumentQuery, TxResult } from './storage'
|
import type { DocumentQuery, TxResult } from './storage'
|
||||||
import { generateId } from './utils'
|
import { generateId } from './utils'
|
||||||
import { clone } from './clone'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -137,10 +137,14 @@ export interface TxApplyIf extends Tx {
|
|||||||
|
|
||||||
// If passed, will send WorkspaceEvent.BulkUpdate event with list of classes to update
|
// If passed, will send WorkspaceEvent.BulkUpdate event with list of classes to update
|
||||||
extraNotify?: Ref<Class<Doc>>[]
|
extraNotify?: Ref<Class<Doc>>[]
|
||||||
|
|
||||||
|
// If defined will go into a separate measure section
|
||||||
|
measureName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TxApplyResult {
|
export interface TxApplyResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
|
serverTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -618,6 +622,7 @@ export class TxFactory {
|
|||||||
match: DocumentClassQuery<Doc>[],
|
match: DocumentClassQuery<Doc>[],
|
||||||
notMatch: DocumentClassQuery<Doc>[],
|
notMatch: DocumentClassQuery<Doc>[],
|
||||||
txes: TxCUD<Doc>[],
|
txes: TxCUD<Doc>[],
|
||||||
|
measureName: string | undefined,
|
||||||
notify: boolean = true,
|
notify: boolean = true,
|
||||||
extraNotify: Ref<Class<Doc>>[] = [],
|
extraNotify: Ref<Class<Doc>>[] = [],
|
||||||
modifiedOn?: Timestamp,
|
modifiedOn?: Timestamp,
|
||||||
@ -634,6 +639,7 @@ export class TxFactory {
|
|||||||
match,
|
match,
|
||||||
notMatch,
|
notMatch,
|
||||||
txes,
|
txes,
|
||||||
|
measureName,
|
||||||
notify,
|
notify,
|
||||||
extraNotify
|
extraNotify
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,6 @@ import {
|
|||||||
type FindOptions,
|
type FindOptions,
|
||||||
type FindResult,
|
type FindResult,
|
||||||
type Hierarchy,
|
type Hierarchy,
|
||||||
type MeasureClient,
|
|
||||||
type MeasureDoneOperation,
|
|
||||||
type ModelDb,
|
type ModelDb,
|
||||||
type Ref,
|
type Ref,
|
||||||
type SearchOptions,
|
type SearchOptions,
|
||||||
@ -65,7 +63,7 @@ export type PresentationMiddlewareCreator = (client: Client, next?: Presentation
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface PresentationPipeline extends MeasureClient, Exclude<PresentationMiddleware, 'next'> {
|
export interface PresentationPipeline extends Client, Exclude<PresentationMiddleware, 'next'> {
|
||||||
close: () => Promise<void>
|
close: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +73,7 @@ export interface PresentationPipeline extends MeasureClient, Exclude<Presentatio
|
|||||||
export class PresentationPipelineImpl implements PresentationPipeline {
|
export class PresentationPipelineImpl implements PresentationPipeline {
|
||||||
private head: PresentationMiddleware | undefined
|
private head: PresentationMiddleware | undefined
|
||||||
|
|
||||||
private constructor (readonly client: MeasureClient) {}
|
private constructor (readonly client: Client) {}
|
||||||
|
|
||||||
getHierarchy (): Hierarchy {
|
getHierarchy (): Hierarchy {
|
||||||
return this.client.getHierarchy()
|
return this.client.getHierarchy()
|
||||||
@ -89,11 +87,7 @@ export class PresentationPipelineImpl implements PresentationPipeline {
|
|||||||
await this.head?.notifyTx(...tx)
|
await this.head?.notifyTx(...tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
static create (client: Client, constructors: PresentationMiddlewareCreator[]): PresentationPipeline {
|
||||||
return await this.client.measure(operationName)
|
|
||||||
}
|
|
||||||
|
|
||||||
static create (client: MeasureClient, constructors: PresentationMiddlewareCreator[]): PresentationPipeline {
|
|
||||||
const pipeline = new PresentationPipelineImpl(client)
|
const pipeline = new PresentationPipelineImpl(client)
|
||||||
pipeline.head = pipeline.buildChain(constructors)
|
pipeline.head = pipeline.buildChain(constructors)
|
||||||
return pipeline
|
return pipeline
|
||||||
|
@ -14,26 +14,64 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { type Mixin, type Class, type Ref } from '@hcengineering/core'
|
import {
|
||||||
|
type Class,
|
||||||
|
type Client,
|
||||||
|
type Doc,
|
||||||
|
type DocumentQuery,
|
||||||
|
type FindOptions,
|
||||||
|
type FindResult,
|
||||||
|
type Mixin,
|
||||||
|
type Ref,
|
||||||
|
type SearchOptions,
|
||||||
|
type SearchQuery,
|
||||||
|
type SearchResult,
|
||||||
|
type Tx,
|
||||||
|
type TxResult,
|
||||||
|
type WithLookup
|
||||||
|
} from '@hcengineering/core'
|
||||||
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
|
import type { Asset, IntlString, Metadata, Plugin, StatusCode } from '@hcengineering/platform'
|
||||||
import { plugin } from '@hcengineering/platform'
|
import { plugin } from '@hcengineering/platform'
|
||||||
import { type ComponentExtensionId } from '@hcengineering/ui'
|
import { type ComponentExtensionId } from '@hcengineering/ui'
|
||||||
import { type PresentationMiddlewareFactory } from './pipeline'
|
import { type PresentationMiddlewareFactory } from './pipeline'
|
||||||
|
import type { PreviewConfig } from './preview'
|
||||||
import {
|
import {
|
||||||
type ComponentPointExtension,
|
type ComponentPointExtension,
|
||||||
type DocRules,
|
|
||||||
type DocCreateExtension,
|
type DocCreateExtension,
|
||||||
|
type DocRules,
|
||||||
type FilePreviewExtension,
|
type FilePreviewExtension,
|
||||||
type ObjectSearchCategory,
|
type InstantTransactions,
|
||||||
type InstantTransactions
|
type ObjectSearchCategory
|
||||||
} from './types'
|
} from './types'
|
||||||
import type { PreviewConfig } from './preview'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export const presentationId = 'presentation' as Plugin
|
export const presentationId = 'presentation' as Plugin
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface ClientHook {
|
||||||
|
findAll: <T extends Doc>(
|
||||||
|
client: Client,
|
||||||
|
_class: Ref<Class<T>>,
|
||||||
|
query: DocumentQuery<T>,
|
||||||
|
options?: FindOptions<T>
|
||||||
|
) => Promise<FindResult<T>>
|
||||||
|
|
||||||
|
findOne: <T extends Doc>(
|
||||||
|
client: Client,
|
||||||
|
_class: Ref<Class<T>>,
|
||||||
|
query: DocumentQuery<T>,
|
||||||
|
options?: FindOptions<T>
|
||||||
|
) => Promise<WithLookup<T> | undefined>
|
||||||
|
|
||||||
|
tx: (client: Client, tx: Tx) => Promise<TxResult>
|
||||||
|
|
||||||
|
searchFulltext: (client: Client, query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
|
||||||
|
}
|
||||||
|
|
||||||
export default plugin(presentationId, {
|
export default plugin(presentationId, {
|
||||||
class: {
|
class: {
|
||||||
ObjectSearchCategory: '' as Ref<Class<ObjectSearchCategory>>,
|
ObjectSearchCategory: '' as Ref<Class<ObjectSearchCategory>>,
|
||||||
@ -95,7 +133,8 @@ export default plugin(presentationId, {
|
|||||||
CollaboratorApiUrl: '' as Metadata<string>,
|
CollaboratorApiUrl: '' as Metadata<string>,
|
||||||
Token: '' as Metadata<string>,
|
Token: '' as Metadata<string>,
|
||||||
FrontUrl: '' as Asset,
|
FrontUrl: '' as Asset,
|
||||||
PreviewConfig: '' as Metadata<PreviewConfig | undefined>
|
PreviewConfig: '' as Metadata<PreviewConfig | undefined>,
|
||||||
|
ClientHook: '' as Metadata<ClientHook>
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
FileTooLarge: '' as StatusCode
|
FileTooLarge: '' as StatusCode
|
||||||
|
@ -33,8 +33,6 @@ import core, {
|
|||||||
type FindOptions,
|
type FindOptions,
|
||||||
type FindResult,
|
type FindResult,
|
||||||
type Hierarchy,
|
type Hierarchy,
|
||||||
type MeasureClient,
|
|
||||||
type MeasureDoneOperation,
|
|
||||||
type Mixin,
|
type Mixin,
|
||||||
type Obj,
|
type Obj,
|
||||||
type Blob as PlatformBlob,
|
type Blob as PlatformBlob,
|
||||||
@ -65,7 +63,7 @@ export { reduceCalls } from '@hcengineering/core'
|
|||||||
|
|
||||||
let liveQuery: LQ
|
let liveQuery: LQ
|
||||||
let rawLiveQuery: LQ
|
let rawLiveQuery: LQ
|
||||||
let client: TxOperations & MeasureClient & OptimisticTxes
|
let client: TxOperations & Client & OptimisticTxes
|
||||||
let pipeline: PresentationPipeline
|
let pipeline: PresentationPipeline
|
||||||
|
|
||||||
const txListeners: Array<(...tx: Tx[]) => void> = []
|
const txListeners: Array<(...tx: Tx[]) => void> = []
|
||||||
@ -95,16 +93,15 @@ export interface OptimisticTxes {
|
|||||||
pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>>
|
pendingCreatedDocs: Writable<Record<Ref<Doc>, boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
class UIClient extends TxOperations implements Client, MeasureClient, OptimisticTxes {
|
class UIClient extends TxOperations implements Client, OptimisticTxes {
|
||||||
|
hook = getMetadata(plugin.metadata.ClientHook)
|
||||||
constructor (
|
constructor (
|
||||||
client: MeasureClient,
|
client: Client,
|
||||||
private readonly liveQuery: Client
|
private readonly liveQuery: Client
|
||||||
) {
|
) {
|
||||||
super(client, getCurrentAccount()._id)
|
super(client, getCurrentAccount()._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
afterMeasure: Tx[] = []
|
|
||||||
measureOp?: MeasureDoneOperation
|
|
||||||
protected pendingTxes = new Set<Ref<Tx>>()
|
protected pendingTxes = new Set<Ref<Tx>>()
|
||||||
protected _pendingCreatedDocs = writable<Record<Ref<Doc>, boolean>>({})
|
protected _pendingCreatedDocs = writable<Record<Ref<Doc>, boolean>>({})
|
||||||
|
|
||||||
@ -113,34 +110,30 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
|
|||||||
}
|
}
|
||||||
|
|
||||||
async doNotify (...tx: Tx[]): Promise<void> {
|
async doNotify (...tx: Tx[]): Promise<void> {
|
||||||
if (this.measureOp !== undefined) {
|
const pending = get(this._pendingCreatedDocs)
|
||||||
this.afterMeasure.push(...tx)
|
let pendingUpdated = false
|
||||||
} else {
|
tx.forEach((t) => {
|
||||||
const pending = get(this._pendingCreatedDocs)
|
if (this.pendingTxes.has(t._id)) {
|
||||||
let pendingUpdated = false
|
this.pendingTxes.delete(t._id)
|
||||||
tx.forEach((t) => {
|
|
||||||
if (this.pendingTxes.has(t._id)) {
|
|
||||||
this.pendingTxes.delete(t._id)
|
|
||||||
|
|
||||||
// Only CUD tx can be pending now
|
// Only CUD tx can be pending now
|
||||||
const innerTx = TxProcessor.extractTx(t) as TxCUD<Doc>
|
const innerTx = TxProcessor.extractTx(t) as TxCUD<Doc>
|
||||||
|
|
||||||
if (innerTx._class === core.class.TxCreateDoc) {
|
if (innerTx._class === core.class.TxCreateDoc) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||||
delete pending[innerTx.objectId]
|
delete pending[innerTx.objectId]
|
||||||
pendingUpdated = true
|
pendingUpdated = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
if (pendingUpdated) {
|
|
||||||
this._pendingCreatedDocs.set(pending)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
// We still want to notify about all transactions because there might be queries created after
|
if (pendingUpdated) {
|
||||||
// the early applied transaction
|
this._pendingCreatedDocs.set(pending)
|
||||||
// For old queries there's a check anyway that prevents the same document from being added twice
|
|
||||||
await this.provideNotify(...tx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We still want to notify about all transactions because there might be queries created after
|
||||||
|
// the early applied transaction
|
||||||
|
// For old queries there's a check anyway that prevents the same document from being added twice
|
||||||
|
await this.provideNotify(...tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async provideNotify (...tx: Tx[]): Promise<void> {
|
private async provideNotify (...tx: Tx[]): Promise<void> {
|
||||||
@ -165,6 +158,9 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
|
|||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
options?: FindOptions<T>
|
options?: FindOptions<T>
|
||||||
): Promise<FindResult<T>> {
|
): Promise<FindResult<T>> {
|
||||||
|
if (this.hook !== undefined) {
|
||||||
|
return await this.hook.findAll(this.liveQuery, _class, query, options)
|
||||||
|
}
|
||||||
return await this.liveQuery.findAll(_class, query, options)
|
return await this.liveQuery.findAll(_class, query, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,12 +169,17 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
|
|||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
options?: FindOptions<T>
|
options?: FindOptions<T>
|
||||||
): Promise<WithLookup<T> | undefined> {
|
): Promise<WithLookup<T> | undefined> {
|
||||||
|
if (this.hook !== undefined) {
|
||||||
|
return await this.hook.findOne(this.liveQuery, _class, query, options)
|
||||||
|
}
|
||||||
return await this.liveQuery.findOne(_class, query, options)
|
return await this.liveQuery.findOne(_class, query, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
override async tx (tx: Tx): Promise<TxResult> {
|
override async tx (tx: Tx): Promise<TxResult> {
|
||||||
void this.notifyEarly(tx)
|
void this.notifyEarly(tx)
|
||||||
|
if (this.hook !== undefined) {
|
||||||
|
return await this.hook.tx(this.client, tx)
|
||||||
|
}
|
||||||
return await this.client.tx(tx)
|
return await this.client.tx(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,39 +222,24 @@ class UIClient extends TxOperations implements Client, MeasureClient, Optimistic
|
|||||||
}
|
}
|
||||||
|
|
||||||
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||||
return await this.client.searchFulltext(query, options)
|
if (this.hook !== undefined) {
|
||||||
}
|
return await this.hook.searchFulltext(this.client, query, options)
|
||||||
|
|
||||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
|
||||||
// return await (this.client as MeasureClient).measure(operationName)
|
|
||||||
const mop = await (this.client as MeasureClient).measure(operationName)
|
|
||||||
this.measureOp = mop
|
|
||||||
return async () => {
|
|
||||||
const result = await mop()
|
|
||||||
this.measureOp = undefined
|
|
||||||
if (this.afterMeasure.length > 0) {
|
|
||||||
const txes = this.afterMeasure
|
|
||||||
this.afterMeasure = []
|
|
||||||
for (const tx of txes) {
|
|
||||||
await this.doNotify(tx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
return await this.client.searchFulltext(query, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function getClient (): TxOperations & MeasureClient & OptimisticTxes {
|
export function getClient (): TxOperations & Client & OptimisticTxes {
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export async function setClient (_client: MeasureClient): Promise<void> {
|
export async function setClient (_client: Client): Promise<void> {
|
||||||
if (liveQuery !== undefined) {
|
if (liveQuery !== undefined) {
|
||||||
await liveQuery.close()
|
await liveQuery.close()
|
||||||
}
|
}
|
||||||
@ -276,6 +262,7 @@ export async function setClient (_client: MeasureClient): Promise<void> {
|
|||||||
liveQuery = new LQ(pipeline)
|
liveQuery = new LQ(pipeline)
|
||||||
|
|
||||||
const uiClient = new UIClient(pipeline, liveQuery)
|
const uiClient = new UIClient(pipeline, liveQuery)
|
||||||
|
|
||||||
client = uiClient
|
client = uiClient
|
||||||
|
|
||||||
_client.notify = (...tx: Tx[]) => {
|
_client.notify = (...tx: Tx[]) => {
|
||||||
@ -285,7 +272,6 @@ export async function setClient (_client: MeasureClient): Promise<void> {
|
|||||||
await refreshClient(true)
|
await refreshClient(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
@ -98,7 +98,6 @@ FulltextStorage & {
|
|||||||
searchFulltext: async (query: SearchQuery, options: SearchOptions): Promise<SearchResult> => {
|
searchFulltext: async (query: SearchQuery, options: SearchOptions): Promise<SearchResult> => {
|
||||||
return { docs: [] }
|
return { docs: [] }
|
||||||
},
|
},
|
||||||
measure: async () => async () => ({ time: 0, serverTime: 0 }),
|
|
||||||
sendForceClose: async () => {}
|
sendForceClose: async () => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,12 +14,12 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DirectMessage } from '@hcengineering/chunter'
|
import { DirectMessage } from '@hcengineering/chunter'
|
||||||
import { Avatar, CombineAvatars, personAccountByIdStore } from '@hcengineering/contact-resources'
|
|
||||||
import { Icon, IconSize } from '@hcengineering/ui'
|
|
||||||
import contact, { Person, PersonAccount } from '@hcengineering/contact'
|
import contact, { Person, PersonAccount } from '@hcengineering/contact'
|
||||||
import { classIcon } from '@hcengineering/view-resources'
|
import { Avatar, CombineAvatars, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
|
||||||
import { Account, IdMap } from '@hcengineering/core'
|
import { Account, IdMap } from '@hcengineering/core'
|
||||||
|
import { getClient } from '@hcengineering/presentation'
|
||||||
|
import { Icon, IconSize } from '@hcengineering/ui'
|
||||||
|
import { classIcon } from '@hcengineering/view-resources'
|
||||||
|
|
||||||
import chunter from '../plugin'
|
import chunter from '../plugin'
|
||||||
import { getDmPersons } from '../utils'
|
import { getDmPersons } from '../utils'
|
||||||
@ -33,10 +33,11 @@
|
|||||||
|
|
||||||
let persons: Person[] = []
|
let persons: Person[] = []
|
||||||
|
|
||||||
$: value &&
|
$: if (value !== undefined) {
|
||||||
getDmPersons(client, value).then((res) => {
|
void getDmPersons(client, value, $personByIdStore).then((res) => {
|
||||||
persons = res
|
persons = res
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
let avatarSize = size
|
let avatarSize = size
|
||||||
|
|
||||||
|
@ -13,15 +13,15 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||||
import { Analytics } from '@hcengineering/analytics'
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
import { createEventDispatcher } from 'svelte'
|
|
||||||
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
|
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||||
import { Class, Doc, generateId, getCurrentAccount, Ref } from '@hcengineering/core'
|
|
||||||
import { createQuery, DraftController, draftsStore, getClient, isSpace } from '@hcengineering/presentation'
|
|
||||||
import chunter, { ChatMessage, ThreadMessage } from '@hcengineering/chunter'
|
import chunter, { ChatMessage, ThreadMessage } from '@hcengineering/chunter'
|
||||||
import { PersonAccount } from '@hcengineering/contact'
|
import { PersonAccount } from '@hcengineering/contact'
|
||||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
import { Class, Doc, generateId, getCurrentAccount, Ref, type CommitResult } from '@hcengineering/core'
|
||||||
|
import { createQuery, DraftController, draftsStore, getClient, isSpace } from '@hcengineering/presentation'
|
||||||
import { EmptyMarkup } from '@hcengineering/text-editor'
|
import { EmptyMarkup } from '@hcengineering/text-editor'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
export let object: Doc
|
export let object: Doc
|
||||||
export let chatMessage: ChatMessage | undefined = undefined
|
export let chatMessage: ChatMessage | undefined = undefined
|
||||||
@ -100,32 +100,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleCreate (event: CustomEvent, _id: Ref<ChatMessage>): Promise<void> {
|
async function handleCreate (event: CustomEvent, _id: Ref<ChatMessage>): Promise<void> {
|
||||||
const doneOp = getClient().measure(`chunter.create.${_class} ${object._class}`)
|
|
||||||
try {
|
try {
|
||||||
await createMessage(event, _id)
|
const res = await createMessage(event, _id, `chunter.create.${_class} ${object._class}`)
|
||||||
|
|
||||||
const d1 = Date.now()
|
console.log(`create.${_class} measure`, res.serverTime, res.time)
|
||||||
void (await doneOp)().then((res) => {
|
|
||||||
console.log(`create.${_class} measure`, res, Date.now() - d1)
|
|
||||||
})
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
void (await doneOp)()
|
|
||||||
Analytics.handleError(err)
|
Analytics.handleError(err)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEdit (event: CustomEvent): Promise<void> {
|
async function handleEdit (event: CustomEvent): Promise<void> {
|
||||||
const doneOp = getClient().measure(`chunter.edit.${_class} ${object._class}`)
|
|
||||||
try {
|
try {
|
||||||
await editMessage(event)
|
await editMessage(event)
|
||||||
|
|
||||||
const d1 = Date.now()
|
|
||||||
void (await doneOp)().then((res) => {
|
|
||||||
console.log(`edit.${_class} measure`, res, Date.now() - d1)
|
|
||||||
})
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
void (await doneOp)()
|
|
||||||
Analytics.handleError(err)
|
Analytics.handleError(err)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -148,9 +136,9 @@
|
|||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createMessage (event: CustomEvent, _id: Ref<ChatMessage>): Promise<void> {
|
async function createMessage (event: CustomEvent, _id: Ref<ChatMessage>, msg: string): Promise<CommitResult> {
|
||||||
const { message, attachments } = event.detail
|
const { message, attachments } = event.detail
|
||||||
const operations = client.apply(_id)
|
const operations = client.apply(_id, msg)
|
||||||
|
|
||||||
if (_class === chunter.class.ThreadMessage) {
|
if (_class === chunter.class.ThreadMessage) {
|
||||||
const parentMessage = object as ActivityMessage
|
const parentMessage = object as ActivityMessage
|
||||||
@ -188,7 +176,7 @@
|
|||||||
_id
|
_id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
await operations.commit()
|
return await operations.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editMessage (event: CustomEvent): Promise<void> {
|
async function editMessage (event: CustomEvent): Promise<void> {
|
||||||
|
@ -13,17 +13,17 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Class, Doc, getCurrentAccount, groupByArray, Ref, SortingOrder } from '@hcengineering/core'
|
|
||||||
import notification, { DocNotifyContext } from '@hcengineering/notification'
|
|
||||||
import { createQuery, getClient, LiveQuery } from '@hcengineering/presentation'
|
|
||||||
import activity from '@hcengineering/activity'
|
import activity from '@hcengineering/activity'
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { Class, Doc, getCurrentAccount, groupByArray, reduceCalls, Ref, SortingOrder } from '@hcengineering/core'
|
||||||
import { Action } from '@hcengineering/ui'
|
import notification, { DocNotifyContext } from '@hcengineering/notification'
|
||||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||||
|
import { IntlString } from '@hcengineering/platform'
|
||||||
|
import { createQuery, getClient, LiveQuery } from '@hcengineering/presentation'
|
||||||
|
import { Action } from '@hcengineering/ui'
|
||||||
|
|
||||||
|
import chunter from '../../../plugin'
|
||||||
import { ChatGroup, ChatNavGroupModel } from '../types'
|
import { ChatGroup, ChatNavGroupModel } from '../types'
|
||||||
import ChatNavSection from './ChatNavSection.svelte'
|
import ChatNavSection from './ChatNavSection.svelte'
|
||||||
import chunter from '../../../plugin'
|
|
||||||
|
|
||||||
export let object: Doc | undefined
|
export let object: Doc | undefined
|
||||||
export let model: ChatNavGroupModel
|
export let model: ChatNavGroupModel
|
||||||
@ -68,7 +68,11 @@
|
|||||||
|
|
||||||
$: loadObjects(contexts)
|
$: loadObjects(contexts)
|
||||||
|
|
||||||
$: void getSections(objectsByClass, model, shouldPushObject ? object : undefined).then((res) => {
|
$: pushObj = shouldPushObject ? object : undefined
|
||||||
|
|
||||||
|
const getPushObj = () => pushObj as Doc
|
||||||
|
|
||||||
|
$: void getSections(objectsByClass, model, pushObj, getPushObj, (res) => {
|
||||||
sections = res
|
sections = res
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -112,63 +116,68 @@
|
|||||||
return 'activity'
|
return 'activity'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSections (
|
const getSections = reduceCalls(
|
||||||
objectsByClass: Map<Ref<Class<Doc>>, Doc[]>,
|
async (
|
||||||
model: ChatNavGroupModel,
|
objectsByClass: Map<Ref<Class<Doc>>, Doc[]>,
|
||||||
object: Doc | undefined
|
model: ChatNavGroupModel,
|
||||||
): Promise<Section[]> {
|
object: { _id: Doc['_id'], _class: Doc['_class'] } | undefined,
|
||||||
const result: Section[] = []
|
getPushObj: () => Doc,
|
||||||
|
handler: (result: Section[]) => void
|
||||||
|
): Promise<void> => {
|
||||||
|
const result: Section[] = []
|
||||||
|
|
||||||
if (!model.wrap) {
|
if (!model.wrap) {
|
||||||
result.push({
|
result.push({
|
||||||
id: model.id,
|
id: model.id,
|
||||||
objects: Array.from(objectsByClass.values()).flat(),
|
objects: Array.from(objectsByClass.values()).flat(),
|
||||||
label: model.label ?? chunter.string.Channels
|
label: model.label ?? chunter.string.Channels
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
handler(result)
|
||||||
}
|
return
|
||||||
|
|
||||||
let isObjectPushed = false
|
|
||||||
|
|
||||||
if (
|
|
||||||
Array.from(objectsByClass.values())
|
|
||||||
.flat()
|
|
||||||
.some((o) => o._id === object?._id)
|
|
||||||
) {
|
|
||||||
isObjectPushed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [_class, objects] of objectsByClass.entries()) {
|
|
||||||
const clazz = hierarchy.getClass(_class)
|
|
||||||
const sectionObjects = [...objects]
|
|
||||||
|
|
||||||
if (object && _class === object._class && !objects.some(({ _id }) => _id === object._id)) {
|
|
||||||
isObjectPushed = true
|
|
||||||
sectionObjects.push(object)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.push({
|
let isObjectPushed = false
|
||||||
id: _class,
|
|
||||||
_class,
|
if (
|
||||||
objects: sectionObjects,
|
Array.from(objectsByClass.values())
|
||||||
label: clazz.pluralLabel ?? clazz.label
|
.flat()
|
||||||
})
|
.some((o) => o._id === object?._id)
|
||||||
|
) {
|
||||||
|
isObjectPushed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [_class, objects] of objectsByClass.entries()) {
|
||||||
|
const clazz = hierarchy.getClass(_class)
|
||||||
|
const sectionObjects = [...objects]
|
||||||
|
|
||||||
|
if (object !== undefined && _class === object._class && !objects.some(({ _id }) => _id === object._id)) {
|
||||||
|
isObjectPushed = true
|
||||||
|
sectionObjects.push(getPushObj())
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
id: _class,
|
||||||
|
_class,
|
||||||
|
objects: sectionObjects,
|
||||||
|
label: clazz.pluralLabel ?? clazz.label
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isObjectPushed && object !== undefined) {
|
||||||
|
const clazz = hierarchy.getClass(object._class)
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
id: object._id,
|
||||||
|
_class: object._class,
|
||||||
|
objects: [getPushObj()],
|
||||||
|
label: clazz.pluralLabel ?? clazz.label
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(result.sort((s1, s2) => s1.label.localeCompare(s2.label)))
|
||||||
}
|
}
|
||||||
|
)
|
||||||
if (!isObjectPushed && object) {
|
|
||||||
const clazz = hierarchy.getClass(object._class)
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
id: object._id,
|
|
||||||
_class: object._class,
|
|
||||||
objects: [object],
|
|
||||||
label: clazz.pluralLabel ?? clazz.label
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.sort((s1, s2) => s1.label.localeCompare(s2.label))
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSectionActions (section: Section, contexts: DocNotifyContext[]): Action[] {
|
function getSectionActions (section: Section, contexts: DocNotifyContext[]): Action[] {
|
||||||
if (model.getActionsFn === undefined) {
|
if (model.getActionsFn === undefined) {
|
||||||
|
@ -13,20 +13,20 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Doc, Ref } from '@hcengineering/core'
|
import contact from '@hcengineering/contact'
|
||||||
|
import { personAccountByIdStore, statusByUserStore } from '@hcengineering/contact-resources'
|
||||||
|
import { Doc, reduceCalls, Ref } from '@hcengineering/core'
|
||||||
import { DocNotifyContext } from '@hcengineering/notification'
|
import { DocNotifyContext } from '@hcengineering/notification'
|
||||||
|
import { getResource, IntlString, translate } from '@hcengineering/platform'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import ui, { Action, AnySvelteComponent, IconSize, ModernButton, NavGroup } from '@hcengineering/ui'
|
import ui, { Action, AnySvelteComponent, IconSize, ModernButton, NavGroup } from '@hcengineering/ui'
|
||||||
import { getDocTitle } from '@hcengineering/view-resources'
|
|
||||||
import contact from '@hcengineering/contact'
|
|
||||||
import { getResource, IntlString, translate } from '@hcengineering/platform'
|
|
||||||
import view from '@hcengineering/view'
|
import view from '@hcengineering/view'
|
||||||
import { personAccountByIdStore, statusByUserStore } from '@hcengineering/contact-resources'
|
import { getDocTitle } from '@hcengineering/view-resources'
|
||||||
|
|
||||||
import ChatNavItem from './ChatNavItem.svelte'
|
|
||||||
import chunter from '../../../plugin'
|
import chunter from '../../../plugin'
|
||||||
|
import { getChannelName, getObjectIcon } from '../../../utils'
|
||||||
import { ChatNavItemModel, SortFnOptions } from '../types'
|
import { ChatNavItemModel, SortFnOptions } from '../types'
|
||||||
import { getObjectIcon, getChannelName } from '../../../utils'
|
import ChatNavItem from './ChatNavItem.svelte'
|
||||||
|
|
||||||
export let id: string
|
export let id: string
|
||||||
export let header: IntlString
|
export let header: IntlString
|
||||||
@ -47,7 +47,7 @@
|
|||||||
let canShowMore = false
|
let canShowMore = false
|
||||||
let isShownMore = false
|
let isShownMore = false
|
||||||
|
|
||||||
$: void getChatNavItems(objects).then((res) => {
|
$: void getChatNavItems(objects, (res) => {
|
||||||
items = res
|
items = res
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -60,44 +60,46 @@
|
|||||||
|
|
||||||
$: visibleItems = getVisibleItems(canShowMore, isShownMore, maxItems, sortedItems, objectId)
|
$: visibleItems = getVisibleItems(canShowMore, isShownMore, maxItems, sortedItems, objectId)
|
||||||
|
|
||||||
async function getChatNavItems (objects: Doc[]): Promise<ChatNavItemModel[]> {
|
const getChatNavItems = reduceCalls(
|
||||||
const items: ChatNavItemModel[] = []
|
async (objects: Doc[], handler: (items: ChatNavItemModel[]) => void): Promise<void> => {
|
||||||
|
const items: ChatNavItemModel[] = []
|
||||||
|
|
||||||
for (const object of objects) {
|
for (const object of objects) {
|
||||||
const { _class } = object
|
const { _class } = object
|
||||||
const iconMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIcon)
|
const iconMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIcon)
|
||||||
const titleIntl = client.getHierarchy().getClass(_class).label
|
const titleIntl = client.getHierarchy().getClass(_class).label
|
||||||
|
|
||||||
const isPerson = hierarchy.isDerived(_class, contact.class.Person)
|
const isPerson = hierarchy.isDerived(_class, contact.class.Person)
|
||||||
const isDocChat = !hierarchy.isDerived(_class, chunter.class.ChunterSpace)
|
const isDocChat = !hierarchy.isDerived(_class, chunter.class.ChunterSpace)
|
||||||
const isDirect = hierarchy.isDerived(_class, chunter.class.DirectMessage)
|
const isDirect = hierarchy.isDerived(_class, chunter.class.DirectMessage)
|
||||||
|
|
||||||
const iconSize: IconSize = isDirect || isPerson ? 'x-small' : 'small'
|
const iconSize: IconSize = isDirect || isPerson ? 'x-small' : 'small'
|
||||||
|
|
||||||
let icon: AnySvelteComponent | undefined = undefined
|
let icon: AnySvelteComponent | undefined = undefined
|
||||||
|
|
||||||
if (iconMixin?.component) {
|
if (iconMixin?.component) {
|
||||||
icon = await getResource(iconMixin.component)
|
icon = await getResource(iconMixin.component)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasId = hierarchy.classHierarchyMixin(object._class, view.mixin.ObjectIdentifier) !== undefined
|
||||||
|
const showDescription = hasId && isDocChat && !isPerson
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
id: object._id,
|
||||||
|
object,
|
||||||
|
title: (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {})),
|
||||||
|
description: showDescription ? await getDocTitle(client, object._id, object._class, object) : undefined,
|
||||||
|
icon: icon ?? getObjectIcon(_class),
|
||||||
|
iconProps: { showStatus: true },
|
||||||
|
iconSize,
|
||||||
|
withIconBackground: !isDirect && !isPerson,
|
||||||
|
isSecondary: isDocChat && !isPerson
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasId = hierarchy.classHierarchyMixin(object._class, view.mixin.ObjectIdentifier) !== undefined
|
handler(items)
|
||||||
const showDescription = hasId && isDocChat && !isPerson
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
id: object._id,
|
|
||||||
object,
|
|
||||||
title: (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {})),
|
|
||||||
description: showDescription ? await getDocTitle(client, object._id, object._class, object) : undefined,
|
|
||||||
icon: icon ?? getObjectIcon(_class),
|
|
||||||
iconProps: { showStatus: true },
|
|
||||||
iconSize,
|
|
||||||
withIconBackground: !isDirect && !isPerson,
|
|
||||||
isSecondary: isDocChat && !isPerson
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
function onShowMore (): void {
|
function onShowMore (): void {
|
||||||
isShownMore = !isShownMore
|
isShownMore = !isShownMore
|
||||||
|
@ -110,7 +110,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Scroller shrink>
|
<Scroller shrink>
|
||||||
{#each chatNavGroupModels as model}
|
{#each chatNavGroupModels as model (model.id)}
|
||||||
<ChatNavGroup {object} {model} on:select />
|
<ChatNavGroup {object} {model} on:select />
|
||||||
{/each}
|
{/each}
|
||||||
</Scroller>
|
</Scroller>
|
||||||
|
@ -304,8 +304,7 @@ function getPinnedActions (contexts: DocNotifyContext[]): Action[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function unpinAllChannels (contexts: DocNotifyContext[]): Promise<void> {
|
async function unpinAllChannels (contexts: DocNotifyContext[]): Promise<void> {
|
||||||
const doneOp = await getClient().measure('unpinAllChannels')
|
const ops = getClient().apply(generateId(), 'unpinAllChannels')
|
||||||
const ops = getClient().apply(generateId())
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const context of contexts) {
|
for (const context of contexts) {
|
||||||
@ -313,7 +312,6 @@ async function unpinAllChannels (contexts: DocNotifyContext[]): Promise<void> {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -404,8 +402,7 @@ export function loadSavedAttachments (): void {
|
|||||||
export async function removeActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
|
export async function removeActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
|
||||||
const client = InboxNotificationsClientImpl.getClient()
|
const client = InboxNotificationsClientImpl.getClient()
|
||||||
const notificationsByContext = get(client.inboxNotificationsByContext)
|
const notificationsByContext = get(client.inboxNotificationsByContext)
|
||||||
const doneOp = await getClient().measure('removeActivityChannels')
|
const ops = getClient().apply(generateId(), 'removeActivityChannels')
|
||||||
const ops = getClient().apply(generateId())
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const context of contexts) {
|
for (const context of contexts) {
|
||||||
@ -418,15 +415,13 @@ export async function removeActivityChannels (contexts: DocNotifyContext[]): Pro
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
|
export async function readActivityChannels (contexts: DocNotifyContext[]): Promise<void> {
|
||||||
const client = InboxNotificationsClientImpl.getClient()
|
const client = InboxNotificationsClientImpl.getClient()
|
||||||
const notificationsByContext = get(client.inboxNotificationsByContext)
|
const notificationsByContext = get(client.inboxNotificationsByContext)
|
||||||
const doneOp = await getClient().measure('readActivityChannels')
|
const ops = getClient().apply(generateId(), 'readActivityChannels')
|
||||||
const ops = getClient().apply(generateId())
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const context of contexts) {
|
for (const context of contexts) {
|
||||||
@ -441,6 +436,5 @@ export async function readActivityChannels (contexts: DocNotifyContext[]): Promi
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,8 @@ import {
|
|||||||
type IdMap,
|
type IdMap,
|
||||||
type Ref,
|
type Ref,
|
||||||
type Space,
|
type Space,
|
||||||
type Timestamp
|
type Timestamp,
|
||||||
|
type WithLookup
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
|
import notification, { type DocNotifyContext, type InboxNotification } from '@hcengineering/notification'
|
||||||
import {
|
import {
|
||||||
@ -163,7 +164,11 @@ async function getDmAccounts (client: Client, space?: Space): Promise<PersonAcco
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDmPersons (client: Client, space: Space): Promise<Person[]> {
|
export async function getDmPersons (
|
||||||
|
client: Client,
|
||||||
|
space: Space,
|
||||||
|
personsMap: Map<Ref<WithLookup<Person>>, WithLookup<Person>>
|
||||||
|
): Promise<Person[]> {
|
||||||
const personAccounts: PersonAccount[] = await getDmAccounts(client, space)
|
const personAccounts: PersonAccount[] = await getDmAccounts(client, space)
|
||||||
const me = getCurrentAccount() as PersonAccount
|
const me = getCurrentAccount() as PersonAccount
|
||||||
const persons: Person[] = []
|
const persons: Person[] = []
|
||||||
@ -172,7 +177,7 @@ export async function getDmPersons (client: Client, space: Space): Promise<Perso
|
|||||||
let myPerson: Person | undefined
|
let myPerson: Person | undefined
|
||||||
|
|
||||||
for (const personRef of personRefs) {
|
for (const personRef of personRefs) {
|
||||||
const person = await client.findOne(contact.class.Person, { _id: personRef })
|
const person = personsMap.get(personRef) ?? (await client.findOne(contact.class.Person, { _id: personRef }))
|
||||||
if (person === undefined) {
|
if (person === undefined) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -192,8 +197,12 @@ export async function getDmPersons (client: Client, space: Space): Promise<Perso
|
|||||||
return myPerson !== undefined ? [myPerson] : []
|
return myPerson !== undefined ? [myPerson] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DirectTitleProvider (client: Client, id: Ref<DirectMessage>): Promise<string> {
|
export async function DirectTitleProvider (
|
||||||
const direct = await client.findOne(chunter.class.DirectMessage, { _id: id })
|
client: Client,
|
||||||
|
id: Ref<DirectMessage>,
|
||||||
|
doc?: DirectMessage
|
||||||
|
): Promise<string> {
|
||||||
|
const direct = doc ?? (await client.findOne(chunter.class.DirectMessage, { _id: id }))
|
||||||
|
|
||||||
if (direct === undefined) {
|
if (direct === undefined) {
|
||||||
return ''
|
return ''
|
||||||
@ -202,8 +211,8 @@ export async function DirectTitleProvider (client: Client, id: Ref<DirectMessage
|
|||||||
return await getDmName(client, direct)
|
return await getDmName(client, direct)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ChannelTitleProvider (client: Client, id: Ref<Channel>): Promise<string> {
|
export async function ChannelTitleProvider (client: Client, id: Ref<Channel>, doc?: Channel): Promise<string> {
|
||||||
const channel = await client.findOne(chunter.class.Channel, { _id: id })
|
const channel = doc ?? (await client.findOne(chunter.class.Channel, { _id: id }))
|
||||||
|
|
||||||
if (channel === undefined) {
|
if (channel === undefined) {
|
||||||
return ''
|
return ''
|
||||||
|
@ -28,7 +28,6 @@ import core, {
|
|||||||
FindOptions,
|
FindOptions,
|
||||||
FindResult,
|
FindResult,
|
||||||
LoadModelResponse,
|
LoadModelResponse,
|
||||||
MeasureDoneOperation,
|
|
||||||
Ref,
|
Ref,
|
||||||
SearchOptions,
|
SearchOptions,
|
||||||
SearchQuery,
|
SearchQuery,
|
||||||
@ -524,26 +523,6 @@ class Connection implements ClientConnection {
|
|||||||
return await promise.promise
|
return await promise.promise
|
||||||
}
|
}
|
||||||
|
|
||||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
|
||||||
const dateNow = Date.now()
|
|
||||||
|
|
||||||
// Send measure-start
|
|
||||||
const mid = await this.sendRequest({
|
|
||||||
method: 'measure',
|
|
||||||
params: [operationName]
|
|
||||||
})
|
|
||||||
return async () => {
|
|
||||||
const serverTime: number = await this.sendRequest({
|
|
||||||
method: 'measure-done',
|
|
||||||
params: [operationName, mid]
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
time: Date.now() - dateNow,
|
|
||||||
serverTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadModel (last: Timestamp, hash?: string): Promise<Tx[] | LoadModelResponse> {
|
async loadModel (last: Timestamp, hash?: string): Promise<Tx[] | LoadModelResponse> {
|
||||||
return await this.sendRequest({ method: 'loadModel', params: [last, hash] })
|
return await this.sendRequest({ method: 'loadModel', params: [last, hash] })
|
||||||
}
|
}
|
||||||
|
@ -28,14 +28,7 @@ import core, {
|
|||||||
createClient,
|
createClient,
|
||||||
type ClientConnection
|
type ClientConnection
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import platform, {
|
import platform, { Severity, Status, getMetadata, getPlugins, setPlatformStatus } from '@hcengineering/platform'
|
||||||
Severity,
|
|
||||||
Status,
|
|
||||||
getMetadata,
|
|
||||||
getPlugins,
|
|
||||||
getResource,
|
|
||||||
setPlatformStatus
|
|
||||||
} from '@hcengineering/platform'
|
|
||||||
import { connect } from './connection'
|
import { connect } from './connection'
|
||||||
|
|
||||||
export { connect }
|
export { connect }
|
||||||
@ -88,7 +81,7 @@ export default async () => {
|
|||||||
): Promise<AccountClient> => {
|
): Promise<AccountClient> => {
|
||||||
const filterModel = getMetadata(clientPlugin.metadata.FilterModel) ?? false
|
const filterModel = getMetadata(clientPlugin.metadata.FilterModel) ?? false
|
||||||
|
|
||||||
let client = createClient(
|
const client = createClient(
|
||||||
(handler: TxHandler) => {
|
(handler: TxHandler) => {
|
||||||
const url = concatLink(endpoint, `/${token}`)
|
const url = concatLink(endpoint, `/${token}`)
|
||||||
|
|
||||||
@ -144,8 +137,6 @@ export default async () => {
|
|||||||
createModelPersistence(getWSFromToken(token)),
|
createModelPersistence(getWSFromToken(token)),
|
||||||
ctx
|
ctx
|
||||||
)
|
)
|
||||||
// Check if we had dev hook for client.
|
|
||||||
client = hookClient(client)
|
|
||||||
return await client
|
return await client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,24 +194,6 @@ function createModelPersistence (workspace: string): TxPersistenceStore | undefi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hookClient (client: Promise<AccountClient>): Promise<AccountClient> {
|
|
||||||
const hook = getMetadata(clientPlugin.metadata.ClientHook)
|
|
||||||
if (hook !== undefined) {
|
|
||||||
const hookProc = await getResource(hook)
|
|
||||||
const _client = client
|
|
||||||
client = new Promise((resolve, reject) => {
|
|
||||||
_client
|
|
||||||
.then((res) => {
|
|
||||||
resolve(hookProc(res))
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return await client
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWSFromToken (token: string): string {
|
function getWSFromToken (token: string): string {
|
||||||
const parts = token.split('.')
|
const parts = token.split('.')
|
||||||
|
|
||||||
|
@ -22,11 +22,6 @@ import { Metadata, plugin } from '@hcengineering/platform'
|
|||||||
*/
|
*/
|
||||||
export const clientId = 'client' as Plugin
|
export const clientId = 'client' as Plugin
|
||||||
|
|
||||||
/**
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
export type ClientHook = (client: AccountClient) => Promise<AccountClient>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -74,7 +69,6 @@ export type ClientFactory = (
|
|||||||
|
|
||||||
export default plugin(clientId, {
|
export default plugin(clientId, {
|
||||||
metadata: {
|
metadata: {
|
||||||
ClientHook: '' as Metadata<Resource<ClientHook>>,
|
|
||||||
ClientSocketFactory: '' as Metadata<ClientSocketFactory>,
|
ClientSocketFactory: '' as Metadata<ClientSocketFactory>,
|
||||||
FilterModel: '' as Metadata<boolean>,
|
FilterModel: '' as Metadata<boolean>,
|
||||||
ExtraPlugins: '' as Metadata<Plugin[]>,
|
ExtraPlugins: '' as Metadata<Plugin[]>,
|
||||||
|
@ -194,7 +194,7 @@ async function createControlledDoc (
|
|||||||
|
|
||||||
const success = await ops.commit()
|
const success = await ops.commit()
|
||||||
|
|
||||||
return { seqNumber, success }
|
return { seqNumber, success: success.result }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDocumentTemplate (
|
export async function createDocumentTemplate (
|
||||||
@ -327,7 +327,7 @@ export async function createDocumentTemplate (
|
|||||||
|
|
||||||
const success = await ops.commit()
|
const success = await ops.commit()
|
||||||
|
|
||||||
return { seqNumber, success }
|
return { seqNumber, success: success.result }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCollaborativeDocForDocument (
|
export function getCollaborativeDocForDocument (
|
||||||
|
@ -13,20 +13,15 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import core, {
|
import {
|
||||||
DOMAIN_MODEL,
|
DOMAIN_MODEL,
|
||||||
cutObjectArray,
|
cutObjectArray,
|
||||||
type Account,
|
|
||||||
type AccountClient,
|
|
||||||
type Class,
|
type Class,
|
||||||
type Client,
|
type Client,
|
||||||
type Doc,
|
type Doc,
|
||||||
type DocumentQuery,
|
type DocumentQuery,
|
||||||
type FindOptions,
|
type FindOptions,
|
||||||
type FindResult,
|
type FindResult,
|
||||||
type Hierarchy,
|
|
||||||
type MeasureDoneOperation,
|
|
||||||
type ModelDb,
|
|
||||||
type Ref,
|
type Ref,
|
||||||
type SearchOptions,
|
type SearchOptions,
|
||||||
type SearchQuery,
|
type SearchQuery,
|
||||||
@ -35,13 +30,10 @@ import core, {
|
|||||||
type TxResult,
|
type TxResult,
|
||||||
type WithLookup
|
type WithLookup
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { devModelId } from '@hcengineering/devmodel'
|
|
||||||
import { Builder } from '@hcengineering/model'
|
|
||||||
import { getMetadata, type IntlString, type Resources } from '@hcengineering/platform'
|
import { getMetadata, type IntlString, type Resources } from '@hcengineering/platform'
|
||||||
|
import { addTxListener } from '@hcengineering/presentation'
|
||||||
|
import type { ClientHook } from '@hcengineering/presentation/src/plugin'
|
||||||
import { testing } from '@hcengineering/ui'
|
import { testing } from '@hcengineering/ui'
|
||||||
import view from '@hcengineering/view'
|
|
||||||
import workbench from '@hcengineering/workbench'
|
|
||||||
import ModelView from './components/ModelView.svelte'
|
|
||||||
import devmodel from './plugin'
|
import devmodel from './plugin'
|
||||||
|
|
||||||
export interface TxWitHResult {
|
export interface TxWitHResult {
|
||||||
@ -57,48 +49,46 @@ export interface QueryWithResult {
|
|||||||
findOne: boolean
|
findOne: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModelClient implements AccountClient {
|
export class PresentationClientHook implements ClientHook {
|
||||||
notifyEnabled = true
|
notifyEnabled = true
|
||||||
constructor (readonly client: AccountClient) {
|
constructor () {
|
||||||
this.notifyEnabled = (localStorage.getItem('#platform.notification.logging') ?? 'true') === 'true'
|
this.notifyEnabled = (localStorage.getItem('#platform.notification.logging') ?? 'true') === 'true'
|
||||||
|
|
||||||
client.notify = (...tx) => {
|
addTxListener((...tx) => {
|
||||||
this.notify?.(...tx)
|
|
||||||
if (this.notifyEnabled) {
|
if (this.notifyEnabled) {
|
||||||
console.debug(
|
console.debug(
|
||||||
'devmodel# notify=>',
|
'devmodel# notify=>',
|
||||||
testing ? JSON.stringify(cutObjectArray(tx)).slice(0, 160) : tx.length === 1 ? tx[0] : tx
|
testing ? JSON.stringify(cutObjectArray(tx)).slice(0, 160) : tx.length === 1 ? tx[0] : tx
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stackLine (): string {
|
||||||
|
const stack = (new Error().stack ?? '').split('\n')
|
||||||
|
|
||||||
|
let candidate = ''
|
||||||
|
for (let l of stack) {
|
||||||
|
l = l.trim()
|
||||||
|
if (l.includes('.svelte')) {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
if (l.includes('plugins/') && !l.includes('devmodel-resources/') && l.includes('.ts') && candidate === '') {
|
||||||
|
candidate = l
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return candidate
|
||||||
|
|
||||||
async measure (operationName: string): Promise<MeasureDoneOperation> {
|
|
||||||
return await this.client.measure(operationName)
|
|
||||||
}
|
|
||||||
|
|
||||||
notify?: (...tx: Tx[]) => void
|
|
||||||
|
|
||||||
getHierarchy (): Hierarchy {
|
|
||||||
return this.client.getHierarchy()
|
|
||||||
}
|
|
||||||
|
|
||||||
getModel (): ModelDb {
|
|
||||||
return this.client.getModel()
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAccount (): Promise<Account> {
|
|
||||||
return await this.client.getAccount()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne<T extends Doc>(
|
async findOne<T extends Doc>(
|
||||||
|
client: Client,
|
||||||
_class: Ref<Class<T>>,
|
_class: Ref<Class<T>>,
|
||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
options?: FindOptions<T>
|
options?: FindOptions<T>
|
||||||
): Promise<WithLookup<T> | undefined> {
|
): Promise<WithLookup<T> | undefined> {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
const isModel = this.getHierarchy().findDomain(_class) === DOMAIN_MODEL
|
const isModel = client.getHierarchy().findDomain(_class) === DOMAIN_MODEL
|
||||||
const result = await this.client.findOne(_class, query, options)
|
const result = await client.findOne(_class, query, options)
|
||||||
if (this.notifyEnabled && !isModel) {
|
if (this.notifyEnabled && !isModel) {
|
||||||
console.debug(
|
console.debug(
|
||||||
'devmodel# findOne=>',
|
'devmodel# findOne=>',
|
||||||
@ -108,22 +98,24 @@ class ModelClient implements AccountClient {
|
|||||||
'result => ',
|
'result => ',
|
||||||
testing ? JSON.stringify(cutObjectArray(result)) : result,
|
testing ? JSON.stringify(cutObjectArray(result)) : result,
|
||||||
' =>model',
|
' =>model',
|
||||||
this.client.getModel(),
|
client.getModel(),
|
||||||
getMetadata(devmodel.metadata.DevModel),
|
getMetadata(devmodel.metadata.DevModel),
|
||||||
Date.now() - startTime
|
Date.now() - startTime,
|
||||||
|
this.stackLine()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll<T extends Doc>(
|
async findAll<T extends Doc>(
|
||||||
|
client: Client,
|
||||||
_class: Ref<Class<T>>,
|
_class: Ref<Class<T>>,
|
||||||
query: DocumentQuery<T>,
|
query: DocumentQuery<T>,
|
||||||
options?: FindOptions<T>
|
options?: FindOptions<T>
|
||||||
): Promise<FindResult<T>> {
|
): Promise<FindResult<T>> {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
const isModel = this.getHierarchy().findDomain(_class) === DOMAIN_MODEL
|
const isModel = client.getHierarchy().findDomain(_class) === DOMAIN_MODEL
|
||||||
const result = await this.client.findAll(_class, query, options)
|
const result = await client.findAll(_class, query, options)
|
||||||
if (this.notifyEnabled && !isModel) {
|
if (this.notifyEnabled && !isModel) {
|
||||||
console.debug(
|
console.debug(
|
||||||
'devmodel# findAll=>',
|
'devmodel# findAll=>',
|
||||||
@ -133,17 +125,18 @@ class ModelClient implements AccountClient {
|
|||||||
'result => ',
|
'result => ',
|
||||||
testing ? JSON.stringify(cutObjectArray(result)).slice(0, 160) : result,
|
testing ? JSON.stringify(cutObjectArray(result)).slice(0, 160) : result,
|
||||||
' =>model',
|
' =>model',
|
||||||
this.client.getModel(),
|
client.getModel(),
|
||||||
getMetadata(devmodel.metadata.DevModel),
|
getMetadata(devmodel.metadata.DevModel),
|
||||||
Date.now() - startTime,
|
Date.now() - startTime,
|
||||||
JSON.stringify(result).length
|
JSON.stringify(result).length,
|
||||||
|
this.stackLine()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchFulltext (query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
async searchFulltext (client: Client, query: SearchQuery, options: SearchOptions): Promise<SearchResult> {
|
||||||
const result = await this.client.searchFulltext(query, options)
|
const result = await client.searchFulltext(query, options)
|
||||||
if (this.notifyEnabled) {
|
if (this.notifyEnabled) {
|
||||||
console.debug(
|
console.debug(
|
||||||
'devmodel# searchFulltext=>',
|
'devmodel# searchFulltext=>',
|
||||||
@ -156,71 +149,25 @@ class ModelClient implements AccountClient {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async tx (tx: Tx): Promise<TxResult> {
|
async tx (client: Client, tx: Tx): Promise<TxResult> {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
const result = await this.client.tx(tx)
|
const result = await client.tx(tx)
|
||||||
if (this.notifyEnabled) {
|
if (this.notifyEnabled) {
|
||||||
console.debug(
|
console.debug(
|
||||||
'devmodel# tx=>',
|
'devmodel# tx=>',
|
||||||
testing ? JSON.stringify(cutObjectArray(tx)).slice(0, 160) : tx,
|
testing ? JSON.stringify(cutObjectArray(tx)).slice(0, 160) : tx,
|
||||||
result,
|
result,
|
||||||
getMetadata(devmodel.metadata.DevModel),
|
getMetadata(devmodel.metadata.DevModel),
|
||||||
Date.now() - startTime
|
Date.now() - startTime,
|
||||||
|
this.stackLine()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async close (): Promise<void> {
|
|
||||||
await this.client.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export async function Hook (client: AccountClient): Promise<Client> {
|
|
||||||
console.debug('devmodel# Client HOOKED by DevModel')
|
|
||||||
|
|
||||||
// Client is alive here, we could hook with some model extensions special for DevModel plugin.
|
|
||||||
const builder = new Builder()
|
|
||||||
|
|
||||||
builder.createDoc(
|
|
||||||
workbench.class.Application,
|
|
||||||
core.space.Model,
|
|
||||||
{
|
|
||||||
label: 'DevModel' as IntlString,
|
|
||||||
icon: view.icon.DevModel,
|
|
||||||
alias: devModelId,
|
|
||||||
hidden: false,
|
|
||||||
navigatorModel: {
|
|
||||||
spaces: [],
|
|
||||||
specials: [
|
|
||||||
{
|
|
||||||
label: 'Transactions' as IntlString,
|
|
||||||
icon: view.icon.Table,
|
|
||||||
id: 'transactions',
|
|
||||||
component: devmodel.component.ModelView
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
devmodel.ids.DevModelApp
|
|
||||||
)
|
|
||||||
|
|
||||||
const model = client.getModel()
|
|
||||||
for (const tx of builder.getTxes()) {
|
|
||||||
await model.tx(tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ModelClient(client)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toIntl (value: string): IntlString {
|
export function toIntl (value: string): IntlString {
|
||||||
return value as IntlString
|
return value as IntlString
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (): Promise<Resources> => ({
|
export default async (): Promise<Resources> => ({})
|
||||||
component: {
|
|
||||||
ModelView
|
|
||||||
},
|
|
||||||
hook: {
|
|
||||||
Hook
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
@ -13,8 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { ClientHook } from '@hcengineering/client'
|
import type { Asset, Metadata, Plugin } from '@hcengineering/platform'
|
||||||
import type { Asset, Metadata, Plugin, Resource } from '@hcengineering/platform'
|
|
||||||
import { plugin } from '@hcengineering/platform'
|
import { plugin } from '@hcengineering/platform'
|
||||||
import type { AnyComponent } from '@hcengineering/ui'
|
import type { AnyComponent } from '@hcengineering/ui'
|
||||||
|
|
||||||
@ -32,9 +31,6 @@ export default plugin(devModelId, {
|
|||||||
TransactionView: '' as AnyComponent,
|
TransactionView: '' as AnyComponent,
|
||||||
NotificationsView: '' as AnyComponent
|
NotificationsView: '' as AnyComponent
|
||||||
},
|
},
|
||||||
hook: {
|
|
||||||
Hook: '' as Resource<ClientHook>
|
|
||||||
},
|
|
||||||
metadata: {
|
metadata: {
|
||||||
DevModel: '' as Metadata<any>
|
DevModel: '' as Metadata<any>
|
||||||
}
|
}
|
||||||
|
@ -13,37 +13,36 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||||
|
import chunter from '@hcengineering/chunter'
|
||||||
|
import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
|
||||||
import { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
|
import { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
|
||||||
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import view, { decodeObjectURI } from '@hcengineering/view'
|
|
||||||
import {
|
import {
|
||||||
AnyComponent,
|
AnyComponent,
|
||||||
Component,
|
Component,
|
||||||
defineSeparators,
|
defineSeparators,
|
||||||
|
deviceOptionsStore as deviceInfo,
|
||||||
Label,
|
Label,
|
||||||
location as locationStore,
|
|
||||||
Location,
|
Location,
|
||||||
|
location as locationStore,
|
||||||
restoreLocation,
|
restoreLocation,
|
||||||
Scroller,
|
Scroller,
|
||||||
Separator,
|
Separator,
|
||||||
TabItem,
|
TabItem,
|
||||||
TabList,
|
TabList
|
||||||
deviceOptionsStore as deviceInfo
|
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import chunter from '@hcengineering/chunter'
|
import view, { decodeObjectURI } from '@hcengineering/view'
|
||||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
|
||||||
import { get } from 'svelte/store'
|
|
||||||
import { translate } from '@hcengineering/platform'
|
|
||||||
import { getCurrentAccount, groupByArray, IdMap, Ref, SortingOrder } from '@hcengineering/core'
|
|
||||||
import { parseLinkId } from '@hcengineering/view-resources'
|
import { parseLinkId } from '@hcengineering/view-resources'
|
||||||
|
import { get } from 'svelte/store'
|
||||||
|
|
||||||
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
import { InboxNotificationsClientImpl } from '../../inboxNotificationsClient'
|
||||||
import SettingsButton from './SettingsButton.svelte'
|
|
||||||
import { getDisplayInboxData, resetInboxContext, resolveLocation, selectInboxContext } from '../../utils'
|
|
||||||
import { InboxData, InboxNotificationsFilter } from '../../types'
|
|
||||||
import InboxGroupedListView from './InboxGroupedListView.svelte'
|
|
||||||
import notification from '../../plugin'
|
import notification from '../../plugin'
|
||||||
|
import { InboxData, InboxNotificationsFilter } from '../../types'
|
||||||
|
import { getDisplayInboxData, resetInboxContext, resolveLocation, selectInboxContext } from '../../utils'
|
||||||
|
import InboxGroupedListView from './InboxGroupedListView.svelte'
|
||||||
import InboxMenuButton from './InboxMenuButton.svelte'
|
import InboxMenuButton from './InboxMenuButton.svelte'
|
||||||
|
import SettingsButton from './SettingsButton.svelte'
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
@ -248,8 +247,7 @@
|
|||||||
|
|
||||||
const contextNotifications = $notificationsByContextStore.get(selectedContext._id) ?? []
|
const contextNotifications = $notificationsByContextStore.get(selectedContext._id) ?? []
|
||||||
|
|
||||||
const doneOp = await getClient().measure('readNotifications')
|
const ops = getClient().apply(selectedContext._id, 'readNotifications')
|
||||||
const ops = getClient().apply(selectedContext._id)
|
|
||||||
try {
|
try {
|
||||||
await inboxClient.readNotifications(
|
await inboxClient.readNotifications(
|
||||||
ops,
|
ops,
|
||||||
@ -261,7 +259,6 @@
|
|||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,8 +242,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async archiveAllNotifications (): Promise<void> {
|
async archiveAllNotifications (): Promise<void> {
|
||||||
const doneOp = await getClient().measure('archiveAllNotifications')
|
const ops = getClient().apply(generateId(), 'archiveAllNotifications')
|
||||||
const ops = getClient().apply(generateId())
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const inboxNotifications = await ops.findAll(
|
const inboxNotifications = await ops.findAll(
|
||||||
@ -264,13 +263,11 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readAllNotifications (): Promise<void> {
|
async readAllNotifications (): Promise<void> {
|
||||||
const doneOp = await getClient().measure('readAllNotifications')
|
const ops = getClient().apply(generateId(), 'readAllNotifications')
|
||||||
const ops = getClient().apply(generateId())
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const inboxNotifications = await ops.findAll(
|
const inboxNotifications = await ops.findAll(
|
||||||
@ -291,13 +288,11 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async unreadAllNotifications (): Promise<void> {
|
async unreadAllNotifications (): Promise<void> {
|
||||||
const doneOp = await getClient().measure('unreadAllNotifications')
|
const ops = getClient().apply(generateId(), 'unreadAllNotifications')
|
||||||
const ops = getClient().apply(generateId())
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const inboxNotifications = await ops.findAll(
|
const inboxNotifications = await ops.findAll(
|
||||||
@ -332,7 +327,6 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient {
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,21 +49,21 @@ import { MessageBox, getClient } from '@hcengineering/presentation'
|
|||||||
import {
|
import {
|
||||||
getCurrentLocation,
|
getCurrentLocation,
|
||||||
getLocation,
|
getLocation,
|
||||||
|
locationStorageKeyId,
|
||||||
navigate,
|
navigate,
|
||||||
parseLocation,
|
parseLocation,
|
||||||
showPopup,
|
showPopup,
|
||||||
type Location,
|
type Location,
|
||||||
type ResolvedLocation,
|
type ResolvedLocation
|
||||||
locationStorageKeyId
|
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { get, writable } from 'svelte/store'
|
import { get, writable } from 'svelte/store'
|
||||||
|
|
||||||
|
import chunter, { type ThreadMessage } from '@hcengineering/chunter'
|
||||||
|
import { getMetadata } from '@hcengineering/platform'
|
||||||
|
import { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
|
||||||
|
import { getObjectLinkId } from '@hcengineering/view-resources'
|
||||||
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
|
import { InboxNotificationsClientImpl } from './inboxNotificationsClient'
|
||||||
import { type InboxData, type InboxNotificationsFilter } from './types'
|
import { type InboxData, type InboxNotificationsFilter } from './types'
|
||||||
import { getMetadata } from '@hcengineering/platform'
|
|
||||||
import { getObjectLinkId } from '@hcengineering/view-resources'
|
|
||||||
import { decodeObjectURI, encodeObjectURI, type LinkIdProvider } from '@hcengineering/view'
|
|
||||||
import chunter, { type ThreadMessage } from '@hcengineering/chunter'
|
|
||||||
|
|
||||||
export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
|
export async function hasDocNotifyContextPinAction (docNotifyContext: DocNotifyContext): Promise<boolean> {
|
||||||
if (docNotifyContext.hidden) {
|
if (docNotifyContext.hidden) {
|
||||||
@ -107,8 +107,7 @@ export async function readNotifyContext (doc: DocNotifyContext): Promise<void> {
|
|||||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||||
const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
|
const inboxNotifications = get(inboxClient.inboxNotificationsByContext).get(doc._id) ?? []
|
||||||
|
|
||||||
const doneOp = await getClient().measure('readNotifyContext')
|
const ops = getClient().apply(doc._id, 'readNotifyContext')
|
||||||
const ops = getClient().apply(doc._id)
|
|
||||||
try {
|
try {
|
||||||
await inboxClient.readNotifications(
|
await inboxClient.readNotifications(
|
||||||
ops,
|
ops,
|
||||||
@ -117,7 +116,6 @@ export async function readNotifyContext (doc: DocNotifyContext): Promise<void> {
|
|||||||
await ops.update(doc, { lastViewedTimestamp: Date.now() })
|
await ops.update(doc, { lastViewedTimestamp: Date.now() })
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,8 +131,7 @@ export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void>
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const doneOp = await getClient().measure('unReadNotifyContext')
|
const ops = getClient().apply(doc._id, 'unReadNotifyContext')
|
||||||
const ops = getClient().apply(doc._id)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await inboxClient.unreadNotifications(
|
await inboxClient.unreadNotifications(
|
||||||
@ -154,7 +151,6 @@ export async function unReadNotifyContext (doc: DocNotifyContext): Promise<void>
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,8 +162,7 @@ export async function archiveContextNotifications (doc?: DocNotifyContext): Prom
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const doneOp = await getClient().measure('archiveContextNotifications')
|
const ops = getClient().apply(doc._id, 'archiveContextNotifications')
|
||||||
const ops = getClient().apply(doc._id)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const notifications = await ops.findAll(
|
const notifications = await ops.findAll(
|
||||||
@ -182,7 +177,6 @@ export async function archiveContextNotifications (doc?: DocNotifyContext): Prom
|
|||||||
await ops.update(doc, { lastViewedTimestamp: Date.now() })
|
await ops.update(doc, { lastViewedTimestamp: Date.now() })
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,8 +188,7 @@ export async function unarchiveContextNotifications (doc?: DocNotifyContext): Pr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const doneOp = await getClient().measure('unarchiveContextNotifications')
|
const ops = getClient().apply(doc._id, 'unarchiveContextNotifications')
|
||||||
const ops = getClient().apply(doc._id)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const notifications = await ops.findAll(
|
const notifications = await ops.findAll(
|
||||||
@ -209,7 +202,6 @@ export async function unarchiveContextNotifications (doc?: DocNotifyContext): Pr
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await ops.commit()
|
await ops.commit()
|
||||||
await doneOp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,10 +442,8 @@
|
|||||||
|
|
||||||
// TODO: We need a measure client and mark all operations with it as measure under one root,
|
// TODO: We need a measure client and mark all operations with it as measure under one root,
|
||||||
// to prevent other operations to infer our measurement.
|
// to prevent other operations to infer our measurement.
|
||||||
const doneOp = await getClient().measure('tracker.createIssue')
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const operations = client.apply(_id)
|
const operations = client.apply(_id, 'tracker.createIssue')
|
||||||
|
|
||||||
const lastOne = await client.findOne<Issue>(
|
const lastOne = await client.findOne<Issue>(
|
||||||
tracker.class.Issue,
|
tracker.class.Issue,
|
||||||
@ -533,7 +531,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await operations.commit()
|
const result = await operations.commit()
|
||||||
await descriptionBox?.createAttachments(_id)
|
await descriptionBox?.createAttachments(_id)
|
||||||
|
|
||||||
const parents: IssueParentInfo[] =
|
const parents: IssueParentInfo[] =
|
||||||
@ -565,15 +563,12 @@
|
|||||||
descriptionBox?.removeDraft(false)
|
descriptionBox?.removeDraft(false)
|
||||||
isAssigneeTouched = false
|
isAssigneeTouched = false
|
||||||
const d1 = Date.now()
|
const d1 = Date.now()
|
||||||
void doneOp().then((res) => {
|
console.log('createIssue measure', result, Date.now() - d1)
|
||||||
console.log('createIssue measure', res, Date.now() - d1)
|
|
||||||
})
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
resetObject()
|
resetObject()
|
||||||
draftController.remove()
|
draftController.remove()
|
||||||
descriptionBox?.removeDraft(false)
|
descriptionBox?.removeDraft(false)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
await doneOp() // Complete in case of error
|
|
||||||
Analytics.handleError(err)
|
Analytics.handleError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,7 +235,7 @@
|
|||||||
await ops.createDoc(tracker.class.Project, core.space.Space, { ...projectData, type: typeId }, projectId)
|
await ops.createDoc(tracker.class.Project, core.space.Space, { ...projectData, type: typeId }, projectId)
|
||||||
const succeeded = await ops.commit()
|
const succeeded = await ops.commit()
|
||||||
|
|
||||||
if (succeeded) {
|
if (succeeded.result) {
|
||||||
// Add space type's mixin with roles assignments
|
// Add space type's mixin with roles assignments
|
||||||
await client.createMixin(
|
await client.createMixin(
|
||||||
projectId,
|
projectId,
|
||||||
|
@ -25,8 +25,8 @@ export async function issueIdentifierProvider (client: TxOperations, ref: Ref<Is
|
|||||||
return object.identifier
|
return object.identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function issueTitleProvider (client: TxOperations, ref: Ref<Doc>): Promise<string> {
|
export async function issueTitleProvider (client: TxOperations, ref: Ref<Doc>, doc?: Issue): Promise<string> {
|
||||||
const object = await client.findOne(tracker.class.Issue, { _id: ref as Ref<Issue> })
|
const object = doc ?? (await client.findOne(tracker.class.Issue, { _id: ref as Ref<Issue> }))
|
||||||
|
|
||||||
if (object === undefined) {
|
if (object === undefined) {
|
||||||
return ''
|
return ''
|
||||||
|
@ -652,7 +652,7 @@ async function ActivityReferenceCreate (tx: TxCUD<Doc>, control: TriggerControl)
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (txes.length !== 0) {
|
if (txes.length !== 0) {
|
||||||
await control.apply(txes, true)
|
await control.apply(txes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
@ -699,7 +699,7 @@ async function ActivityReferenceUpdate (tx: TxCUD<Doc>, control: TriggerControl)
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (txes.length !== 0) {
|
if (txes.length !== 0) {
|
||||||
await control.apply(txes, true)
|
await control.apply(txes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
@ -723,7 +723,7 @@ async function ActivityReferenceRemove (tx: Tx, control: TriggerControl): Promis
|
|||||||
|
|
||||||
const txes: Tx[] = await getRemoveActivityReferenceTxes(control, txFactory, ctx.objectId)
|
const txes: Tx[] = await getRemoveActivityReferenceTxes(control, txFactory, ctx.objectId)
|
||||||
if (txes.length !== 0) {
|
if (txes.length !== 0) {
|
||||||
await control.apply(txes, true)
|
await control.apply(txes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,9 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
|
||||||
import chunter, { Channel, ChatMessage, chunterId, ChunterSpace, ThreadMessage } from '@hcengineering/chunter'
|
import chunter, { Channel, ChatMessage, chunterId, ChunterSpace, ThreadMessage } from '@hcengineering/chunter'
|
||||||
|
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||||
import core, {
|
import core, {
|
||||||
Account,
|
Account,
|
||||||
AttachedDoc,
|
AttachedDoc,
|
||||||
@ -38,14 +40,12 @@ import notification, { Collaborators, NotificationContent } from '@hcengineering
|
|||||||
import { getMetadata, IntlString } from '@hcengineering/platform'
|
import { getMetadata, IntlString } from '@hcengineering/platform'
|
||||||
import serverCore, { TriggerControl } from '@hcengineering/server-core'
|
import serverCore, { TriggerControl } from '@hcengineering/server-core'
|
||||||
import {
|
import {
|
||||||
|
createCollaboratorNotifications,
|
||||||
getDocCollaborators,
|
getDocCollaborators,
|
||||||
getMixinTx,
|
getMixinTx
|
||||||
createCollaboratorNotifications
|
|
||||||
} from '@hcengineering/server-notification-resources'
|
} from '@hcengineering/server-notification-resources'
|
||||||
import { workbenchId } from '@hcengineering/workbench'
|
|
||||||
import { stripTags } from '@hcengineering/text'
|
import { stripTags } from '@hcengineering/text'
|
||||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
import { workbenchId } from '@hcengineering/workbench'
|
||||||
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
|
|
||||||
|
|
||||||
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
|
import { NOTIFICATION_BODY_SIZE } from '@hcengineering/server-notification'
|
||||||
|
|
||||||
@ -258,13 +258,24 @@ async function OnThreadMessageDeleted (tx: Tx, control: TriggerControl): Promise
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export async function ChunterTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
export async function ChunterTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||||
const res = await Promise.all([
|
const res: Tx[] = []
|
||||||
OnThreadMessageCreated(tx, control),
|
res.push(
|
||||||
OnThreadMessageDeleted(tx, control),
|
...(await control.ctx.with('OnThreadMessageCreated', {}, async (ctx) => await OnThreadMessageCreated(tx, control)))
|
||||||
OnCollaboratorsChanged(tx as TxMixin<Doc, Collaborators>, control),
|
)
|
||||||
OnChatMessageCreated(tx, control)
|
res.push(
|
||||||
])
|
...(await control.ctx.with('OnThreadMessageDeleted', {}, async (ctx) => await OnThreadMessageDeleted(tx, control)))
|
||||||
return res.flat()
|
)
|
||||||
|
res.push(
|
||||||
|
...(await control.ctx.with(
|
||||||
|
'OnCollaboratorsChanged',
|
||||||
|
{},
|
||||||
|
async (ctx) => await OnCollaboratorsChanged(tx as TxMixin<Doc, Collaborators>, control)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
res.push(
|
||||||
|
...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control)))
|
||||||
|
)
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -393,7 +404,7 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc<Channel>, control: Trigg
|
|||||||
lastViewedTimestamp: tx.modifiedOn
|
lastViewedTimestamp: tx.modifiedOn
|
||||||
})
|
})
|
||||||
|
|
||||||
await control.apply([createTx], true)
|
await control.apply([createTx])
|
||||||
} else {
|
} else {
|
||||||
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
|
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
@ -284,7 +284,7 @@ async function createDocumentTrainingRequest (doc: ControlledDocument, control:
|
|||||||
// Force space to make transaction persistent and raise notifications
|
// Force space to make transaction persistent and raise notifications
|
||||||
resTx.space = core.space.Tx
|
resTx.space = core.space.Tx
|
||||||
|
|
||||||
await control.apply([resTx], true)
|
await control.apply([resTx])
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@ -368,7 +368,7 @@ export async function OnDocPlannedEffectiveDateChanged (
|
|||||||
if (tx.operations.plannedEffectiveDate === 0 && doc.controlledState === ControlledDocumentState.Approved) {
|
if (tx.operations.plannedEffectiveDate === 0 && doc.controlledState === ControlledDocumentState.Approved) {
|
||||||
// Create with not derived tx factory in order for notifications to work
|
// Create with not derived tx factory in order for notifications to work
|
||||||
const factory = new TxFactory(control.txFactory.account)
|
const factory = new TxFactory(control.txFactory.account)
|
||||||
await control.apply([makeDocEffective(doc, factory)], true)
|
await control.apply([makeDocEffective(doc, factory)])
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
@ -385,7 +385,7 @@ export async function OnDocApprovalRequestApproved (
|
|||||||
|
|
||||||
// Create with not derived tx factory in order for notifications to work
|
// Create with not derived tx factory in order for notifications to work
|
||||||
const factory = new TxFactory(control.txFactory.account)
|
const factory = new TxFactory(control.txFactory.account)
|
||||||
await control.apply([makeDocEffective(doc, factory)], true)
|
await control.apply([makeDocEffective(doc, factory)])
|
||||||
|
|
||||||
// make doc effective immediately
|
// make doc effective immediately
|
||||||
return []
|
return []
|
||||||
|
@ -97,9 +97,10 @@ async function createUserInfo (acc: Ref<Account>, control: TriggerControl): Prom
|
|||||||
query: { person }
|
query: { person }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[tx]
|
[tx],
|
||||||
|
'createUserInfo'
|
||||||
)
|
)
|
||||||
await control.apply([ptx], true)
|
await control.apply([ptx])
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +115,7 @@ async function removeUserInfo (acc: Ref<Account>, control: TriggerControl): Prom
|
|||||||
const person = account.person
|
const person = account.person
|
||||||
const infos = await control.findAll(love.class.ParticipantInfo, { person })
|
const infos = await control.findAll(love.class.ParticipantInfo, { person })
|
||||||
for (const info of infos) {
|
for (const info of infos) {
|
||||||
await control.apply([control.txFactory.createTxRemoveDoc(info._class, info.space, info._id)], true)
|
await control.apply([control.txFactory.createTxRemoveDoc(info._class, info.space, info._id)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
|
import activity, { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
|
||||||
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||||
import contact, {
|
import contact, {
|
||||||
type AvatarInfo,
|
type AvatarInfo,
|
||||||
@ -42,6 +43,7 @@ import core, {
|
|||||||
RefTo,
|
RefTo,
|
||||||
Space,
|
Space,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
|
toIdMap,
|
||||||
Tx,
|
Tx,
|
||||||
TxCollectionCUD,
|
TxCollectionCUD,
|
||||||
TxCreateDoc,
|
TxCreateDoc,
|
||||||
@ -75,12 +77,11 @@ import serverNotification, {
|
|||||||
getPersonAccountById,
|
getPersonAccountById,
|
||||||
NOTIFICATION_BODY_SIZE
|
NOTIFICATION_BODY_SIZE
|
||||||
} from '@hcengineering/server-notification'
|
} from '@hcengineering/server-notification'
|
||||||
|
import serverView from '@hcengineering/server-view'
|
||||||
import { stripTags } from '@hcengineering/text'
|
import { stripTags } from '@hcengineering/text'
|
||||||
|
import { encodeObjectURI } from '@hcengineering/view'
|
||||||
import { workbenchId } from '@hcengineering/workbench'
|
import { workbenchId } from '@hcengineering/workbench'
|
||||||
import webpush, { WebPushError } from 'web-push'
|
import webpush, { WebPushError } from 'web-push'
|
||||||
import { encodeObjectURI } from '@hcengineering/view'
|
|
||||||
import serverView from '@hcengineering/server-view'
|
|
||||||
import { Analytics } from '@hcengineering/analytics'
|
|
||||||
|
|
||||||
import { Content, NotifyParams, NotifyResult, UserInfo } from './types'
|
import { Content, NotifyParams, NotifyResult, UserInfo } from './types'
|
||||||
import {
|
import {
|
||||||
@ -396,14 +397,24 @@ export async function pushInboxNotifications (
|
|||||||
hidden: false,
|
hidden: false,
|
||||||
lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined
|
lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined
|
||||||
})
|
})
|
||||||
await control.apply([createContextTx], true, [account.email])
|
await control.apply([createContextTx])
|
||||||
|
control.operationContext.derived.targets['docNotifyContext' + createContextTx._id] = (it) => {
|
||||||
|
if (it._id === createContextTx._id) {
|
||||||
|
return [account.email]
|
||||||
|
}
|
||||||
|
}
|
||||||
docNotifyContextId = createContextTx.objectId
|
docNotifyContextId = createContextTx.objectId
|
||||||
} else {
|
} else {
|
||||||
if (shouldUpdateTimestamp && context.lastUpdateTimestamp !== modifiedOn) {
|
if (shouldUpdateTimestamp && context.lastUpdateTimestamp !== modifiedOn) {
|
||||||
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
|
const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, {
|
||||||
lastUpdateTimestamp: modifiedOn
|
lastUpdateTimestamp: modifiedOn
|
||||||
})
|
})
|
||||||
await control.apply([updateTx], true, [account.email])
|
await control.apply([updateTx])
|
||||||
|
control.operationContext.derived.targets['docNotifyContext' + updateTx._id] = (it) => {
|
||||||
|
if (it._id === updateTx._id) {
|
||||||
|
return [account.email]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
docNotifyContextId = context._id
|
docNotifyContextId = context._id
|
||||||
}
|
}
|
||||||
@ -636,7 +647,7 @@ async function sendPushToSubscription (
|
|||||||
console.log('Cannot send push notification to', targetUser, err)
|
console.log('Cannot send push notification to', targetUser, err)
|
||||||
if (err instanceof WebPushError && err.body.includes('expired')) {
|
if (err instanceof WebPushError && err.body.includes('expired')) {
|
||||||
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
|
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
|
||||||
await control.apply([tx], true)
|
await control.apply([tx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1286,7 +1297,14 @@ async function applyUserTxes (
|
|||||||
|
|
||||||
if (account !== undefined) {
|
if (account !== undefined) {
|
||||||
cache.set(account._id, account)
|
cache.set(account._id, account)
|
||||||
await control.apply(txs, true, [account.email])
|
await control.apply(txs)
|
||||||
|
|
||||||
|
const m1 = toIdMap(txes)
|
||||||
|
control.operationContext.derived.targets.docNotifyContext = (it) => {
|
||||||
|
if (m1.has(it._id)) {
|
||||||
|
return [account.email]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ async function OnRequestUpdate (tx: TxCollectionCUD<Doc, Request>, control: Trig
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (applyTxes.length > 0) {
|
if (applyTxes.length > 0) {
|
||||||
await control.apply(applyTxes, true)
|
await control.apply(applyTxes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
@ -107,7 +107,7 @@ export async function OnWorkSlotCreate (tx: Tx, control: TriggerControl): Promis
|
|||||||
issue.collection,
|
issue.collection,
|
||||||
innerTx
|
innerTx
|
||||||
)
|
)
|
||||||
await control.apply([outerTx], true)
|
await control.apply([outerTx])
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,7 +153,7 @@ export async function OnToDoRemove (tx: Tx, control: TriggerControl): Promise<Tx
|
|||||||
issue.collection,
|
issue.collection,
|
||||||
innerTx
|
innerTx
|
||||||
)
|
)
|
||||||
await control.apply([outerTx], true)
|
await control.apply([outerTx])
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -304,7 +304,7 @@ export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx
|
|||||||
if (funcs !== undefined) {
|
if (funcs !== undefined) {
|
||||||
const func = await getResource(funcs.onDone)
|
const func = await getResource(funcs.onDone)
|
||||||
const todoRes = await func(control, resEvents, todo)
|
const todoRes = await func(control, resEvents, todo)
|
||||||
await control.apply(todoRes, true)
|
await control.apply(todoRes)
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@ -447,7 +447,7 @@ async function createIssueHandler (issue: Issue, control: TriggerControl): Promi
|
|||||||
if (status.category === task.statusCategory.Active || status.category === task.statusCategory.ToDo) {
|
if (status.category === task.statusCategory.Active || status.category === task.statusCategory.ToDo) {
|
||||||
const tx = await getCreateToDoTx(issue, issue.assignee, control)
|
const tx = await getCreateToDoTx(issue, issue.assignee, control)
|
||||||
if (tx !== undefined) {
|
if (tx !== undefined) {
|
||||||
await control.apply([tx], true)
|
await control.apply([tx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -561,7 +561,7 @@ async function changeIssueStatusHandler (
|
|||||||
if (todos.length === 0) {
|
if (todos.length === 0) {
|
||||||
const tx = await getCreateToDoTx(issue, issue.assignee, control)
|
const tx = await getCreateToDoTx(issue, issue.assignee, control)
|
||||||
if (tx !== undefined) {
|
if (tx !== undefined) {
|
||||||
await control.apply([tx], true)
|
await control.apply([tx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,10 @@ import core, {
|
|||||||
TxProcessor,
|
TxProcessor,
|
||||||
cutObjectArray,
|
cutObjectArray,
|
||||||
toFindResult,
|
toFindResult,
|
||||||
type Branding,
|
|
||||||
type Account,
|
type Account,
|
||||||
type AttachedDoc,
|
type AttachedDoc,
|
||||||
|
type Branding,
|
||||||
|
type BroadcastTargets,
|
||||||
type Class,
|
type Class,
|
||||||
type Client,
|
type Client,
|
||||||
type Collection,
|
type Collection,
|
||||||
@ -50,6 +51,7 @@ import core, {
|
|||||||
type Timestamp,
|
type Timestamp,
|
||||||
type Tx,
|
type Tx,
|
||||||
type TxApplyIf,
|
type TxApplyIf,
|
||||||
|
type TxApplyResult,
|
||||||
type TxCUD,
|
type TxCUD,
|
||||||
type TxCollectionCUD,
|
type TxCollectionCUD,
|
||||||
type TxRemoveDoc,
|
type TxRemoveDoc,
|
||||||
@ -612,7 +614,7 @@ export class TServerStorage implements ServerStorage {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private async broadcastCtx (derived: SessionOperationContext['derived']): Promise<void> {
|
private async broadcastCtx (derived: Tx[], targets?: BroadcastTargets): Promise<void> {
|
||||||
const toSendTarget = new Map<string, Tx[]>()
|
const toSendTarget = new Map<string, Tx[]>()
|
||||||
|
|
||||||
const getTxes = (key: string): Tx[] => {
|
const getTxes = (key: string): Tx[] => {
|
||||||
@ -626,16 +628,23 @@ export class TServerStorage implements ServerStorage {
|
|||||||
|
|
||||||
// Put current user as send target
|
// Put current user as send target
|
||||||
for (const txd of derived) {
|
for (const txd of derived) {
|
||||||
if (txd.target === undefined) {
|
let target: string[] | undefined
|
||||||
|
for (const tt of Object.values(targets ?? {})) {
|
||||||
|
target = tt(txd)
|
||||||
|
if (target !== undefined) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target === undefined) {
|
||||||
getTxes('') // Be sure we have empty one
|
getTxes('') // Be sure we have empty one
|
||||||
|
|
||||||
// Also add to all other targeted sends
|
// Also add to all other targeted sends
|
||||||
for (const v of toSendTarget.values()) {
|
for (const v of toSendTarget.values()) {
|
||||||
v.push(...txd.derived)
|
v.push(txd)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const t of txd.target) {
|
for (const t of target) {
|
||||||
getTxes(t).push(...txd.derived)
|
getTxes(t).push(txd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -708,7 +717,9 @@ export class TServerStorage implements ServerStorage {
|
|||||||
)
|
)
|
||||||
const moves = await ctx.with('process-move', {}, (ctx) => this.processMove(ctx.ctx, txes, findAll))
|
const moves = await ctx.with('process-move', {}, (ctx) => this.processMove(ctx.ctx, txes, findAll))
|
||||||
|
|
||||||
const triggerControl: Omit<TriggerControl, 'txFactory' | 'ctx' | 'result'> = {
|
const applyTxes: Tx[] = []
|
||||||
|
|
||||||
|
const triggerControl: Omit<TriggerControl, 'txFactory' | 'ctx' | 'result' | 'apply'> = {
|
||||||
operationContext: ctx,
|
operationContext: ctx,
|
||||||
removedMap,
|
removedMap,
|
||||||
workspace: this.workspaceId,
|
workspace: this.workspaceId,
|
||||||
@ -719,52 +730,73 @@ export class TServerStorage implements ServerStorage {
|
|||||||
findAllCtx: findAll,
|
findAllCtx: findAll,
|
||||||
modelDb: this.modelDb,
|
modelDb: this.modelDb,
|
||||||
hierarchy: this.hierarchy,
|
hierarchy: this.hierarchy,
|
||||||
apply: async (tx, broadcast, target) => {
|
applyCtx: async (ctx, tx, needResult) => {
|
||||||
return await this.apply(ctx, tx, broadcast, target)
|
if (needResult === true) {
|
||||||
},
|
return await this.apply(ctx, tx)
|
||||||
applyCtx: async (ctx, tx, broadcast, target) => {
|
} else {
|
||||||
return await this.apply(ctx, tx, broadcast, target)
|
applyTxes.push(...tx)
|
||||||
|
}
|
||||||
|
return {}
|
||||||
},
|
},
|
||||||
// Will create a live query if missing and return values immediately if already asked.
|
// Will create a live query if missing and return values immediately if already asked.
|
||||||
queryFind: async (_class, query, options) => {
|
queryFind: async (_class, query, options) => {
|
||||||
return await this.liveQuery.queryFind(_class, query, options)
|
return await this.liveQuery.queryFind(_class, query, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const triggers = await ctx.with('process-triggers', {}, async (ctx) => {
|
const triggers = await ctx.with(
|
||||||
const result: Tx[] = []
|
'process-triggers',
|
||||||
const { transactions, performAsync } = await this.triggers.apply(ctx, txes, {
|
{},
|
||||||
...triggerControl,
|
async (ctx) => {
|
||||||
ctx: ctx.ctx,
|
const result: Tx[] = []
|
||||||
findAll: fAll(ctx.ctx),
|
const { transactions, performAsync } = await this.triggers.apply(ctx, txes, {
|
||||||
result
|
...triggerControl,
|
||||||
})
|
ctx: ctx.ctx,
|
||||||
result.push(...transactions)
|
findAll: fAll(ctx.ctx),
|
||||||
|
result
|
||||||
|
})
|
||||||
|
result.push(...transactions)
|
||||||
|
|
||||||
if (performAsync !== undefined) {
|
if (applyTxes.length > 0) {
|
||||||
const asyncTriggerProcessor = async (): Promise<void> => {
|
await this.apply(ctx, applyTxes)
|
||||||
await ctx.with('process-async-triggers', {}, async (ctx) => {
|
}
|
||||||
const sctx = ctx as SessionContext
|
|
||||||
const applyCtx: SessionContextImpl = new SessionContextImpl(
|
if (performAsync !== undefined) {
|
||||||
ctx.ctx,
|
const asyncTriggerProcessor = async (): Promise<void> => {
|
||||||
sctx.userEmail,
|
await ctx.with(
|
||||||
sctx.sessionId,
|
'process-async-triggers',
|
||||||
sctx.admin,
|
{},
|
||||||
[],
|
async (ctx) => {
|
||||||
this.workspaceId,
|
const sctx = ctx as SessionContext
|
||||||
this.options.branding
|
const applyCtx: SessionContextImpl = new SessionContextImpl(
|
||||||
|
ctx.ctx,
|
||||||
|
sctx.userEmail,
|
||||||
|
sctx.sessionId,
|
||||||
|
sctx.admin,
|
||||||
|
{ txes: [], targets: {} },
|
||||||
|
this.workspaceId,
|
||||||
|
this.options.branding,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
const result = await performAsync(applyCtx)
|
||||||
|
|
||||||
|
if (applyTxes.length > 0) {
|
||||||
|
await this.apply(applyCtx, applyTxes)
|
||||||
|
}
|
||||||
|
// We need to broadcast changes
|
||||||
|
await this.broadcastCtx(applyCtx.derived.txes.concat(result), applyCtx.derived.targets)
|
||||||
|
},
|
||||||
|
{ count: txes.length }
|
||||||
)
|
)
|
||||||
const result = await performAsync(applyCtx)
|
}
|
||||||
// We need to broadcast changes
|
setTimeout(() => {
|
||||||
await this.broadcastCtx([{ derived: result }, ...applyCtx.derived])
|
void asyncTriggerProcessor()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
|
||||||
void asyncTriggerProcessor()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
},
|
||||||
|
{ count: txes.length }
|
||||||
|
)
|
||||||
|
|
||||||
const derived = [...removed, ...collections, ...moves, ...triggers]
|
const derived = [...removed, ...collections, ...moves, ...triggers]
|
||||||
|
|
||||||
@ -835,8 +867,8 @@ export class TServerStorage implements ServerStorage {
|
|||||||
return { passed, onEnd }
|
return { passed, onEnd }
|
||||||
}
|
}
|
||||||
|
|
||||||
async apply (ctx: SessionOperationContext, txes: Tx[], broadcast: boolean, target?: string[]): Promise<TxResult> {
|
async apply (ctx: SessionOperationContext, txes: Tx[]): Promise<TxResult> {
|
||||||
return await this.processTxes(ctx, txes, broadcast, target)
|
return await this.processTxes(ctx, txes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fillTxes (txes: Tx[], txToStore: Tx[], modelTx: Tx[], txToProcess: Tx[], applyTxes: Tx[]): void {
|
fillTxes (txes: Tx[], txToStore: Tx[], modelTx: Tx[], txToProcess: Tx[], applyTxes: Tx[]): void {
|
||||||
@ -861,12 +893,7 @@ export class TServerStorage implements ServerStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async processTxes (
|
async processTxes (ctx: SessionOperationContext, txes: Tx[]): Promise<TxResult> {
|
||||||
ctx: SessionOperationContext,
|
|
||||||
txes: Tx[],
|
|
||||||
broadcast: boolean,
|
|
||||||
target?: string[]
|
|
||||||
): Promise<TxResult> {
|
|
||||||
// store tx
|
// store tx
|
||||||
const _findAll: ServerStorage['findAll'] = async <T extends Doc>(
|
const _findAll: ServerStorage['findAll'] = async <T extends Doc>(
|
||||||
ctx: MeasureContext,
|
ctx: MeasureContext,
|
||||||
@ -914,16 +941,25 @@ export class TServerStorage implements ServerStorage {
|
|||||||
await this.triggers.tx(tx)
|
await this.triggers.tx(tx)
|
||||||
await this.modelDb.tx(tx)
|
await this.modelDb.tx(tx)
|
||||||
}
|
}
|
||||||
await ctx.with('domain-tx', {}, async (ctx) => await this.getAdapter(DOMAIN_TX, true).tx(ctx.ctx, ...txToStore))
|
await ctx.with('domain-tx', {}, async (ctx) => await this.getAdapter(DOMAIN_TX, true).tx(ctx.ctx, ...txToStore), {
|
||||||
result.push(...(await ctx.with('apply', {}, (ctx) => this.routeTx(ctx.ctx, removedMap, ...txToProcess))))
|
count: txToStore.length
|
||||||
|
})
|
||||||
|
result.push(...(await ctx.with('apply', {}, (ctx) => this.routeTx(ctx.ctx, removedMap, ...txToProcess))), {
|
||||||
|
count: txToProcess.length
|
||||||
|
})
|
||||||
|
|
||||||
// invoke triggers and store derived objects
|
// invoke triggers and store derived objects
|
||||||
derived.push(...(await this.processDerived(ctx, txToProcess, _findAll, removedMap)))
|
derived.push(...(await this.processDerived(ctx, txToProcess, _findAll, removedMap)))
|
||||||
|
|
||||||
// index object
|
// index object
|
||||||
await ctx.with('fulltext-tx', {}, async (ctx) => {
|
await ctx.with(
|
||||||
await this.fulltext.tx(ctx.ctx, [...txToProcess, ...derived])
|
'fulltext-tx',
|
||||||
})
|
{},
|
||||||
|
async (ctx) => {
|
||||||
|
await this.fulltext.tx(ctx.ctx, [...txToProcess, ...derived])
|
||||||
|
},
|
||||||
|
{ count: txToProcess.length + derived.length }
|
||||||
|
)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.ctx.error('error process tx', { error: err })
|
ctx.ctx.error('error process tx', { error: err })
|
||||||
Analytics.handleError(err)
|
Analytics.handleError(err)
|
||||||
@ -933,16 +969,33 @@ export class TServerStorage implements ServerStorage {
|
|||||||
p()
|
p()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (derived.length > 0 && broadcast) {
|
if (derived.length > 0) {
|
||||||
ctx.derived.push({ derived, target })
|
ctx.derived.txes.push(...derived)
|
||||||
}
|
}
|
||||||
return result[0]
|
return result[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
async tx (ctx: SessionOperationContext, tx: Tx): Promise<TxResult> {
|
async tx (ctx: SessionOperationContext, tx: Tx): Promise<TxResult> {
|
||||||
return await ctx.with('client-tx', { _class: tx._class }, async (ctx) => {
|
let measureName: string | undefined
|
||||||
return await this.processTxes(ctx, [tx], true)
|
let st: number | undefined
|
||||||
})
|
if (tx._class === core.class.TxApplyIf && (tx as TxApplyIf).measureName !== undefined) {
|
||||||
|
measureName = (tx as TxApplyIf).measureName
|
||||||
|
st = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await ctx.with(
|
||||||
|
measureName !== undefined ? `📶 ${measureName}` : 'client-tx',
|
||||||
|
{ _class: tx._class },
|
||||||
|
async (ctx) => {
|
||||||
|
return await this.processTxes(ctx, [tx])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (measureName !== undefined && st !== undefined) {
|
||||||
|
;(result as TxApplyResult).serverTime = Date.now() - st
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
find (ctx: MeasureContext, domain: Domain, recheck?: boolean): StorageIterator {
|
find (ctx: MeasureContext, domain: Domain, recheck?: boolean): StorageIterator {
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import core, {
|
import core, {
|
||||||
TxFactory,
|
TxFactory,
|
||||||
cutObjectArray,
|
cutObjectArray,
|
||||||
|
groupByArray,
|
||||||
matchQuery,
|
matchQuery,
|
||||||
type AttachedDoc,
|
type AttachedDoc,
|
||||||
type Class,
|
type Class,
|
||||||
@ -31,15 +32,18 @@ import core, {
|
|||||||
type TxCreateDoc
|
type TxCreateDoc
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
|
|
||||||
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
import { getResource, type Resource } from '@hcengineering/platform'
|
import { getResource, type Resource } from '@hcengineering/platform'
|
||||||
import type { Trigger, TriggerControl, TriggerFunc } from './types'
|
import type { Trigger, TriggerControl, TriggerFunc } from './types'
|
||||||
import { Analytics } from '@hcengineering/analytics'
|
|
||||||
|
|
||||||
import serverCore from './plugin'
|
import serverCore from './plugin'
|
||||||
|
import type { SessionContextImpl } from './utils'
|
||||||
|
|
||||||
interface TriggerRecord {
|
interface TriggerRecord {
|
||||||
query?: DocumentQuery<Tx>
|
query?: DocumentQuery<Tx>
|
||||||
trigger: { op: TriggerFunc, resource: Resource<TriggerFunc>, isAsync: boolean }
|
trigger: { op: TriggerFunc, resource: Resource<TriggerFunc>, isAsync: boolean }
|
||||||
|
|
||||||
|
arrays: boolean
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -64,7 +68,8 @@ export class Triggers {
|
|||||||
|
|
||||||
this.triggers.push({
|
this.triggers.push({
|
||||||
query: match,
|
query: match,
|
||||||
trigger: { op: func, resource: trigger, isAsync }
|
trigger: { op: func, resource: trigger, isAsync },
|
||||||
|
arrays: (createTx as TxCreateDoc<Trigger>).attributes.arrays === true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,47 +78,55 @@ export class Triggers {
|
|||||||
async apply (
|
async apply (
|
||||||
ctx: SessionOperationContext,
|
ctx: SessionOperationContext,
|
||||||
tx: Tx[],
|
tx: Tx[],
|
||||||
ctrl: Omit<TriggerControl, 'txFactory'>
|
ctrl: Omit<TriggerControl, 'txFactory' | 'apply'>
|
||||||
): Promise<{
|
): Promise<{
|
||||||
transactions: Tx[]
|
transactions: Tx[]
|
||||||
performAsync?: (ctx: SessionOperationContext) => Promise<Tx[]>
|
performAsync?: (ctx: SessionOperationContext) => Promise<Tx[]>
|
||||||
}> {
|
}> {
|
||||||
const result: Tx[] = []
|
const result: Tx[] = []
|
||||||
|
|
||||||
|
const suppressAsync = (ctx as SessionContextImpl).isAsyncContext ?? false
|
||||||
|
|
||||||
const asyncRequest: {
|
const asyncRequest: {
|
||||||
matches: Tx[]
|
matches: Tx[]
|
||||||
trigger: TriggerRecord['trigger']
|
trigger: TriggerRecord['trigger']
|
||||||
|
arrays: TriggerRecord['arrays']
|
||||||
}[] = []
|
}[] = []
|
||||||
|
|
||||||
const applyTrigger = async (
|
const applyTrigger = async (
|
||||||
ctx: SessionOperationContext,
|
ctx: SessionOperationContext,
|
||||||
matches: Tx[],
|
matches: Tx[],
|
||||||
trigger: TriggerRecord['trigger'],
|
{ trigger, arrays }: TriggerRecord,
|
||||||
result: Tx[]
|
result: Tx[]
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
for (const tx of matches) {
|
const group = groupByArray(matches, (it) => it.modifiedBy)
|
||||||
try {
|
|
||||||
result.push(
|
const tctrl: TriggerControl = {
|
||||||
...(await trigger.op(tx, {
|
...ctrl,
|
||||||
...ctrl,
|
operationContext: ctx,
|
||||||
ctx: ctx.ctx,
|
ctx: ctx.ctx,
|
||||||
txFactory: new TxFactory(tx.modifiedBy, true),
|
txFactory: new TxFactory(core.account.System, true),
|
||||||
findAll: async (clazz, query, options) => await ctrl.findAllCtx(ctx.ctx, clazz, query, options),
|
findAll: async (clazz, query, options) => await ctrl.findAllCtx(ctx.ctx, clazz, query, options),
|
||||||
apply: async (tx, broadcast, target) => {
|
apply: async (tx, needResult) => {
|
||||||
return await ctrl.applyCtx(ctx, tx, broadcast, target)
|
return await ctrl.applyCtx(ctx, tx, needResult)
|
||||||
},
|
},
|
||||||
result
|
result
|
||||||
}))
|
}
|
||||||
)
|
for (const [k, v] of group.entries()) {
|
||||||
} catch (err: any) {
|
const m = arrays ? [v] : v
|
||||||
ctx.ctx.error('failed to process trigger', { trigger: trigger.resource, tx, err })
|
tctrl.txFactory = new TxFactory(k, true)
|
||||||
Analytics.handleError(err)
|
for (const tx of m) {
|
||||||
|
try {
|
||||||
|
result.push(...(await trigger.op(tx, tctrl)))
|
||||||
|
} catch (err: any) {
|
||||||
|
ctx.ctx.error('failed to process trigger', { trigger: trigger.resource, tx, err })
|
||||||
|
Analytics.handleError(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises: Promise<void>[] = []
|
for (const { query, trigger, arrays } of this.triggers) {
|
||||||
for (const { query, trigger } of this.triggers) {
|
|
||||||
let matches = tx
|
let matches = tx
|
||||||
if (query !== undefined) {
|
if (query !== undefined) {
|
||||||
this.addDerived(query, 'objectClass')
|
this.addDerived(query, 'objectClass')
|
||||||
@ -121,23 +134,26 @@ export class Triggers {
|
|||||||
matches = matchQuery(tx, query, core.class.Tx, ctrl.hierarchy) as Tx[]
|
matches = matchQuery(tx, query, core.class.Tx, ctrl.hierarchy) as Tx[]
|
||||||
}
|
}
|
||||||
if (matches.length > 0) {
|
if (matches.length > 0) {
|
||||||
if (trigger.isAsync) {
|
if (trigger.isAsync && !suppressAsync) {
|
||||||
asyncRequest.push({
|
asyncRequest.push({
|
||||||
matches,
|
matches,
|
||||||
trigger
|
trigger,
|
||||||
|
arrays
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
promises.push(
|
await ctx.with(
|
||||||
ctx.with(trigger.resource, {}, async (ctx) => {
|
trigger.resource,
|
||||||
await applyTrigger(ctx, matches, trigger, result)
|
{},
|
||||||
})
|
async (ctx) => {
|
||||||
|
const tresult: Tx[] = []
|
||||||
|
await applyTrigger(ctx, matches, { trigger, arrays }, tresult)
|
||||||
|
result.push(...tresult)
|
||||||
|
},
|
||||||
|
{ count: matches.length, arrays }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Wait all regular triggers to complete in parallel
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transactions: result,
|
transactions: result,
|
||||||
performAsync:
|
performAsync:
|
||||||
@ -148,7 +164,7 @@ export class Triggers {
|
|||||||
for (const request of asyncRequest) {
|
for (const request of asyncRequest) {
|
||||||
try {
|
try {
|
||||||
await ctx.with(request.trigger.resource, {}, async (ctx) => {
|
await ctx.with(request.trigger.resource, {}, async (ctx) => {
|
||||||
await applyTrigger(ctx, request.matches, request.trigger, result)
|
await applyTrigger(ctx, request.matches, request, result)
|
||||||
})
|
})
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.ctx.error('failed to process trigger', {
|
ctx.ctx.error('failed to process trigger', {
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
import {
|
import {
|
||||||
MeasureMetricsContext,
|
MeasureMetricsContext,
|
||||||
type Account,
|
type Account,
|
||||||
|
type Branding,
|
||||||
type Class,
|
type Class,
|
||||||
type Doc,
|
type Doc,
|
||||||
type DocumentQuery,
|
type DocumentQuery,
|
||||||
@ -40,8 +41,7 @@ import {
|
|||||||
type TxFactory,
|
type TxFactory,
|
||||||
type TxResult,
|
type TxResult,
|
||||||
type WorkspaceId,
|
type WorkspaceId,
|
||||||
type WorkspaceIdWithUrl,
|
type WorkspaceIdWithUrl
|
||||||
type Branding
|
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import type { Asset, Resource } from '@hcengineering/platform'
|
import type { Asset, Resource } from '@hcengineering/platform'
|
||||||
import { type Readable } from 'stream'
|
import { type Readable } from 'stream'
|
||||||
@ -162,8 +162,8 @@ export interface TriggerControl {
|
|||||||
storageAdapter: StorageAdapter
|
storageAdapter: StorageAdapter
|
||||||
serviceAdaptersManager: ServiceAdaptersManager
|
serviceAdaptersManager: ServiceAdaptersManager
|
||||||
// Bulk operations in case trigger require some
|
// Bulk operations in case trigger require some
|
||||||
apply: (tx: Tx[], broadcast: boolean, target?: string[]) => Promise<TxResult>
|
apply: (tx: Tx[], needResult?: boolean) => Promise<TxResult>
|
||||||
applyCtx: (ctx: SessionOperationContext, tx: Tx[], broadcast: boolean, target?: string[]) => Promise<TxResult>
|
applyCtx: (ctx: SessionOperationContext, tx: Tx[], needResult?: boolean) => Promise<TxResult>
|
||||||
|
|
||||||
// Will create a live query if missing and return values immediately if already asked.
|
// Will create a live query if missing and return values immediately if already asked.
|
||||||
queryFind: <T extends Doc>(
|
queryFind: <T extends Doc>(
|
||||||
@ -179,7 +179,7 @@ export interface TriggerControl {
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type TriggerFunc = (tx: Tx, ctrl: TriggerControl) => Promise<Tx[]>
|
export type TriggerFunc = (tx: Tx | Tx[], ctrl: TriggerControl) => Promise<Tx[]>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -192,6 +192,9 @@ export interface Trigger extends Doc {
|
|||||||
|
|
||||||
// We should match transaction
|
// We should match transaction
|
||||||
txMatch?: DocumentQuery<Tx>
|
txMatch?: DocumentQuery<Tx>
|
||||||
|
|
||||||
|
// If set trigger will handle Tx[] instead of Tx
|
||||||
|
arrays?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -142,7 +142,8 @@ export class SessionContextImpl implements SessionContext {
|
|||||||
readonly admin: boolean | undefined,
|
readonly admin: boolean | undefined,
|
||||||
readonly derived: SessionContext['derived'],
|
readonly derived: SessionContext['derived'],
|
||||||
readonly workspace: WorkspaceIdWithUrl,
|
readonly workspace: WorkspaceIdWithUrl,
|
||||||
readonly branding: Branding | null
|
readonly branding: Branding | null,
|
||||||
|
readonly isAsyncContext: boolean
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
with<T>(
|
with<T>(
|
||||||
@ -163,7 +164,8 @@ export class SessionContextImpl implements SessionContext {
|
|||||||
this.admin,
|
this.admin,
|
||||||
this.derived,
|
this.derived,
|
||||||
this.workspace,
|
this.workspace,
|
||||||
this.branding
|
this.branding,
|
||||||
|
this.isAsyncContext
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
fullParams
|
fullParams
|
||||||
|
@ -32,7 +32,7 @@ import platform, { PlatformError, Severity, Status } from '@hcengineering/platfo
|
|||||||
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
|
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
|
||||||
import { DOMAIN_PREFERENCE } from '@hcengineering/server-preference'
|
import { DOMAIN_PREFERENCE } from '@hcengineering/server-preference'
|
||||||
import { BaseMiddleware } from './base'
|
import { BaseMiddleware } from './base'
|
||||||
import { getUser, mergeTargets } from './utils'
|
import { getUser } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -65,12 +65,7 @@ export class PrivateMiddleware extends BaseMiddleware implements Middleware {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const res = await this.provideTx(ctx, tx)
|
return await this.provideTx(ctx, tx)
|
||||||
// Add target to all broadcasts
|
|
||||||
ctx.derived.forEach((it) => {
|
|
||||||
it.target = mergeTargets(target, it.target)
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override async findAll<T extends Doc>(
|
override async findAll<T extends Doc>(
|
||||||
|
@ -335,10 +335,8 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
|||||||
await this.processPermissionsUpdatesFromTx(ctx, tx)
|
await this.processPermissionsUpdatesFromTx(ctx, tx)
|
||||||
await this.checkPermissions(ctx, tx)
|
await this.checkPermissions(ctx, tx)
|
||||||
const res = await this.provideTx(ctx, tx)
|
const res = await this.provideTx(ctx, tx)
|
||||||
for (const txd of ctx.derived) {
|
for (const txd of ctx.derived.txes) {
|
||||||
for (const tx of txd.derived) {
|
await this.processPermissionsUpdatesFromTx(ctx, txd)
|
||||||
await this.processPermissionsUpdatesFromTx(ctx, tx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@ -347,7 +345,9 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
|
|||||||
if (tx._class === core.class.TxApplyIf) {
|
if (tx._class === core.class.TxApplyIf) {
|
||||||
const applyTx = tx as TxApplyIf
|
const applyTx = tx as TxApplyIf
|
||||||
|
|
||||||
await Promise.all(applyTx.txes.map((t) => this.checkPermissions(ctx, t)))
|
for (const t of applyTx.txes) {
|
||||||
|
await this.checkPermissions(ctx, t)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ import core, {
|
|||||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||||
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
|
import { Middleware, SessionContext, TxMiddlewareResult, type ServerStorage } from '@hcengineering/server-core'
|
||||||
import { BaseMiddleware } from './base'
|
import { BaseMiddleware } from './base'
|
||||||
import { getUser, isOwner, isSystem, mergeTargets } from './utils'
|
import { getUser, isOwner, isSystem } from './utils'
|
||||||
|
|
||||||
type SpaceWithMembers = Pick<Space, '_id' | 'members' | 'private' | '_class'>
|
type SpaceWithMembers = Pick<Space, '_id' | 'members' | 'private' | '_class'>
|
||||||
|
|
||||||
@ -246,10 +246,13 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
space: core.space.DerivedTx,
|
space: core.space.DerivedTx,
|
||||||
params: null
|
params: null
|
||||||
}
|
}
|
||||||
ctx.derived.push({
|
ctx.derived.txes.push(tx)
|
||||||
derived: [tx],
|
ctx.derived.targets.security = (it) => {
|
||||||
target: targets
|
// TODO: I'm not sure it is called
|
||||||
})
|
if (it._id === tx._id) {
|
||||||
|
return targets
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async broadcastNonMembers (ctx: SessionContext, space: SpaceWithMembers): Promise<void> {
|
private async broadcastNonMembers (ctx: SessionContext, space: SpaceWithMembers): Promise<void> {
|
||||||
@ -413,17 +416,10 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||||
}
|
}
|
||||||
await this.processTx(ctx, tx)
|
await this.processTx(ctx, tx)
|
||||||
const targets = await this.getTxTargets(ctx, tx)
|
|
||||||
const res = await this.provideTx(ctx, tx)
|
const res = await this.provideTx(ctx, tx)
|
||||||
for (const txd of ctx.derived) {
|
for (const txd of ctx.derived.txes) {
|
||||||
for (const tx of txd.derived) {
|
await this.processTx(ctx, txd)
|
||||||
await this.processTx(ctx, tx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ctx.derived.forEach((it) => {
|
|
||||||
it.target = mergeTargets(targets, it.target)
|
|
||||||
})
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,6 +431,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
|
|||||||
for (const tx of txes) {
|
for (const tx of txes) {
|
||||||
const h = this.storage.hierarchy
|
const h = this.storage.hierarchy
|
||||||
if (h.isDerived(tx._class, core.class.TxCUD)) {
|
if (h.isDerived(tx._class, core.class.TxCUD)) {
|
||||||
|
// TODO: Do we need security check here?
|
||||||
const cudTx = tx as TxCUD<Doc>
|
const cudTx = tx as TxCUD<Doc>
|
||||||
await this.processTxSpaceDomain(cudTx)
|
await this.processTxSpaceDomain(cudTx)
|
||||||
} else if (tx._class === core.class.TxWorkspaceEvent) {
|
} else if (tx._class === core.class.TxWorkspaceEvent) {
|
||||||
|
@ -17,18 +17,6 @@ import core, { Account, AccountRole, systemAccountEmail } from '@hcengineering/c
|
|||||||
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
|
||||||
import { SessionContext, type ServerStorage } from '@hcengineering/server-core'
|
import { SessionContext, type ServerStorage } from '@hcengineering/server-core'
|
||||||
|
|
||||||
export function mergeTargets (current: string[] | undefined, prev: string[] | undefined): string[] | undefined {
|
|
||||||
if (current === undefined) return prev
|
|
||||||
if (prev === undefined) return current
|
|
||||||
const res: string[] = []
|
|
||||||
for (const value of current) {
|
|
||||||
if (prev.includes(value)) {
|
|
||||||
res.push(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUser (storage: ServerStorage, ctx: SessionContext): Promise<Account> {
|
export async function getUser (storage: ServerStorage, ctx: SessionContext): Promise<Account> {
|
||||||
if (ctx.userEmail === undefined) {
|
if (ctx.userEmail === undefined) {
|
||||||
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
|
||||||
|
@ -182,7 +182,10 @@ describe('mongo operations', () => {
|
|||||||
})
|
})
|
||||||
const soCtx: SessionOperationContext = {
|
const soCtx: SessionOperationContext = {
|
||||||
ctx,
|
ctx,
|
||||||
derived: [],
|
derived: {
|
||||||
|
txes: [],
|
||||||
|
targets: {}
|
||||||
|
},
|
||||||
with: async <T>(
|
with: async <T>(
|
||||||
name: string,
|
name: string,
|
||||||
params: ParamsType,
|
params: ParamsType,
|
||||||
@ -206,7 +209,6 @@ describe('mongo operations', () => {
|
|||||||
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
|
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
|
||||||
loadModel: async () => txes,
|
loadModel: async () => txes,
|
||||||
getAccount: async () => ({}) as any,
|
getAccount: async () => ({}) as any,
|
||||||
measure: async () => async () => ({ time: 0, serverTime: 0 }),
|
|
||||||
sendForceClose: async () => {}
|
sendForceClose: async () => {}
|
||||||
}
|
}
|
||||||
return st
|
return st
|
||||||
|
@ -114,9 +114,13 @@ export class ClientSession implements Session {
|
|||||||
this.token.email,
|
this.token.email,
|
||||||
this.sessionId,
|
this.sessionId,
|
||||||
this.token.extra?.admin === 'true',
|
this.token.extra?.admin === 'true',
|
||||||
[],
|
{
|
||||||
|
txes: [],
|
||||||
|
targets: {}
|
||||||
|
},
|
||||||
this._pipeline.storage.workspaceId,
|
this._pipeline.storage.workspaceId,
|
||||||
this._pipeline.storage.branding
|
this._pipeline.storage.branding,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
await this._pipeline.tx(context, createTx)
|
await this._pipeline.tx(context, createTx)
|
||||||
const acc = TxProcessor.createDoc2Doc(createTx)
|
const acc = TxProcessor.createDoc2Doc(createTx)
|
||||||
@ -144,9 +148,13 @@ export class ClientSession implements Session {
|
|||||||
this.token.email,
|
this.token.email,
|
||||||
this.sessionId,
|
this.sessionId,
|
||||||
this.token.extra?.admin === 'true',
|
this.token.extra?.admin === 'true',
|
||||||
[],
|
{
|
||||||
|
txes: [],
|
||||||
|
targets: {}
|
||||||
|
},
|
||||||
this._pipeline.storage.workspaceId,
|
this._pipeline.storage.workspaceId,
|
||||||
this._pipeline.storage.branding
|
this._pipeline.storage.branding,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
return await this._pipeline.findAll(context, _class, query, options)
|
return await this._pipeline.findAll(context, _class, query, options)
|
||||||
}
|
}
|
||||||
@ -167,9 +175,13 @@ export class ClientSession implements Session {
|
|||||||
this.token.email,
|
this.token.email,
|
||||||
this.sessionId,
|
this.sessionId,
|
||||||
this.token.extra?.admin === 'true',
|
this.token.extra?.admin === 'true',
|
||||||
[],
|
{
|
||||||
|
txes: [],
|
||||||
|
targets: {}
|
||||||
|
},
|
||||||
this._pipeline.storage.workspaceId,
|
this._pipeline.storage.workspaceId,
|
||||||
this._pipeline.storage.branding
|
this._pipeline.storage.branding,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
await ctx.sendResponse(await this._pipeline.searchFulltext(context, query, options))
|
await ctx.sendResponse(await this._pipeline.searchFulltext(context, query, options))
|
||||||
}
|
}
|
||||||
@ -183,9 +195,13 @@ export class ClientSession implements Session {
|
|||||||
this.token.email,
|
this.token.email,
|
||||||
this.sessionId,
|
this.sessionId,
|
||||||
this.token.extra?.admin === 'true',
|
this.token.extra?.admin === 'true',
|
||||||
[],
|
{
|
||||||
|
txes: [],
|
||||||
|
targets: {}
|
||||||
|
},
|
||||||
this._pipeline.storage.workspaceId,
|
this._pipeline.storage.workspaceId,
|
||||||
this._pipeline.storage.branding
|
this._pipeline.storage.branding,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
const result = await this._pipeline.tx(context, tx)
|
const result = await this._pipeline.tx(context, tx)
|
||||||
@ -209,18 +225,24 @@ export class ClientSession implements Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Put current user as send target
|
// Put current user as send target
|
||||||
toSendTarget.set(this.getUser(), [])
|
for (const txd of context.derived.txes) {
|
||||||
for (const txd of context.derived) {
|
let target: string[] | undefined
|
||||||
if (txd.target === undefined) {
|
for (const tt of Object.values(context.derived.targets ?? {})) {
|
||||||
getTxes('') // be sure we have empty one
|
target = tt(txd)
|
||||||
|
if (target !== undefined) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target === undefined) {
|
||||||
|
getTxes('') // Be sure we have empty one
|
||||||
|
|
||||||
// Also add to all other targeted sends
|
// Also add to all other targeted sends
|
||||||
for (const v of toSendTarget.values()) {
|
for (const v of toSendTarget.values()) {
|
||||||
v.push(...txd.derived)
|
v.push(txd)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const t of txd.target) {
|
for (const t of target) {
|
||||||
getTxes(t).push(...txd.derived)
|
getTxes(t).push(txd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -912,10 +912,6 @@ class TSessionManager implements SessionManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (request.method === 'measure' || request.method === 'measure-done') {
|
|
||||||
await this.handleMeasure<S>(service, request, opContext(ctx))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
service.requests.set(reqId, {
|
service.requests.set(reqId, {
|
||||||
id: reqId,
|
id: reqId,
|
||||||
params: request,
|
params: request,
|
||||||
@ -930,9 +926,7 @@ class TSessionManager implements SessionManager {
|
|||||||
try {
|
try {
|
||||||
const params = [...request.params]
|
const params = [...request.params]
|
||||||
|
|
||||||
service.measureCtx?.ctx !== undefined
|
await ctx.with('🧨 process', {}, async (callTx) => f.apply(service, [opContext(callTx), ...params]))
|
||||||
? await f.apply(service, [opContext(service.measureCtx?.ctx), ...params])
|
|
||||||
: await ctx.with('🧨 process', {}, async (callTx) => f.apply(service, [opContext(callTx), ...params]))
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Analytics.handleError(err)
|
Analytics.handleError(err)
|
||||||
if (LOGGING_ENABLED) {
|
if (LOGGING_ENABLED) {
|
||||||
@ -955,34 +949,7 @@ class TSessionManager implements SessionManager {
|
|||||||
service.requests.delete(reqId)
|
service.requests.delete(reqId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleMeasure<S extends Session>(
|
|
||||||
service: S,
|
|
||||||
request: Request<any[]>,
|
|
||||||
ctx: ClientSessionCtx
|
|
||||||
): Promise<void> {
|
|
||||||
let serverTime = 0
|
|
||||||
if (request.method === 'measure') {
|
|
||||||
service.measureCtx = { ctx: ctx.ctx.newChild('📶 ' + request.params[0], {}), time: Date.now() }
|
|
||||||
} else {
|
|
||||||
if (service.measureCtx !== undefined) {
|
|
||||||
serverTime = Date.now() - service.measureCtx.time
|
|
||||||
service.measureCtx.ctx.end(serverTime)
|
|
||||||
service.measureCtx = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await ctx.sendResponse(request.method === 'measure' ? 'started' : serverTime)
|
|
||||||
} catch (err: any) {
|
|
||||||
Analytics.handleError(err)
|
|
||||||
if (LOGGING_ENABLED) {
|
|
||||||
ctx.ctx.error('error handle measure', { error: err, request })
|
|
||||||
}
|
|
||||||
await ctx.sendError(JSON.parse(JSON.stringify(err?.stack)), unknownError(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
@ -77,8 +77,6 @@ export interface Session {
|
|||||||
current: StatisticsElement
|
current: StatisticsElement
|
||||||
mins5: StatisticsElement
|
mins5: StatisticsElement
|
||||||
|
|
||||||
measureCtx?: { ctx: MeasureContext, time: number }
|
|
||||||
|
|
||||||
lastRequest: number
|
lastRequest: number
|
||||||
|
|
||||||
isUpgradeClient: () => boolean
|
isUpgradeClient: () => boolean
|
||||||
|
Loading…
Reference in New Issue
Block a user