mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
[TSK-932] Show only diffs for description changes in activity (#3062)
Signed-off-by: Ruslan Bayandinov <wazsone@ya.ru>
This commit is contained in:
parent
ad8e748d91
commit
6d99032f2d
@ -29,6 +29,7 @@ import type {
|
||||
AttributeEditor,
|
||||
AttributeFilter,
|
||||
AttributePresenter,
|
||||
ActivityAttributePresenter,
|
||||
BuildModelKey,
|
||||
ClassFilters,
|
||||
ClassSortFuncs,
|
||||
@ -87,7 +88,8 @@ export function classPresenter (
|
||||
_class: Ref<Class<Doc>>,
|
||||
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, {
|
||||
|
@ -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,
|
||||
|
@ -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 @@
|
||||
</script>
|
||||
|
||||
<div class="ref-container">
|
||||
{#if comparedVersion !== undefined}
|
||||
{#if comparedVersion !== undefined && !noButton}
|
||||
<div class="flex">
|
||||
<div class="flex-grow" />
|
||||
<div class="formatPanel buttons-group xsmall-gap mb-4">
|
||||
|
@ -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`)
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
<span class="lower"><Label label={activity.string.Changed} /></span>
|
||||
<span class="lower"><Label label={m.label} /></span>
|
||||
<span class="lower"><Label label={activity.string.To} /></span>
|
||||
|
||||
{#if !hasMessageType}
|
||||
<span class="lower"><Label label={activity.string.To} /></span>
|
||||
<span class="strong overflow-label">
|
||||
{#if value.isObjectSet}
|
||||
<ObjectPresenter value={value.set} inline />
|
||||
@ -276,6 +277,11 @@
|
||||
<svelte:component this={m.presenter} value={value.set} inline />
|
||||
{/if}
|
||||
</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}
|
||||
{/await}
|
||||
@ -346,11 +352,18 @@
|
||||
{:else if hasMessageType && model.length > 0 && (tx.updateTx || tx.mixinTx)}
|
||||
{#await getValue(client, model[0], tx) then value}
|
||||
<div class="activity-content content" class:indent={isAttached} class:contentHidden>
|
||||
<ShowMore ignore={edit}>
|
||||
<ShowMore ignore={edit || showDiff}>
|
||||
{#if value.isObjectSet}
|
||||
<ObjectPresenter value={value.set} inline />
|
||||
{:else}
|
||||
<svelte:component this={model[0].presenter} value={value.set} inline />
|
||||
{:else if showDiff}
|
||||
<svelte:component
|
||||
this={model[0].presenter}
|
||||
value={value.set}
|
||||
inline
|
||||
prevValue
|
||||
compareValue={getPrevValue(client, model[0], tx)}
|
||||
showOnlyDiff
|
||||
/>
|
||||
{/if}
|
||||
</ShowMore>
|
||||
</div>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -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<Ref<Class<Doc>>> = [
|
||||
core.class.TypeString,
|
||||
@ -150,6 +154,77 @@ async function checkInlineViewlets (
|
||||
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 (
|
||||
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[]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
@ -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<Resources> => ({
|
||||
ActionsPopup,
|
||||
StringEditorPopup: EditBoxPopup,
|
||||
MarkupPresenter,
|
||||
MarkupDiffPresenter,
|
||||
MarkupEditor,
|
||||
MarkupEditorPopup,
|
||||
BooleanTruePresenter,
|
||||
|
@ -159,6 +159,13 @@ export interface AttributePresenter extends Class<Doc> {
|
||||
presenter: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ActivityAttributePresenter extends Class<Doc> {
|
||||
presenter: AnyComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -603,6 +610,7 @@ const view = plugin(viewId, {
|
||||
InlineAttributEditor: '' as Ref<Mixin<InlineAttributEditor>>,
|
||||
ArrayEditor: '' as Ref<Mixin<ArrayEditor>>,
|
||||
AttributePresenter: '' as Ref<Mixin<AttributePresenter>>,
|
||||
ActivityAttributePresenter: '' as Ref<Mixin<ActivityAttributePresenter>>,
|
||||
ListItemPresenter: '' as Ref<Mixin<ListItemPresenter>>,
|
||||
ObjectEditor: '' as Ref<Mixin<ObjectEditor>>,
|
||||
ObjectPresenter: '' as Ref<Mixin<ObjectPresenter>>,
|
||||
|
Loading…
Reference in New Issue
Block a user