diff --git a/models/view/src/index.ts b/models/view/src/index.ts index ec7d39cf12..b8565b830e 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -29,6 +29,7 @@ import type { AttributeEditor, AttributeFilter, AttributePresenter, + ActivityAttributePresenter, BuildModelKey, ClassFilters, ClassSortFuncs, @@ -87,7 +88,8 @@ export function classPresenter ( _class: Ref>, presenter: AnyComponent, editor?: AnyComponent, - popup?: AnyComponent + popup?: AnyComponent, + activity?: AnyComponent ): void { builder.mixin(_class, core.class.Class, view.mixin.AttributePresenter, { presenter @@ -98,6 +100,11 @@ export function classPresenter ( popup }) } + if (activity !== undefined) { + builder.mixin(_class, core.class.Class, view.mixin.ActivityAttributePresenter, { + presenter: activity + }) + } } @Model(view.class.FilteredView, core.class.Doc, DOMAIN_PREFERENCE) @@ -158,6 +165,11 @@ export class TAttributePresenter extends TClass implements AttributePresenter { presenter!: AnyComponent } +@Mixin(view.mixin.ActivityAttributePresenter, core.class.Class) +export class TActivityAttributePresenter extends TClass implements ActivityAttributePresenter { + presenter!: AnyComponent +} + @Mixin(view.mixin.ObjectPresenter, core.class.Class) export class TObjectPresenter extends TClass implements ObjectPresenter { presenter!: AnyComponent @@ -337,6 +349,7 @@ export function createModel (builder: Builder): void { TAttributeFilter, TAttributeEditor, TAttributePresenter, + TActivityAttributePresenter, TListItemPresenter, TCollectionEditor, TCollectionPresenter, @@ -387,7 +400,8 @@ export function createModel (builder: Builder): void { core.class.TypeMarkup, view.component.MarkupPresenter, view.component.MarkupEditor, - view.component.MarkupEditorPopup + view.component.MarkupEditorPopup, + view.component.MarkupDiffPresenter ) builder.mixin(core.class.TypeMarkup, core.class.Class, view.mixin.InlineAttributEditor, { diff --git a/models/view/src/plugin.ts b/models/view/src/plugin.ts index b2d0f3da9d..7c2a2998a0 100644 --- a/models/view/src/plugin.ts +++ b/models/view/src/plugin.ts @@ -47,6 +47,7 @@ export default mergeIds(viewId, view, { IntlStringPresenter: '' as AnyComponent, NumberEditor: '' as AnyComponent, NumberPresenter: '' as AnyComponent, + MarkupDiffPresenter: '' as AnyComponent, MarkupPresenter: '' as AnyComponent, BooleanPresenter: '' as AnyComponent, BooleanEditor: '' as AnyComponent, diff --git a/packages/text-editor/src/components/CollaborationDiffViewer.svelte b/packages/text-editor/src/components/CollaborationDiffViewer.svelte index 57476c88e1..a1e6ec77e1 100644 --- a/packages/text-editor/src/components/CollaborationDiffViewer.svelte +++ b/packages/text-editor/src/components/CollaborationDiffViewer.svelte @@ -34,6 +34,8 @@ export let content: Markup export let buttonSize: IconSize = 'small' export let comparedVersion: Markup | undefined = undefined + export let noButton: boolean = false + export let readonly = false let element: HTMLElement let editor: Editor @@ -41,8 +43,8 @@ let _decoration = DecorationSet.empty let oldContent = '' - function updateEditor (editor?: Editor, comparedVersion?: Markup): void { - const r = calculateDecorations(editor, oldContent, comparedVersion) + function updateEditor (editor?: Editor, comparedVersion?: Markup | ArrayBuffer): void { + const r = calculateDecorations(editor, oldContent, undefined, comparedVersion) if (r !== undefined) { oldContent = r.oldContent _decoration = r.decorations @@ -87,6 +89,7 @@ editor = editor } }) + editor.setEditable(!readonly) }) onDestroy(() => { @@ -98,7 +101,7 @@
- {#if comparedVersion !== undefined} + {#if comparedVersion !== undefined && !noButton}
diff --git a/packages/text-editor/src/index.ts b/packages/text-editor/src/index.ts index cfd5054058..22f37b1978 100644 --- a/packages/text-editor/src/index.ts +++ b/packages/text-editor/src/index.ts @@ -30,6 +30,8 @@ export { default as CollaborationDiffViewer } from './components/CollaborationDi export { default } from './plugin' export * from './types' export { default as Collaboration } from './components/Collaboration.svelte' +export { default as IconObjects } from './components/icons/Objects.svelte' +export { default as StyleButton } from './components/StyleButton.svelte' addStringsLoader(textEditorId, async (lang: string) => { return await import(`../lang/${lang}.json`) diff --git a/plugins/activity-resources/src/activity.ts b/plugins/activity-resources/src/activity.ts index db54a38b1c..8672115d59 100644 --- a/plugins/activity-resources/src/activity.ts +++ b/plugins/activity-resources/src/activity.ts @@ -301,6 +301,7 @@ class ActivityImpl implements Activity { result.collectionAttribute = collectionAttribute result.doc = firstTx?.doc ?? result.doc + result.prevDoc = this.hierarchy.clone(result.doc) firstTx = firstTx ?? result parents.set(tx.objectId, firstTx) @@ -351,7 +352,7 @@ class ActivityImpl implements Activity { results.push(result) return results } - const newResult = results.filter((prevTx) => { + const newResults = results.filter((prevTx) => { const prevUpdate: any = getCombineOpFromTx(prevTx) if (this.isInitTx(prevTx, result)) { result = prevTx @@ -379,8 +380,9 @@ class ActivityImpl implements Activity { return true }) - newResult.push(result) - return newResult + + newResults.push(result) + return newResults } isInitTx (prevTx: DisplayTx, result: DisplayTx): boolean { diff --git a/plugins/activity-resources/src/components/TxView.svelte b/plugins/activity-resources/src/components/TxView.svelte index c8d2109c11..9454849e03 100644 --- a/plugins/activity-resources/src/components/TxView.svelte +++ b/plugins/activity-resources/src/components/TxView.svelte @@ -19,7 +19,7 @@ import core, { AnyAttribute, Doc, getCurrentAccount, Ref, Class, TxCUD } from '@hcengineering/core' import { Asset } from '@hcengineering/platform' import { createQuery, getClient } from '@hcengineering/presentation' - import { + import ui, { ActionIcon, AnyComponent, Component, @@ -37,7 +37,7 @@ import { Menu, ObjectPresenter } from '@hcengineering/view-resources' import { ActivityKey } from '../activity' import activity from '../plugin' - import { getValue, TxDisplayViewlet, updateViewlet } from '../utils' + import { getPrevValue, getValue, TxDisplayViewlet, updateViewlet } from '../utils' import TxViewTx from './TxViewTx.svelte' import Edit from './icons/Edit.svelte' import { tick } from 'svelte' @@ -61,7 +61,8 @@ let modelIcon: Asset | undefined = undefined let iconComponent: AnyComponent | undefined = undefined - let edit = false + let edit: boolean = false + let showDiff: boolean = false $: if (tx.tx._id !== ptx?.tx._id) { if (tx.tx.modifiedBy !== account?._id) { @@ -266,9 +267,9 @@ {:else} - {#if !hasMessageType} + {#if value.isObjectSet} @@ -276,6 +277,11 @@ {/if} + {:else} + + (showDiff = !showDiff)}> + {/if} {/if} {/await} @@ -346,11 +352,18 @@ {:else if hasMessageType && model.length > 0 && (tx.updateTx || tx.mixinTx)} {#await getValue(client, model[0], tx) then value}
- + {#if value.isObjectSet} - {:else} - + {:else if showDiff} + {/if}
@@ -511,4 +524,15 @@ margin-top: 0.5rem; } } + + .show-diff { + color: var(--accent-color); + cursor: pointer; + &:hover { + color: var(--caption-color); + } + &:active { + color: var(--accent-color); + } + } diff --git a/plugins/activity-resources/src/utils.ts b/plugins/activity-resources/src/utils.ts index 2a491b525f..54e167a7c4 100644 --- a/plugins/activity-resources/src/utils.ts +++ b/plugins/activity-resources/src/utils.ts @@ -2,8 +2,11 @@ import type { DisplayTx, TxViewlet } from '@hcengineering/activity' import core, { AttachedDoc, Class, + Client, Collection, Doc, + getObjectValue, + Obj, Ref, TxCollectionCUD, TxCreateDoc, @@ -13,12 +16,13 @@ import core, { TxProcessor, TxUpdateDoc } from '@hcengineering/core' -import { Asset, IntlString, translate } from '@hcengineering/platform' -import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui' -import { AttributeModel } from '@hcengineering/view' -import { buildModel, getObjectPresenter } from '@hcengineering/view-resources' +import { Asset, IntlString, getResource, translate } from '@hcengineering/platform' +import { AnyComponent, AnySvelteComponent, ErrorPresenter } from '@hcengineering/ui' +import view, { AttributeModel, BuildModelKey, BuildModelOptions } from '@hcengineering/view' +import { getObjectPresenter } from '@hcengineering/view-resources' import { ActivityKey, activityKey } from './activity' import activity from './plugin' +import { getAttributePresenterClass } from '@hcengineering/presentation' const valueTypes: ReadonlyArray>> = [ core.class.TypeString, @@ -150,6 +154,77 @@ async function checkInlineViewlets ( return { viewlet, model } } +async function getAttributePresenter ( + client: Client, + _class: Ref>, + key: string, + preserveKey: BuildModelKey +): Promise { + const hierarchy = client.getHierarchy() + const attribute = hierarchy.getAttribute(_class, key) + const presenterClass = getAttributePresenterClass(hierarchy, attribute) + const isCollectionAttr = presenterClass.category === 'collection' + const mixin = isCollectionAttr ? view.mixin.CollectionPresenter : view.mixin.ActivityAttributePresenter + let presenterMixin = hierarchy.classHierarchyMixin(presenterClass.attrClass, mixin) + if (presenterMixin?.presenter === undefined && mixin === view.mixin.ActivityAttributePresenter) { + presenterMixin = hierarchy.classHierarchyMixin(presenterClass.attrClass, view.mixin.AttributePresenter) + if (presenterMixin?.presenter === undefined) { + throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey)) + } + } else if (presenterMixin?.presenter === undefined) { + throw new Error('attribute presenter not found for ' + JSON.stringify(preserveKey)) + } + const resultKey = preserveKey.sortingKey ?? preserveKey.key + const sortingKey = Array.isArray(resultKey) + ? resultKey + : attribute.type._class === core.class.ArrOf + ? resultKey + '.length' + : resultKey + const presenter = await getResource(presenterMixin.presenter) + + return { + key: preserveKey.key, + sortingKey, + _class: presenterClass.attrClass, + label: preserveKey.label ?? attribute.shortLabel ?? attribute.label, + presenter, + props: preserveKey.props, + icon: presenterMixin.icon, + attribute, + collectionAttr: isCollectionAttr, + isLookup: false + } +} + +async function buildModel (options: BuildModelOptions): Promise { + // eslint-disable-next-line array-callback-return + const model = options.keys + .map((key) => (typeof key === 'string' ? { key } : key)) + .map(async (key) => { + try { + return await getAttributePresenter(options.client, options._class, key.key, key) + } catch (err: any) { + if (options.ignoreMissing ?? false) { + return undefined + } + const stringKey = key.label ?? key.key + console.error('Failed to find presenter for', key, err) + const errorPresenter: AttributeModel = { + key: '', + sortingKey: '', + presenter: ErrorPresenter, + label: stringKey as IntlString, + _class: core.class.TypeString, + props: { error: err }, + collectionAttr: false, + isLookup: false + } + return errorPresenter + } + }) + return (await Promise.all(model)).filter((a) => a !== undefined) as AttributeModel[] +} + async function createUpdateModel ( dtx: DisplayTx, client: TxOperations, @@ -301,6 +376,13 @@ export async function getValue (client: TxOperations, m: AttributeModel, tx: Dis return value } +export function getPrevValue (client: TxOperations, m: AttributeModel, tx: DisplayTx): any { + if (tx.prevDoc !== undefined) { + return getObjectValue(m.key, tx.prevDoc) + } + return undefined +} + export function filterCollectionTxes (txes: DisplayTx[]): DisplayTx[] { return txes.map(filterCollectionTx).filter(Boolean) as DisplayTx[] } diff --git a/plugins/activity/src/index.ts b/plugins/activity/src/index.ts index 739e39e3ce..cec29a6efa 100644 --- a/plugins/activity/src/index.ts +++ b/plugins/activity/src/index.ts @@ -84,6 +84,8 @@ export interface DisplayTx { // Document in case it is required. doc?: Doc + // Previous document in case it is required. + prevDoc?: Doc updated: boolean mixin: boolean diff --git a/plugins/view-resources/src/components/MarkupDiffPresenter.svelte b/plugins/view-resources/src/components/MarkupDiffPresenter.svelte new file mode 100644 index 0000000000..4ad5da6f22 --- /dev/null +++ b/plugins/view-resources/src/components/MarkupDiffPresenter.svelte @@ -0,0 +1,49 @@ + + + + + {#key [value, compareValue]} + + {/key} + diff --git a/plugins/view-resources/src/index.ts b/plugins/view-resources/src/index.ts index b0f0715b6a..8e62e2263f 100644 --- a/plugins/view-resources/src/index.ts +++ b/plugins/view-resources/src/index.ts @@ -49,6 +49,7 @@ import DividerPresenter from './components/list/DividerPresenter.svelte' import ListView from './components/list/ListView.svelte' import SortableList from './components/list/SortableList.svelte' import SortableListItem from './components/list/SortableListItem.svelte' +import MarkupDiffPresenter from './components/MarkupDiffPresenter.svelte' import MarkupEditor from './components/MarkupEditor.svelte' import MarkupEditorPopup from './components/MarkupEditorPopup.svelte' import MarkupPresenter from './components/MarkupPresenter.svelte' @@ -94,6 +95,7 @@ export { default as FixedColumn } from './components/FixedColumn.svelte' export { default as SourcePresenter } from './components/inference/SourcePresenter.svelte' export { default as LinkPresenter } from './components/LinkPresenter.svelte' export { default as List } from './components/list/List.svelte' +export { default as MarkupDiffPresenter } from './components/MarkupDiffPresenter.svelte' export { default as MarkupPresenter } from './components/MarkupPresenter.svelte' export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte' export { default as ContextMenu } from './components/Menu.svelte' @@ -189,6 +191,7 @@ export default async (): Promise => ({ ActionsPopup, StringEditorPopup: EditBoxPopup, MarkupPresenter, + MarkupDiffPresenter, MarkupEditor, MarkupEditorPopup, BooleanTruePresenter, diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index baf6daefb2..3147f1e89a 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -159,6 +159,13 @@ export interface AttributePresenter extends Class { presenter: AnyComponent } +/** + * @public + */ +export interface ActivityAttributePresenter extends Class { + presenter: AnyComponent +} + /** * @public */ @@ -603,6 +610,7 @@ const view = plugin(viewId, { InlineAttributEditor: '' as Ref>, ArrayEditor: '' as Ref>, AttributePresenter: '' as Ref>, + ActivityAttributePresenter: '' as Ref>, ListItemPresenter: '' as Ref>, ObjectEditor: '' as Ref>, ObjectPresenter: '' as Ref>,