[TSK-932] Show only diffs for description changes in activity (#3062)

Signed-off-by: Ruslan Bayandinov <wazsone@ya.ru>
This commit is contained in:
Ruslan Bayandinov 2023-04-27 14:09:46 +04:00 committed by GitHub
parent ad8e748d91
commit 6d99032f2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 209 additions and 19 deletions

View File

@ -29,6 +29,7 @@ import type {
AttributeEditor, AttributeEditor,
AttributeFilter, AttributeFilter,
AttributePresenter, AttributePresenter,
ActivityAttributePresenter,
BuildModelKey, BuildModelKey,
ClassFilters, ClassFilters,
ClassSortFuncs, ClassSortFuncs,
@ -87,7 +88,8 @@ export function classPresenter (
_class: Ref<Class<Doc>>, _class: Ref<Class<Doc>>,
presenter: AnyComponent, presenter: AnyComponent,
editor?: AnyComponent, editor?: AnyComponent,
popup?: AnyComponent popup?: AnyComponent,
activity?: AnyComponent
): void { ): void {
builder.mixin(_class, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(_class, core.class.Class, view.mixin.AttributePresenter, {
presenter presenter
@ -98,6 +100,11 @@ export function classPresenter (
popup popup
}) })
} }
if (activity !== undefined) {
builder.mixin(_class, core.class.Class, view.mixin.ActivityAttributePresenter, {
presenter: activity
})
}
} }
@Model(view.class.FilteredView, core.class.Doc, DOMAIN_PREFERENCE) @Model(view.class.FilteredView, core.class.Doc, DOMAIN_PREFERENCE)
@ -158,6 +165,11 @@ export class TAttributePresenter extends TClass implements AttributePresenter {
presenter!: AnyComponent 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) @Mixin(view.mixin.ObjectPresenter, core.class.Class)
export class TObjectPresenter extends TClass implements ObjectPresenter { export class TObjectPresenter extends TClass implements ObjectPresenter {
presenter!: AnyComponent presenter!: AnyComponent
@ -337,6 +349,7 @@ export function createModel (builder: Builder): void {
TAttributeFilter, TAttributeFilter,
TAttributeEditor, TAttributeEditor,
TAttributePresenter, TAttributePresenter,
TActivityAttributePresenter,
TListItemPresenter, TListItemPresenter,
TCollectionEditor, TCollectionEditor,
TCollectionPresenter, TCollectionPresenter,
@ -387,7 +400,8 @@ export function createModel (builder: Builder): void {
core.class.TypeMarkup, core.class.TypeMarkup,
view.component.MarkupPresenter, view.component.MarkupPresenter,
view.component.MarkupEditor, view.component.MarkupEditor,
view.component.MarkupEditorPopup view.component.MarkupEditorPopup,
view.component.MarkupDiffPresenter
) )
builder.mixin(core.class.TypeMarkup, core.class.Class, view.mixin.InlineAttributEditor, { builder.mixin(core.class.TypeMarkup, core.class.Class, view.mixin.InlineAttributEditor, {

View File

@ -47,6 +47,7 @@ export default mergeIds(viewId, view, {
IntlStringPresenter: '' as AnyComponent, IntlStringPresenter: '' as AnyComponent,
NumberEditor: '' as AnyComponent, NumberEditor: '' as AnyComponent,
NumberPresenter: '' as AnyComponent, NumberPresenter: '' as AnyComponent,
MarkupDiffPresenter: '' as AnyComponent,
MarkupPresenter: '' as AnyComponent, MarkupPresenter: '' as AnyComponent,
BooleanPresenter: '' as AnyComponent, BooleanPresenter: '' as AnyComponent,
BooleanEditor: '' as AnyComponent, BooleanEditor: '' as AnyComponent,

View File

@ -34,6 +34,8 @@
export let content: Markup export let content: Markup
export let buttonSize: IconSize = 'small' export let buttonSize: IconSize = 'small'
export let comparedVersion: Markup | undefined = undefined export let comparedVersion: Markup | undefined = undefined
export let noButton: boolean = false
export let readonly = false
let element: HTMLElement let element: HTMLElement
let editor: Editor let editor: Editor
@ -41,8 +43,8 @@
let _decoration = DecorationSet.empty let _decoration = DecorationSet.empty
let oldContent = '' let oldContent = ''
function updateEditor (editor?: Editor, comparedVersion?: Markup): void { function updateEditor (editor?: Editor, comparedVersion?: Markup | ArrayBuffer): void {
const r = calculateDecorations(editor, oldContent, comparedVersion) const r = calculateDecorations(editor, oldContent, undefined, comparedVersion)
if (r !== undefined) { if (r !== undefined) {
oldContent = r.oldContent oldContent = r.oldContent
_decoration = r.decorations _decoration = r.decorations
@ -87,6 +89,7 @@
editor = editor editor = editor
} }
}) })
editor.setEditable(!readonly)
}) })
onDestroy(() => { onDestroy(() => {
@ -98,7 +101,7 @@
</script> </script>
<div class="ref-container"> <div class="ref-container">
{#if comparedVersion !== undefined} {#if comparedVersion !== undefined && !noButton}
<div class="flex"> <div class="flex">
<div class="flex-grow" /> <div class="flex-grow" />
<div class="formatPanel buttons-group xsmall-gap mb-4"> <div class="formatPanel buttons-group xsmall-gap mb-4">

View File

@ -30,6 +30,8 @@ export { default as CollaborationDiffViewer } from './components/CollaborationDi
export { default } from './plugin' export { default } from './plugin'
export * from './types' export * from './types'
export { default as Collaboration } from './components/Collaboration.svelte' 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) => { addStringsLoader(textEditorId, async (lang: string) => {
return await import(`../lang/${lang}.json`) return await import(`../lang/${lang}.json`)

View File

@ -301,6 +301,7 @@ class ActivityImpl implements Activity {
result.collectionAttribute = collectionAttribute result.collectionAttribute = collectionAttribute
result.doc = firstTx?.doc ?? result.doc result.doc = firstTx?.doc ?? result.doc
result.prevDoc = this.hierarchy.clone(result.doc)
firstTx = firstTx ?? result firstTx = firstTx ?? result
parents.set(tx.objectId, firstTx) parents.set(tx.objectId, firstTx)
@ -351,7 +352,7 @@ class ActivityImpl implements Activity {
results.push(result) results.push(result)
return results return results
} }
const newResult = results.filter((prevTx) => { const newResults = results.filter((prevTx) => {
const prevUpdate: any = getCombineOpFromTx(prevTx) const prevUpdate: any = getCombineOpFromTx(prevTx)
if (this.isInitTx(prevTx, result)) { if (this.isInitTx(prevTx, result)) {
result = prevTx result = prevTx
@ -379,8 +380,9 @@ class ActivityImpl implements Activity {
return true return true
}) })
newResult.push(result)
return newResult newResults.push(result)
return newResults
} }
isInitTx (prevTx: DisplayTx, result: DisplayTx): boolean { isInitTx (prevTx: DisplayTx, result: DisplayTx): boolean {

View File

@ -19,7 +19,7 @@
import core, { AnyAttribute, Doc, getCurrentAccount, Ref, Class, TxCUD } from '@hcengineering/core' import core, { AnyAttribute, Doc, getCurrentAccount, Ref, Class, TxCUD } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform' import { Asset } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { import ui, {
ActionIcon, ActionIcon,
AnyComponent, AnyComponent,
Component, Component,
@ -37,7 +37,7 @@
import { Menu, ObjectPresenter } from '@hcengineering/view-resources' import { Menu, ObjectPresenter } from '@hcengineering/view-resources'
import { ActivityKey } from '../activity' import { ActivityKey } from '../activity'
import activity from '../plugin' import activity from '../plugin'
import { getValue, TxDisplayViewlet, updateViewlet } from '../utils' import { getPrevValue, getValue, TxDisplayViewlet, updateViewlet } from '../utils'
import TxViewTx from './TxViewTx.svelte' import TxViewTx from './TxViewTx.svelte'
import Edit from './icons/Edit.svelte' import Edit from './icons/Edit.svelte'
import { tick } from 'svelte' import { tick } from 'svelte'
@ -61,7 +61,8 @@
let modelIcon: Asset | undefined = undefined let modelIcon: Asset | undefined = undefined
let iconComponent: AnyComponent | 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._id !== ptx?.tx._id) {
if (tx.tx.modifiedBy !== account?._id) { if (tx.tx.modifiedBy !== account?._id) {
@ -266,9 +267,9 @@
{:else} {:else}
<span class="lower"><Label label={activity.string.Changed} /></span> <span class="lower"><Label label={activity.string.Changed} /></span>
<span class="lower"><Label label={m.label} /></span> <span class="lower"><Label label={m.label} /></span>
<span class="lower"><Label label={activity.string.To} /></span>
{#if !hasMessageType} {#if !hasMessageType}
<span class="lower"><Label label={activity.string.To} /></span>
<span class="strong overflow-label"> <span class="strong overflow-label">
{#if value.isObjectSet} {#if value.isObjectSet}
<ObjectPresenter value={value.set} inline /> <ObjectPresenter value={value.set} inline />
@ -276,6 +277,11 @@
<svelte:component this={m.presenter} value={value.set} inline /> <svelte:component this={m.presenter} value={value.set} inline />
{/if} {/if}
</span> </span>
{:else}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<span class="show-diff" on:click={() => (showDiff = !showDiff)}>
<Label label={showDiff ? ui.string.ShowLess : ui.string.ShowMore} />
</span>
{/if} {/if}
{/if} {/if}
{/await} {/await}
@ -346,11 +352,18 @@
{:else if hasMessageType && model.length > 0 && (tx.updateTx || tx.mixinTx)} {:else if hasMessageType && model.length > 0 && (tx.updateTx || tx.mixinTx)}
{#await getValue(client, model[0], tx) then value} {#await getValue(client, model[0], tx) then value}
<div class="activity-content content" class:indent={isAttached} class:contentHidden> <div class="activity-content content" class:indent={isAttached} class:contentHidden>
<ShowMore ignore={edit}> <ShowMore ignore={edit || showDiff}>
{#if value.isObjectSet} {#if value.isObjectSet}
<ObjectPresenter value={value.set} inline /> <ObjectPresenter value={value.set} inline />
{:else} {:else if showDiff}
<svelte:component this={model[0].presenter} value={value.set} inline /> <svelte:component
this={model[0].presenter}
value={value.set}
inline
prevValue
compareValue={getPrevValue(client, model[0], tx)}
showOnlyDiff
/>
{/if} {/if}
</ShowMore> </ShowMore>
</div> </div>
@ -511,4 +524,15 @@
margin-top: 0.5rem; margin-top: 0.5rem;
} }
} }
.show-diff {
color: var(--accent-color);
cursor: pointer;
&:hover {
color: var(--caption-color);
}
&:active {
color: var(--accent-color);
}
}
</style> </style>

View File

@ -2,8 +2,11 @@ import type { DisplayTx, TxViewlet } from '@hcengineering/activity'
import core, { import core, {
AttachedDoc, AttachedDoc,
Class, Class,
Client,
Collection, Collection,
Doc, Doc,
getObjectValue,
Obj,
Ref, Ref,
TxCollectionCUD, TxCollectionCUD,
TxCreateDoc, TxCreateDoc,
@ -13,12 +16,13 @@ import core, {
TxProcessor, TxProcessor,
TxUpdateDoc TxUpdateDoc
} from '@hcengineering/core' } from '@hcengineering/core'
import { Asset, IntlString, translate } from '@hcengineering/platform' import { Asset, IntlString, getResource, translate } from '@hcengineering/platform'
import { AnyComponent, AnySvelteComponent } from '@hcengineering/ui' import { AnyComponent, AnySvelteComponent, ErrorPresenter } from '@hcengineering/ui'
import { AttributeModel } from '@hcengineering/view' import view, { AttributeModel, BuildModelKey, BuildModelOptions } from '@hcengineering/view'
import { buildModel, getObjectPresenter } from '@hcengineering/view-resources' import { getObjectPresenter } from '@hcengineering/view-resources'
import { ActivityKey, activityKey } from './activity' import { ActivityKey, activityKey } from './activity'
import activity from './plugin' import activity from './plugin'
import { getAttributePresenterClass } from '@hcengineering/presentation'
const valueTypes: ReadonlyArray<Ref<Class<Doc>>> = [ const valueTypes: ReadonlyArray<Ref<Class<Doc>>> = [
core.class.TypeString, core.class.TypeString,
@ -150,6 +154,77 @@ async function checkInlineViewlets (
return { viewlet, model } return { viewlet, model }
} }
async function getAttributePresenter (
client: Client,
_class: Ref<Class<Obj>>,
key: string,
preserveKey: BuildModelKey
): Promise<AttributeModel> {
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<AttributeModel[]> {
// 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 ( async function createUpdateModel (
dtx: DisplayTx, dtx: DisplayTx,
client: TxOperations, client: TxOperations,
@ -301,6 +376,13 @@ export async function getValue (client: TxOperations, m: AttributeModel, tx: Dis
return value 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[] { export function filterCollectionTxes (txes: DisplayTx[]): DisplayTx[] {
return txes.map(filterCollectionTx).filter(Boolean) as DisplayTx[] return txes.map(filterCollectionTx).filter(Boolean) as DisplayTx[]
} }

View File

@ -84,6 +84,8 @@ export interface DisplayTx {
// Document in case it is required. // Document in case it is required.
doc?: Doc doc?: Doc
// Previous document in case it is required.
prevDoc?: Doc
updated: boolean updated: boolean
mixin: boolean mixin: boolean

View File

@ -0,0 +1,49 @@
<!--
// Copyright © 2023 Anticrm Platform Contributors.
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { CollaborationDiffViewer } from '@hcengineering/text-editor'
import { ShowMore } from '@hcengineering/ui'
export let value: string | undefined
export let compareValue: string | undefined = undefined
export let showOnlyDiff: boolean = false
function removeSimilarLines (str1: string | undefined, str2: string | undefined) {
if (str1 === undefined || str2 === undefined) {
return
}
const lines1 = str1.split('</p>')
const lines2 = str2.split('</p>')
let result1 = ''
let result2 = ''
for (let i = 0; i < lines1.length; i++) {
if (lines1[i] !== lines2[i]) {
result1 += lines1[i] ?? '' + '</p>'
result2 += lines2[i] ?? '' + '</p>'
}
}
value = result1
compareValue = result2
}
$: showOnlyDiff && removeSimilarLines(value, compareValue)
</script>
<ShowMore>
{#key [value, compareValue]}
<CollaborationDiffViewer content={value ?? ''} comparedVersion={compareValue} noButton readonly />
{/key}
</ShowMore>

View File

@ -49,6 +49,7 @@ import DividerPresenter from './components/list/DividerPresenter.svelte'
import ListView from './components/list/ListView.svelte' import ListView from './components/list/ListView.svelte'
import SortableList from './components/list/SortableList.svelte' import SortableList from './components/list/SortableList.svelte'
import SortableListItem from './components/list/SortableListItem.svelte' import SortableListItem from './components/list/SortableListItem.svelte'
import MarkupDiffPresenter from './components/MarkupDiffPresenter.svelte'
import MarkupEditor from './components/MarkupEditor.svelte' import MarkupEditor from './components/MarkupEditor.svelte'
import MarkupEditorPopup from './components/MarkupEditorPopup.svelte' import MarkupEditorPopup from './components/MarkupEditorPopup.svelte'
import MarkupPresenter from './components/MarkupPresenter.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 SourcePresenter } from './components/inference/SourcePresenter.svelte'
export { default as LinkPresenter } from './components/LinkPresenter.svelte' export { default as LinkPresenter } from './components/LinkPresenter.svelte'
export { default as List } from './components/list/List.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 MarkupPresenter } from './components/MarkupPresenter.svelte'
export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte' export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte'
export { default as ContextMenu } from './components/Menu.svelte' export { default as ContextMenu } from './components/Menu.svelte'
@ -189,6 +191,7 @@ export default async (): Promise<Resources> => ({
ActionsPopup, ActionsPopup,
StringEditorPopup: EditBoxPopup, StringEditorPopup: EditBoxPopup,
MarkupPresenter, MarkupPresenter,
MarkupDiffPresenter,
MarkupEditor, MarkupEditor,
MarkupEditorPopup, MarkupEditorPopup,
BooleanTruePresenter, BooleanTruePresenter,

View File

@ -159,6 +159,13 @@ export interface AttributePresenter extends Class<Doc> {
presenter: AnyComponent presenter: AnyComponent
} }
/**
* @public
*/
export interface ActivityAttributePresenter extends Class<Doc> {
presenter: AnyComponent
}
/** /**
* @public * @public
*/ */
@ -603,6 +610,7 @@ const view = plugin(viewId, {
InlineAttributEditor: '' as Ref<Mixin<InlineAttributEditor>>, InlineAttributEditor: '' as Ref<Mixin<InlineAttributEditor>>,
ArrayEditor: '' as Ref<Mixin<ArrayEditor>>, ArrayEditor: '' as Ref<Mixin<ArrayEditor>>,
AttributePresenter: '' as Ref<Mixin<AttributePresenter>>, AttributePresenter: '' as Ref<Mixin<AttributePresenter>>,
ActivityAttributePresenter: '' as Ref<Mixin<ActivityAttributePresenter>>,
ListItemPresenter: '' as Ref<Mixin<ListItemPresenter>>, ListItemPresenter: '' as Ref<Mixin<ListItemPresenter>>,
ObjectEditor: '' as Ref<Mixin<ObjectEditor>>, ObjectEditor: '' as Ref<Mixin<ObjectEditor>>,
ObjectPresenter: '' as Ref<Mixin<ObjectPresenter>>, ObjectPresenter: '' as Ref<Mixin<ObjectPresenter>>,