mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-25 09:50:19 +00:00
Activity update (#362)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
581f5f0e96
commit
6c281b38f8
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@ -17,6 +17,17 @@
|
|||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"cwd": "${workspaceRoot}/server/server",
|
"cwd": "${workspaceRoot}/server/server",
|
||||||
"protocol": "inspector"
|
"protocol": "inspector"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug Jest tests",
|
||||||
|
"program": "${fileDirname}/../../node_modules/@rushstack/heft/lib/start.js",
|
||||||
|
"cwd": "${fileDirname}/../../",
|
||||||
|
"args": ["--debug", "test", "--clean", "--test-path-pattern", "${file}"],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"protocol": "inspector"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,15 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import { PlatformError, Severity, Status } from '@anticrm/platform'
|
import { PlatformError, Severity, Status } from '@anticrm/platform'
|
||||||
|
import clone from 'just-clone'
|
||||||
import type { Class, Doc, Ref } from './classes'
|
import type { Class, Doc, Ref } from './classes'
|
||||||
import type { Tx, TxCreateDoc, TxMixin, TxPutBag, TxRemoveDoc, TxUpdateDoc } from './tx'
|
|
||||||
import core from './component'
|
import core from './component'
|
||||||
import type { Hierarchy } from './hierarchy'
|
import type { Hierarchy } from './hierarchy'
|
||||||
import { _getOperator } from './operator'
|
|
||||||
import { findProperty, resultSort } from './query'
|
import { findProperty, resultSort } from './query'
|
||||||
import type { DocumentQuery, FindOptions, FindResult, Storage, WithLookup, LookupData, Refs, TxResult } from './storage'
|
import type { DocumentQuery, FindOptions, FindResult, LookupData, Refs, Storage, TxResult, WithLookup } from './storage'
|
||||||
|
import type { Tx, TxCreateDoc, TxMixin, TxPutBag, TxRemoveDoc, TxUpdateDoc } from './tx'
|
||||||
import { TxProcessor } from './tx'
|
import { TxProcessor } from './tx'
|
||||||
|
|
||||||
import clone from 'just-clone'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -200,17 +198,7 @@ export class ModelDb extends MemDb implements Storage {
|
|||||||
|
|
||||||
protected async txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult> {
|
protected async txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult> {
|
||||||
const doc = this.getObject(tx.objectId) as any
|
const doc = this.getObject(tx.objectId) as any
|
||||||
const ops = tx.operations as any
|
TxProcessor.updateDoc2Doc(doc, tx)
|
||||||
for (const key in ops) {
|
|
||||||
if (key.startsWith('$')) {
|
|
||||||
const operator = _getOperator(key)
|
|
||||||
operator(doc, ops[key])
|
|
||||||
} else {
|
|
||||||
doc[key] = ops[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
doc.modifiedBy = tx.modifiedBy
|
|
||||||
doc.modifiedOn = tx.modifiedOn
|
|
||||||
return tx.retrieve === true ? { object: doc } : {}
|
return tx.retrieve === true ? { object: doc } : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import type { Class, Data, Doc, Domain, Ref, Account, Space, Arr, Mixin, Propert
|
|||||||
import type { DocumentQuery, FindOptions, FindResult, Storage, WithLookup, TxResult } from './storage'
|
import type { DocumentQuery, FindOptions, FindResult, Storage, WithLookup, TxResult } from './storage'
|
||||||
import core from './component'
|
import core from './component'
|
||||||
import { generateId } from './utils'
|
import { generateId } from './utils'
|
||||||
|
import { _getOperator } from '.'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -223,6 +224,21 @@ export abstract class TxProcessor implements WithTx {
|
|||||||
} as T
|
} as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static updateDoc2Doc<T extends Doc>(doc: T, tx: TxUpdateDoc<T>): T {
|
||||||
|
const ops = tx.operations as any
|
||||||
|
for (const key in ops) {
|
||||||
|
if (key.startsWith('$')) {
|
||||||
|
const operator = _getOperator(key)
|
||||||
|
operator(doc, ops[key])
|
||||||
|
} else {
|
||||||
|
(doc as any)[key] = ops[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc.modifiedBy = tx.modifiedBy
|
||||||
|
doc.modifiedOn = tx.modifiedOn
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract txCreateDoc (tx: TxCreateDoc<Doc>): Promise<TxResult>
|
protected abstract txCreateDoc (tx: TxCreateDoc<Doc>): Promise<TxResult>
|
||||||
protected abstract txPutBag (tx: TxPutBag<PropertyType>): Promise<TxResult>
|
protected abstract txPutBag (tx: TxPutBag<PropertyType>): Promise<TxResult>
|
||||||
protected abstract txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult>
|
protected abstract txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult>
|
||||||
@ -328,6 +344,25 @@ export class TxOperations implements Storage {
|
|||||||
return tx.objectId
|
return tx.objectId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeCollection<T extends Doc, P extends AttachedDoc>(
|
||||||
|
_class: Ref<Class<P>>,
|
||||||
|
space: Ref<Space>,
|
||||||
|
objectId: Ref<P>,
|
||||||
|
attachedTo: Ref<T>,
|
||||||
|
attachedToClass: Ref<Class<T>>,
|
||||||
|
collection: string
|
||||||
|
): Promise<Ref<T>> {
|
||||||
|
const tx = this.txFactory.createTxCollectionCUD(
|
||||||
|
attachedToClass,
|
||||||
|
attachedTo,
|
||||||
|
space,
|
||||||
|
collection,
|
||||||
|
this.txFactory.createTxRemoveDoc(_class, space, objectId)
|
||||||
|
)
|
||||||
|
await this.storage.tx(tx)
|
||||||
|
return tx.objectId
|
||||||
|
}
|
||||||
|
|
||||||
putBag <P extends PropertyType>(
|
putBag <P extends PropertyType>(
|
||||||
_class: Ref<Class<Doc>>,
|
_class: Ref<Class<Doc>>,
|
||||||
space: Ref<Space>,
|
space: Ref<Space>,
|
||||||
|
@ -58,7 +58,7 @@ export function setClient(_client: Client) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LiveQuery {
|
export class LiveQuery {
|
||||||
private unsubscribe = () => {}
|
private unsubscribe = () => {}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -104,7 +104,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const whileShow = (ev: MouseEvent): void => {
|
const whileShow = (ev: MouseEvent): void => {
|
||||||
if ($tooltip.element) {
|
if ($tooltip.element && tooltipHTML) {
|
||||||
const rectP = tooltipHTML.getBoundingClientRect()
|
const rectP = tooltipHTML.getBoundingClientRect()
|
||||||
const rectT = {
|
const rectT = {
|
||||||
top: (dir === 'top') ? rect.top - 16 : rect.top,
|
top: (dir === 'top') ? rect.top - 16 : rect.top,
|
||||||
|
253
plugins/activity-resources/src/activity.ts
Normal file
253
plugins/activity-resources/src/activity.ts
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import core, {
|
||||||
|
AttachedDoc,
|
||||||
|
Class,
|
||||||
|
Client,
|
||||||
|
Doc,
|
||||||
|
DocumentUpdate,
|
||||||
|
Hierarchy,
|
||||||
|
Ref,
|
||||||
|
SortingOrder,
|
||||||
|
Tx,
|
||||||
|
TxCollectionCUD,
|
||||||
|
TxCreateDoc,
|
||||||
|
TxCUD,
|
||||||
|
TxProcessor,
|
||||||
|
TxUpdateDoc
|
||||||
|
} from '@anticrm/core'
|
||||||
|
import { createQuery, LiveQuery } from '@anticrm/presentation'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type ActivityKey = string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function activityKey (objectClass: Ref<Class<Doc>>, txClass: Ref<Class<Tx>>): ActivityKey {
|
||||||
|
return objectClass + ':' + txClass
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEqualOps (op1: DocumentUpdate<Doc>, op2: DocumentUpdate<Doc>): boolean {
|
||||||
|
const o1 = Object.keys(op1).sort().join('-')
|
||||||
|
const o2 = Object.keys(op2).sort().join('-')
|
||||||
|
return o1 === o2
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transaction being displayed.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface DisplayTx {
|
||||||
|
// Source tx
|
||||||
|
tx: TxCUD<Doc>
|
||||||
|
|
||||||
|
// A set of collapsed transactions.
|
||||||
|
txes: Array<TxCUD<Doc>>
|
||||||
|
|
||||||
|
// type check for createTx
|
||||||
|
createTx?: TxCreateDoc<Doc>
|
||||||
|
|
||||||
|
// Type check for updateTx
|
||||||
|
updateTx?: TxUpdateDoc<Doc>
|
||||||
|
|
||||||
|
// Document in case it is required.
|
||||||
|
doc?: Doc
|
||||||
|
|
||||||
|
removed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type DisplayTxListener = (txes: DisplayTx[]) => void
|
||||||
|
|
||||||
|
// Use 5 minutes to combine similar transactions.
|
||||||
|
const combineThreshold = 5 * 60 * 1000
|
||||||
|
/**
|
||||||
|
* Define activity.
|
||||||
|
*
|
||||||
|
* Allow to recieve a list of transactions and notify client about it.
|
||||||
|
*/
|
||||||
|
export interface Activity {
|
||||||
|
update: (object: Doc, listener: DisplayTxListener, sort: SortingOrder) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActivityImpl implements Activity {
|
||||||
|
private readonly txQuery1: LiveQuery
|
||||||
|
private readonly txQuery2: LiveQuery
|
||||||
|
|
||||||
|
private txes1: Array<TxCUD<Doc>> = []
|
||||||
|
private txes2: Array<TxCUD<Doc>> = []
|
||||||
|
constructor (readonly client: Client) {
|
||||||
|
this.txQuery1 = createQuery()
|
||||||
|
this.txQuery2 = createQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
private notify (object: Doc, listener: DisplayTxListener, sort: SortingOrder): void {
|
||||||
|
this.combineTransactions(object, this.txes1, this.txes2).then(
|
||||||
|
(result) => {
|
||||||
|
const sorted = result.sort((a, b) => (a.tx.modifiedOn - b.tx.modifiedOn) * sort)
|
||||||
|
listener(sorted)
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
update (object: Doc, listener: DisplayTxListener, sort: SortingOrder): void {
|
||||||
|
let isAttached = false
|
||||||
|
|
||||||
|
isAttached = this.client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)
|
||||||
|
|
||||||
|
this.txQuery1.query<TxCollectionCUD<Doc, AttachedDoc>>(
|
||||||
|
isAttached ? core.class.TxCollectionCUD : core.class.TxCUD,
|
||||||
|
isAttached
|
||||||
|
? { 'tx.objectId': object._id as Ref<AttachedDoc> }
|
||||||
|
: {
|
||||||
|
objectId: object._id,
|
||||||
|
_class: { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc] }
|
||||||
|
},
|
||||||
|
(result) => {
|
||||||
|
this.txes1 = result
|
||||||
|
this.notify(object, listener, sort)
|
||||||
|
},
|
||||||
|
{ sort: { modifiedOn: SortingOrder.Descending } }
|
||||||
|
)
|
||||||
|
|
||||||
|
this.txQuery2.query<TxCUD<Doc>>(
|
||||||
|
core.class.TxCollectionCUD,
|
||||||
|
{
|
||||||
|
objectId: object._id,
|
||||||
|
'tx._class': { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc] }
|
||||||
|
},
|
||||||
|
(result) => {
|
||||||
|
this.txes2 = result
|
||||||
|
this.notify(object, listener, sort)
|
||||||
|
},
|
||||||
|
{ sort: { modifiedOn: SortingOrder.Descending } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async combineTransactions (object: Doc, txes1: Array<TxCUD<Doc>>, txes2: Array<TxCUD<Doc>>): Promise<DisplayTx[]> {
|
||||||
|
const hierarchy = this.client.getHierarchy()
|
||||||
|
|
||||||
|
// We need to sort with with natural order, to build a proper doc values.
|
||||||
|
const allTx = Array.from(txes1).concat(txes2).sort(this.sortByLastModified)
|
||||||
|
const txCUD: Array<TxCUD<Doc>> = this.filterTxCUD(allTx, hierarchy)
|
||||||
|
const parents = new Map<Ref<Doc>, DisplayTx>()
|
||||||
|
|
||||||
|
const results: DisplayTx[] = []
|
||||||
|
|
||||||
|
for (const tx of txCUD) {
|
||||||
|
const { collectionCUD, updateCUD, result, tx: ntx } = this.createDisplayTx(tx, parents)
|
||||||
|
|
||||||
|
// We do not need collection object updates, in main list of displayed transactions.
|
||||||
|
if (this.isDisplayTxRequired(collectionCUD, updateCUD, ntx, object)) {
|
||||||
|
// Combine previous update transaction for same field and if same operation and time treshold is ok
|
||||||
|
this.checkIntegratePreviousTx(results, result)
|
||||||
|
results.push(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortByLastModified (a: TxCUD<Doc>, b: TxCUD<Doc>): number {
|
||||||
|
return a.modifiedOn - b.modifiedOn
|
||||||
|
}
|
||||||
|
|
||||||
|
isDisplayTxRequired (collectionCUD: boolean, updateCUD: boolean, ntx: TxCUD<Doc>, object: Doc): boolean {
|
||||||
|
return !(collectionCUD && updateCUD) || ntx.objectId === object._id
|
||||||
|
}
|
||||||
|
|
||||||
|
filterTxCUD (allTx: Array<TxCUD<Doc>>, hierarchy: Hierarchy): Array<TxCUD<Doc>> {
|
||||||
|
return allTx.filter((tx) => hierarchy.isDerived(tx._class, core.class.TxCUD))
|
||||||
|
}
|
||||||
|
|
||||||
|
createDisplayTx (
|
||||||
|
tx: TxCUD<Doc>,
|
||||||
|
parents: Map<Ref<Doc>, DisplayTx>
|
||||||
|
): { collectionCUD: boolean, updateCUD: boolean, result: DisplayTx, tx: TxCUD<Doc> } {
|
||||||
|
let collectionCUD = false
|
||||||
|
let updateCUD = false
|
||||||
|
const hierarchy = this.client.getHierarchy()
|
||||||
|
if (hierarchy.isDerived(tx._class, core.class.TxCollectionCUD)) {
|
||||||
|
tx = (tx as TxCollectionCUD<Doc, AttachedDoc>).tx
|
||||||
|
collectionCUD = true
|
||||||
|
}
|
||||||
|
let firstTx = parents.get(tx.objectId)
|
||||||
|
const result: DisplayTx = this.newDisplayTx(tx)
|
||||||
|
|
||||||
|
result.doc = firstTx?.doc ?? result.doc
|
||||||
|
|
||||||
|
firstTx = firstTx ?? result
|
||||||
|
parents.set(tx.objectId, firstTx)
|
||||||
|
|
||||||
|
// If we have updates also apply them all.
|
||||||
|
updateCUD = this.updateFirstTx(result, firstTx)
|
||||||
|
|
||||||
|
if (hierarchy.isDerived(tx._class, core.class.TxRemoveDoc) && result.doc !== undefined) {
|
||||||
|
firstTx.removed = true
|
||||||
|
}
|
||||||
|
return { collectionCUD, updateCUD, result, tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFirstTx (result: DisplayTx, firstTx: DisplayTx): boolean {
|
||||||
|
if (this.client.getHierarchy().isDerived(result.tx._class, core.class.TxUpdateDoc) && result.doc !== undefined) {
|
||||||
|
firstTx.doc = TxProcessor.updateDoc2Doc(result.doc, result.tx as TxUpdateDoc<Doc>)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
newDisplayTx (tx: TxCUD<Doc>): DisplayTx {
|
||||||
|
const hierarchy = this.client.getHierarchy()
|
||||||
|
const createTx = hierarchy.isDerived(tx._class, core.class.TxCreateDoc) ? (tx as TxCreateDoc<Doc>) : undefined
|
||||||
|
return {
|
||||||
|
tx,
|
||||||
|
txes: [],
|
||||||
|
createTx,
|
||||||
|
updateTx: hierarchy.isDerived(tx._class, core.class.TxUpdateDoc) ? (tx as TxUpdateDoc<Doc>) : undefined,
|
||||||
|
removed: false,
|
||||||
|
doc: createTx !== undefined ? TxProcessor.createDoc2Doc(createTx) : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIntegratePreviousTx (results: DisplayTx[], result: DisplayTx): void {
|
||||||
|
if (results.length > 0) {
|
||||||
|
const prevTx = results[results.length - 1]
|
||||||
|
if (this.isSameKindTx(prevTx, result)) {
|
||||||
|
const prevUpdate = prevTx.tx as unknown as TxUpdateDoc<Doc>
|
||||||
|
const curUpdate = result.tx as unknown as TxUpdateDoc<Doc>
|
||||||
|
if (
|
||||||
|
isEqualOps(prevUpdate.operations, curUpdate.operations) &&
|
||||||
|
result.tx.modifiedOn - prevUpdate.modifiedOn < combineThreshold
|
||||||
|
) {
|
||||||
|
// we have same keys, l
|
||||||
|
// Remember previous transactions
|
||||||
|
result.txes.push(...prevTx.txes, prevTx.tx)
|
||||||
|
// Remove last item
|
||||||
|
results.splice(results.length - 1, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isSameKindTx (prevTx: DisplayTx, result: DisplayTx): boolean {
|
||||||
|
return (
|
||||||
|
prevTx.tx.objectId === result.tx.objectId && // Same document id
|
||||||
|
prevTx.tx._class === result.tx._class && // Same transaction class
|
||||||
|
result.tx._class === core.class.TxUpdateDoc &&
|
||||||
|
prevTx.tx.modifiedBy === result.tx.modifiedBy // Same user
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct an new activity, to listend for displayed transactions in UI.
|
||||||
|
* @param client
|
||||||
|
*/
|
||||||
|
export function newActivity (client: Client): Activity {
|
||||||
|
return new ActivityImpl(client)
|
||||||
|
}
|
@ -13,62 +13,31 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import activity, { TxViewlet } from '@anticrm/activity'
|
import activity, { TxViewlet } from '@anticrm/activity'
|
||||||
import chunter from '@anticrm/chunter'
|
import chunter from '@anticrm/chunter'
|
||||||
import type { AttachedDoc, Doc, Ref, TxCollectionCUD, TxCUD } from '@anticrm/core'
|
import { Doc, SortingOrder } from '@anticrm/core'
|
||||||
import core, { SortingOrder } from '@anticrm/core'
|
|
||||||
import { createQuery, getClient } from '@anticrm/presentation'
|
import { createQuery, getClient } from '@anticrm/presentation'
|
||||||
import { ReferenceInput } from '@anticrm/text-editor'
|
import { ReferenceInput } from '@anticrm/text-editor'
|
||||||
import { Grid, IconActivity, ScrollBox } from '@anticrm/ui'
|
import { Grid, IconActivity, ScrollBox } from '@anticrm/ui'
|
||||||
import { ActivityKey, activityKey } from '../utils'
|
import { ActivityKey, activityKey, DisplayTx, newActivity } from '../activity'
|
||||||
import TxView from './TxView.svelte'
|
import TxView from './TxView.svelte'
|
||||||
|
|
||||||
export let fullSize: boolean = false
|
export let fullSize: boolean = false
|
||||||
export let object: Doc
|
export let object: Doc
|
||||||
|
|
||||||
let txes1: TxCUD<Doc>[] = []
|
let txes: DisplayTx[] = []
|
||||||
let txes2: TxCUD<Doc>[] = []
|
|
||||||
|
|
||||||
let txes: TxCUD<Doc>[]
|
|
||||||
|
|
||||||
$: txes = Array.from(txes1)
|
|
||||||
.concat(txes2)
|
|
||||||
.sort((a, b) => b.modifiedOn - a.modifiedOn)
|
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
const txQuery1 = createQuery()
|
const activityQuery = newActivity(client)
|
||||||
const txQuery2 = createQuery()
|
|
||||||
|
|
||||||
let isAttached = false
|
$: activityQuery.update(
|
||||||
|
object,
|
||||||
$: isAttached = client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)
|
|
||||||
$: txQuery1.query<TxCollectionCUD<Doc, AttachedDoc>>(
|
|
||||||
isAttached ? core.class.TxCollectionCUD : core.class.TxCUD,
|
|
||||||
isAttached
|
|
||||||
? { 'tx.objectId': object._id as Ref<AttachedDoc> }
|
|
||||||
: {
|
|
||||||
objectId: object._id,
|
|
||||||
_class: { $in: [core.class.TxCreateDoc, core.class.TxUpdateDoc, core.class.TxRemoveDoc] }
|
|
||||||
},
|
|
||||||
(result) => {
|
(result) => {
|
||||||
txes1 = result
|
txes = result
|
||||||
},
|
},
|
||||||
{ sort: { modifiedOn: SortingOrder.Descending } }
|
SortingOrder.Descending
|
||||||
)
|
|
||||||
|
|
||||||
$: txQuery2.query<TxCUD<Doc>>(
|
|
||||||
core.class.TxCollectionCUD,
|
|
||||||
{
|
|
||||||
objectId: object._id,
|
|
||||||
'tx._class': { $in: [core.class.TxCreateDoc, core.class.TxRemoveDoc] }
|
|
||||||
},
|
|
||||||
(result) => {
|
|
||||||
txes2 = result
|
|
||||||
},
|
|
||||||
{ sort: { modifiedOn: SortingOrder.Descending } }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
function onMessage (event: CustomEvent) {
|
function onMessage (event: CustomEvent) {
|
||||||
@ -94,7 +63,7 @@
|
|||||||
<ScrollBox vertical stretch>
|
<ScrollBox vertical stretch>
|
||||||
{#if txes}
|
{#if txes}
|
||||||
<Grid column={1} rowGap={1.5}>
|
<Grid column={1} rowGap={1.5}>
|
||||||
{#each txes as tx}
|
{#each txes as tx (tx.tx._id)}
|
||||||
<TxView {tx} {viewlets} />
|
<TxView {tx} {viewlets} />
|
||||||
{/each}
|
{/each}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -13,101 +13,98 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TxViewlet } from '@anticrm/activity'
|
import type { TxViewlet } from '@anticrm/activity'
|
||||||
import activity from '@anticrm/activity'
|
import activity from '@anticrm/activity'
|
||||||
import contact, { EmployeeAccount, formatName } from '@anticrm/contact'
|
import contact, { EmployeeAccount, formatName } from '@anticrm/contact'
|
||||||
import core, {
|
import core, { Class, Doc, Ref, TxCUD, TxUpdateDoc } from '@anticrm/core'
|
||||||
AttachedDoc,
|
|
||||||
Doc,
|
|
||||||
Ref,
|
|
||||||
Tx,
|
|
||||||
TxCollectionCUD,
|
|
||||||
TxCreateDoc,
|
|
||||||
TxCUD,
|
|
||||||
TxProcessor,
|
|
||||||
TxUpdateDoc
|
|
||||||
} from '@anticrm/core'
|
|
||||||
import { IntlString } from '@anticrm/platform'
|
import { IntlString } from '@anticrm/platform'
|
||||||
import { getClient } from '@anticrm/presentation'
|
import { getClient } from '@anticrm/presentation'
|
||||||
import { AnyComponent, AnySvelteComponent, Component, Icon, Label, TimeSince } from '@anticrm/ui'
|
import { AnyComponent, AnySvelteComponent, Component, Icon, Label, TimeSince } from '@anticrm/ui'
|
||||||
import type { AttributeModel } from '@anticrm/view'
|
import type { AttributeModel } from '@anticrm/view'
|
||||||
import { buildModel, getObjectPresenter } from '@anticrm/view-resources'
|
import { buildModel, getObjectPresenter } from '@anticrm/view-resources'
|
||||||
import { activityKey, ActivityKey } from '../utils'
|
import { activityKey, ActivityKey, DisplayTx } from '../activity'
|
||||||
|
|
||||||
export let tx: Tx
|
export let tx: DisplayTx
|
||||||
export let viewlets: Map<ActivityKey, TxViewlet>
|
export let viewlets: Map<ActivityKey, TxViewlet>
|
||||||
|
|
||||||
type TxDisplayViewlet =
|
type TxDisplayViewlet =
|
||||||
| (Pick<TxViewlet, 'icon' | 'label' | 'display'> & { component?: AnyComponent | AnySvelteComponent })
|
| (Pick<TxViewlet, 'icon' | 'label' | 'display'> & { component?: AnyComponent | AnySvelteComponent })
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
let viewlet: TxDisplayViewlet
|
let ptx: DisplayTx | undefined
|
||||||
|
|
||||||
|
let viewlet: TxDisplayViewlet | undefined
|
||||||
let props: any
|
let props: any
|
||||||
let displayTx: TxCUD<Doc> | undefined
|
let employee: EmployeeAccount | undefined
|
||||||
let utx: TxUpdateDoc<Doc> | undefined
|
let model: AttributeModel[] = []
|
||||||
|
|
||||||
|
$: if (tx.tx._id !== ptx?.tx._id) {
|
||||||
|
viewlet = undefined
|
||||||
|
props = undefined
|
||||||
|
employee = undefined
|
||||||
|
model = []
|
||||||
|
ptx = tx
|
||||||
|
}
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
$: if (client.getHierarchy().isDerived(tx._class, core.class.TxCollectionCUD)) {
|
async function createPseudoViewlet (dtx: DisplayTx, label: string): Promise<TxDisplayViewlet> {
|
||||||
const colCUD = tx as TxCollectionCUD<Doc, AttachedDoc>
|
const doc = dtx.doc
|
||||||
displayTx = colCUD.tx
|
if (doc === undefined) {
|
||||||
viewlet = undefined
|
return
|
||||||
} else if (client.getHierarchy().isDerived(tx._class, core.class.TxCUD)) {
|
|
||||||
displayTx = tx as TxCUD<Doc>
|
|
||||||
viewlet = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateViewlet (displayTx?: TxCUD<Doc>): Promise<TxDisplayViewlet> {
|
|
||||||
if (displayTx === undefined) {
|
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
const key = activityKey(displayTx.objectClass, displayTx._class)
|
const docClass: Class<Doc> = client.getModel().getObject(doc._class)
|
||||||
let viewlet: TxDisplayViewlet = viewlets.get(key)
|
|
||||||
|
|
||||||
props = { tx: displayTx }
|
const presenter = await getObjectPresenter(client, doc._class, 'doc-presenter')
|
||||||
|
if (presenter !== undefined) {
|
||||||
if (viewlet === undefined && displayTx._class === core.class.TxCreateDoc) {
|
return {
|
||||||
// Check if we have a class presenter we could have a pseudo viewlet based on class presenter.
|
display: 'inline',
|
||||||
const doc = TxProcessor.createDoc2Doc(displayTx as TxCreateDoc<Doc>)
|
icon: docClass.icon ?? activity.icon.Activity,
|
||||||
const docClass = client.getModel().getObject(doc._class)
|
label: (`${label} ` + docClass.label) as IntlString,
|
||||||
|
component: presenter.presenter
|
||||||
const presenter = await getObjectPresenter(client, doc._class, 'doc-presenter')
|
|
||||||
if (presenter !== undefined) {
|
|
||||||
viewlet = {
|
|
||||||
display: 'inline',
|
|
||||||
icon: docClass.icon ?? activity.icon.Activity,
|
|
||||||
label: ('created ' + docClass.label) as IntlString,
|
|
||||||
component: presenter.presenter
|
|
||||||
}
|
|
||||||
props = { value: doc }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return viewlet
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: updateViewlet(displayTx).then((result) => {
|
async function updateViewlet (dtx: DisplayTx): Promise<{ viewlet: TxDisplayViewlet; id: Ref<TxCUD<Doc>> }> {
|
||||||
viewlet = result
|
const key = activityKey(dtx.tx.objectClass, dtx.tx._class)
|
||||||
|
let viewlet: TxDisplayViewlet = viewlets.get(key)
|
||||||
|
|
||||||
|
props = { tx: dtx.tx, value: dtx.doc }
|
||||||
|
|
||||||
|
if (viewlet === undefined && dtx.tx._class === core.class.TxCreateDoc) {
|
||||||
|
// Check if we have a class presenter we could have a pseudo viewlet based on class presenter.
|
||||||
|
viewlet = await createPseudoViewlet(dtx, 'created')
|
||||||
|
}
|
||||||
|
if (viewlet === undefined && dtx.tx._class === core.class.TxRemoveDoc) {
|
||||||
|
viewlet = await createPseudoViewlet(dtx, 'deleted')
|
||||||
|
}
|
||||||
|
return { viewlet, id: dtx.tx._id }
|
||||||
|
}
|
||||||
|
|
||||||
|
$: updateViewlet(tx).then((result) => {
|
||||||
|
if (result.id === tx.tx._id) {
|
||||||
|
viewlet = result.viewlet
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let employee: EmployeeAccount | undefined
|
$: client
|
||||||
$: client.findOne(contact.class.EmployeeAccount, { _id: tx.modifiedBy as Ref<EmployeeAccount> }).then((account) => {
|
.findOne(contact.class.EmployeeAccount, { _id: tx.tx.modifiedBy as Ref<EmployeeAccount> })
|
||||||
employee = account
|
.then((account) => {
|
||||||
})
|
employee = account
|
||||||
|
})
|
||||||
|
|
||||||
let model: AttributeModel[] = []
|
$: if (tx.updateTx !== undefined) {
|
||||||
|
const ops = {
|
||||||
$: if (displayTx !== undefined && displayTx._class === core.class.TxUpdateDoc) {
|
client,
|
||||||
utx = displayTx as TxUpdateDoc<Doc>
|
_class: tx.updateTx.objectClass,
|
||||||
const ops = { client, _class: utx.objectClass, keys: Object.keys(utx.operations), ignoreMissing: true }
|
keys: Object.keys(tx.updateTx.operations).filter(id => !id.startsWith('$')),
|
||||||
model = []
|
ignoreMissing: true
|
||||||
|
}
|
||||||
buildModel(ops).then((m) => {
|
buildModel(ops).then((m) => {
|
||||||
model = m
|
model = m
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
model = []
|
|
||||||
utx = undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getValue (utx: TxUpdateDoc<Doc>, key: string): any {
|
function getValue (utx: TxUpdateDoc<Doc>, key: string): any {
|
||||||
@ -115,7 +112,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if displayTx && (viewlet !== undefined || model.length > 0)}
|
{#if viewlet !== undefined || model.length > 0}
|
||||||
<div class="flex-col msgactivity-container">
|
<div class="flex-col msgactivity-container">
|
||||||
<div class="flex-between">
|
<div class="flex-between">
|
||||||
<div class="flex-center icon">
|
<div class="flex-center icon">
|
||||||
@ -138,10 +135,10 @@
|
|||||||
{#if viewlet && viewlet.label}
|
{#if viewlet && viewlet.label}
|
||||||
<div><Label label={viewlet.label} /></div>
|
<div><Label label={viewlet.label} /></div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if viewlet === undefined && model.length > 0 && utx}
|
{#if viewlet === undefined && model.length > 0 && tx.updateTx}
|
||||||
{#each model as m}
|
{#each model as m}
|
||||||
<span>changed {m.label} to</span>
|
<span>changed {m.label} to</span>
|
||||||
<div class="strong"><svelte:component this={m.presenter} value={getValue(utx, m.key)} /></div>
|
<div class="strong"><svelte:component this={m.presenter} value={getValue(tx.updateTx, m.key)} /></div>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if viewlet && viewlet.display === 'inline' && viewlet.component}
|
{:else if viewlet && viewlet.display === 'inline' && viewlet.component}
|
||||||
<div>
|
<div>
|
||||||
@ -153,7 +150,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="time"><TimeSince value={tx.modifiedOn} /></div>
|
<div class="time"><TimeSince value={tx.tx.modifiedOn} /></div>
|
||||||
</div>
|
</div>
|
||||||
{#if viewlet && viewlet.component && viewlet.display !== 'inline'}
|
{#if viewlet && viewlet.component && viewlet.display !== 'inline'}
|
||||||
<div class="content" class:emphasize={viewlet.display === 'emphasized'}>
|
<div class="content" class:emphasize={viewlet.display === 'emphasized'}>
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import { Class, Doc, Ref, Tx } from '@anticrm/core'
|
|
||||||
|
|
||||||
export type ActivityKey = string
|
|
||||||
|
|
||||||
export function activityKey (objectClass: Ref<Class<Doc>>, txClass: Ref<Class<Tx>>): ActivityKey {
|
|
||||||
return objectClass + ':' + txClass
|
|
||||||
}
|
|
@ -13,7 +13,8 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import type { Doc } from '@anticrm/core'
|
import type { AttachedDoc, Doc } from '@anticrm/core'
|
||||||
|
import core from '@anticrm/core'
|
||||||
|
|
||||||
import StringEditor from './components/StringEditor.svelte'
|
import StringEditor from './components/StringEditor.svelte'
|
||||||
import StringPresenter from './components/StringPresenter.svelte'
|
import StringPresenter from './components/StringPresenter.svelte'
|
||||||
@ -39,7 +40,12 @@ function Delete(object: Doc): void {
|
|||||||
}, undefined, (result) => {
|
}, undefined, (result) => {
|
||||||
if (result) {
|
if (result) {
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
client.removeDoc(object._class, object.space, object._id)
|
if(client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) {
|
||||||
|
const adoc = object as AttachedDoc
|
||||||
|
client.removeCollection(object._class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection)
|
||||||
|
} else {
|
||||||
|
client.removeDoc(object._class, object.space, object._id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user