UBERF-7844 Collaborative markup activity diff (#6320)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-08-12 16:27:23 +07:00 committed by GitHub
parent 319ce9cafc
commit 27717058cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 73 additions and 43 deletions

View File

@ -54,7 +54,9 @@ function getActivityControl (client: MigrationClient): ActivityControl {
modelDb: client.model, modelDb: client.model,
hierarchy: client.hierarchy, hierarchy: client.hierarchy,
findAll: async (_class, query, options) => findAll: async (_class, query, options) =>
toFindResult(await client.find(client.hierarchy.getDomain(_class), query, options)) toFindResult(await client.find(client.hierarchy.getDomain(_class), query, options)),
storageAdapter: client.storageAdapter,
workspace: client.workspaceId
} }
} }

View File

@ -513,7 +513,7 @@ export function createModel (builder: Builder): void {
}) })
builder.mixin(core.class.TypeCollaborativeDoc, core.class.Class, view.mixin.ActivityAttributePresenter, { builder.mixin(core.class.TypeCollaborativeDoc, core.class.Class, view.mixin.ActivityAttributePresenter, {
presenter: view.component.CollaborativeDocActivityPresenter presenter: view.component.MarkupDiffPresenter
}) })
builder.mixin(core.class.TypeCollaborativeDocVersion, core.class.Class, view.mixin.InlineAttributEditor, { builder.mixin(core.class.TypeCollaborativeDocVersion, core.class.Class, view.mixin.InlineAttributEditor, {

View File

@ -74,7 +74,6 @@ export default mergeIds(viewId, view, {
HTMLEditor: '' as AnyComponent, HTMLEditor: '' as AnyComponent,
CollaborativeHTMLEditor: '' as AnyComponent, CollaborativeHTMLEditor: '' as AnyComponent,
CollaborativeDocEditor: '' as AnyComponent, CollaborativeDocEditor: '' as AnyComponent,
CollaborativeDocActivityPresenter: '' as AnyComponent,
MarkupEditor: '' as AnyComponent, MarkupEditor: '' as AnyComponent,
MarkupEditorPopup: '' as AnyComponent, MarkupEditorPopup: '' as AnyComponent,
ListView: '' as AnyComponent, ListView: '' as AnyComponent,

View File

@ -13,12 +13,30 @@
// limitations under the License. // limitations under the License.
// //
import { Markup } from '@hcengineering/core'
import { Extensions, getSchema } from '@tiptap/core' import { Extensions, getSchema } from '@tiptap/core'
import { Node, Schema } from 'prosemirror-model' import { Node, Schema } from 'prosemirror-model'
import { prosemirrorJSONToYDoc, yDocToProsemirrorJSON } from 'y-prosemirror' import { prosemirrorJSONToYDoc, prosemirrorToYDoc, yDocToProsemirrorJSON } from 'y-prosemirror'
import { Doc, applyUpdate, encodeStateAsUpdate } from 'yjs' import { Doc as YDoc, applyUpdate, encodeStateAsUpdate } from 'yjs'
import { defaultExtensions } from './extensions' import { defaultExtensions } from './extensions'
import { MarkupNode } from './markup/model' import { MarkupNode } from './markup/model'
import { jsonToMarkup, markupToPmNode } from './markup/utils'
/**
* @public
*/
export function markupToYDoc (markup: Markup, field: string): YDoc {
const node = markupToPmNode(markup)
return prosemirrorToYDoc(node, field)
}
/**
* @public
*/
export function yDocToMarkup (ydoc: YDoc, field: string): Markup {
const json = yDocToProsemirrorJSON(ydoc, field)
return jsonToMarkup(json as MarkupNode)
}
/** /**
* Get ProseMirror node from Y.Doc content * Get ProseMirror node from Y.Doc content
@ -31,7 +49,7 @@ export function yDocContentToNode (
schema?: Schema, schema?: Schema,
extensions?: Extensions extensions?: Extensions
): Node { ): Node {
const ydoc = new Doc() const ydoc = new YDoc()
const uint8arr = new Uint8Array(content) const uint8arr = new Uint8Array(content)
applyUpdate(ydoc, uint8arr) applyUpdate(ydoc, uint8arr)
@ -43,7 +61,7 @@ export function yDocContentToNode (
* *
* @public * @public
*/ */
export function yDocToNode (ydoc: Doc, field?: string, schema?: Schema, extensions?: Extensions): Node { export function yDocToNode (ydoc: YDoc, field?: string, schema?: Schema, extensions?: Extensions): Node {
schema ??= getSchema(extensions ?? defaultExtensions) schema ??= getSchema(extensions ?? defaultExtensions)
try { try {
@ -66,7 +84,7 @@ export function yDocContentToNodes (content: ArrayBuffer, schema?: Schema, exten
const nodes: Node[] = [] const nodes: Node[] = []
try { try {
const ydoc = new Doc() const ydoc = new YDoc()
const uint8arr = new Uint8Array(content) const uint8arr = new Uint8Array(content)
applyUpdate(ydoc, uint8arr) applyUpdate(ydoc, uint8arr)
@ -93,12 +111,12 @@ export function updateYDocContent (
updateFn: (body: Record<string, any>) => Record<string, any>, updateFn: (body: Record<string, any>) => Record<string, any>,
schema?: Schema, schema?: Schema,
extensions?: Extensions extensions?: Extensions
): Doc | undefined { ): YDoc | undefined {
schema ??= getSchema(extensions ?? defaultExtensions) schema ??= getSchema(extensions ?? defaultExtensions)
try { try {
const ydoc = new Doc() const ydoc = new YDoc()
const res = new Doc({ gc: false }) const res = new YDoc({ gc: false })
const uint8arr = new Uint8Array(content) const uint8arr = new Uint8Array(content)
applyUpdate(ydoc, uint8arr) applyUpdate(ydoc, uint8arr)
@ -121,10 +139,10 @@ export function updateYDocContent (
* *
* @public * @public
*/ */
export function YDocFromContent (content: MarkupNode, field: string, schema?: Schema, extensions?: Extensions): Doc { export function YDocFromContent (content: MarkupNode, field: string, schema?: Schema, extensions?: Extensions): YDoc {
schema ??= getSchema(extensions ?? defaultExtensions) schema ??= getSchema(extensions ?? defaultExtensions)
const res = new Doc({ gc: false }) const res = new YDoc({ gc: false })
const yDoc = prosemirrorJSONToYDoc(schema, content, field) const yDoc = prosemirrorJSONToYDoc(schema, content, field)
const update = encodeStateAsUpdate(yDoc) const update = encodeStateAsUpdate(yDoc)

View File

@ -1,21 +0,0 @@
<!--
// Copyright © 2024 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 { CollaborativeDoc } from '@hcengineering/core'
export let value: CollaborativeDoc
</script>
<span>{value}</span>

View File

@ -28,7 +28,6 @@ import ClassPresenter from './components/ClassPresenter.svelte'
import ClassRefPresenter from './components/ClassRefPresenter.svelte' import ClassRefPresenter from './components/ClassRefPresenter.svelte'
import CollaborativeDocEditor from './components/CollaborativeDocEditor.svelte' import CollaborativeDocEditor from './components/CollaborativeDocEditor.svelte'
import CollaborativeHTMLEditor from './components/CollaborativeHTMLEditor.svelte' import CollaborativeHTMLEditor from './components/CollaborativeHTMLEditor.svelte'
import CollaborativeDocActivityPresenter from './components/CollaborativeDocActivityPresenter.svelte'
import ColorsPopup from './components/ColorsPopup.svelte' import ColorsPopup from './components/ColorsPopup.svelte'
import DateEditor from './components/DateEditor.svelte' import DateEditor from './components/DateEditor.svelte'
import DatePresenter from './components/DatePresenter.svelte' import DatePresenter from './components/DatePresenter.svelte'
@ -277,7 +276,6 @@ export default async (): Promise<Resources> => ({
HTMLEditor, HTMLEditor,
CollaborativeDocEditor, CollaborativeDocEditor,
CollaborativeHTMLEditor, CollaborativeHTMLEditor,
CollaborativeDocActivityPresenter,
ListView, ListView,
GrowPresenter, GrowPresenter,
DividerPresenter, DividerPresenter,

View File

@ -197,7 +197,7 @@ function getDocUpdateMessageTx (
} }
export async function pushDocUpdateMessages ( export async function pushDocUpdateMessages (
ctx: MeasureContext | undefined, ctx: MeasureContext,
control: ActivityControl, control: ActivityControl,
res: TxCollectionCUD<Doc, DocUpdateMessage>[], res: TxCollectionCUD<Doc, DocUpdateMessage>[],
object: Doc | undefined, object: Doc | undefined,
@ -231,7 +231,7 @@ export async function pushDocUpdateMessages (
: undefined : undefined
} }
const attributesUpdates = await getTxAttributesUpdates(control, originTx, tx, object, objectCache, controlRules) const attributesUpdates = await getTxAttributesUpdates(ctx, control, originTx, tx, object, objectCache, controlRules)
for (const attributeUpdates of attributesUpdates) { for (const attributeUpdates of attributesUpdates) {
res.push( res.push(

View File

@ -3,9 +3,13 @@ import {
AttachedDoc, AttachedDoc,
type Attribute, type Attribute,
Class, Class,
CollaborativeDoc,
collaborativeDocFromLastVersion,
Collection, Collection,
Doc, Doc,
Hierarchy, Hierarchy,
Markup,
MeasureContext,
Mixin, Mixin,
Ref, Ref,
RefTo, RefTo,
@ -14,15 +18,18 @@ import {
TxCUD, TxCUD,
TxMixin, TxMixin,
TxProcessor, TxProcessor,
TxUpdateDoc TxUpdateDoc,
WorkspaceId
} from '@hcengineering/core' } from '@hcengineering/core'
import core from '@hcengineering/core/src/component' import core from '@hcengineering/core/src/component'
import { ActivityMessageControl, DocAttributeUpdates, DocUpdateAction } from '@hcengineering/activity' import { ActivityMessageControl, DocAttributeUpdates, DocUpdateAction } from '@hcengineering/activity'
import { ActivityControl, DocObjectCache, getAllObjectTransactions } from '@hcengineering/server-activity' import { ActivityControl, DocObjectCache, getAllObjectTransactions } from '@hcengineering/server-activity'
import { getDocCollaborators } from '@hcengineering/server-notification-resources' import { getDocCollaborators } from '@hcengineering/server-notification-resources'
import notification from '@hcengineering/notification' import notification from '@hcengineering/notification'
import { TriggerControl } from '@hcengineering/server-core' import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
import { translate } from '@hcengineering/platform' import { translate } from '@hcengineering/platform'
import { loadCollaborativeDoc } from '@hcengineering/collaboration'
import { EmptyMarkup, yDocToMarkup } from '@hcengineering/text'
function getAvailableAttributesKeys (tx: TxCUD<Doc>, hierarchy: Hierarchy): string[] { function getAvailableAttributesKeys (tx: TxCUD<Doc>, hierarchy: Hierarchy): string[] {
if (hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { if (hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) {
@ -226,6 +233,7 @@ export async function getAttributeDiff (
} }
export async function getTxAttributesUpdates ( export async function getTxAttributesUpdates (
ctx: MeasureContext,
control: ActivityControl, control: ActivityControl,
originTx: TxCUD<Doc>, originTx: TxCUD<Doc>,
tx: TxCUD<Doc>, tx: TxCUD<Doc>,
@ -296,6 +304,7 @@ export async function getTxAttributesUpdates (
if ( if (
hierarchy.isDerived(attrClass, core.class.TypeMarkup) || hierarchy.isDerived(attrClass, core.class.TypeMarkup) ||
hierarchy.isDerived(attrClass, core.class.TypeCollaborativeMarkup) || hierarchy.isDerived(attrClass, core.class.TypeCollaborativeMarkup) ||
hierarchy.isDerived(attrClass, core.class.TypeCollaborativeDoc) ||
mixin === notification.mixin.Collaborators mixin === notification.mixin.Collaborators
) { ) {
if (docDiff === undefined) { if (docDiff === undefined) {
@ -323,6 +332,16 @@ export async function getTxAttributesUpdates (
} }
} }
// we don't want to show collaborative documents in activity
// instead we show their content as Markup
// TODO this should be generalized via activity extension
const attrType = mixin !== undefined ? hierarchy.findAttribute(mixin, key) : clazz
if (attrType?.type?._class === core.class.TypeCollaborativeDoc) {
attrClass = isMixin ? attrClass : core.class.TypeMarkup
attrValue = await getMarkup(ctx, control.storageAdapter, control.workspace, attrValue, key)
prevValue = await getMarkup(ctx, control.storageAdapter, control.workspace, prevValue, key)
}
let setAttr = [] let setAttr = []
if (Array.isArray(attrValue)) { if (Array.isArray(attrValue)) {
@ -406,3 +425,16 @@ export function getCollectionAttribute (
return undefined return undefined
} }
async function getMarkup (
ctx: MeasureContext,
storage: StorageAdapter,
workspace: WorkspaceId,
value: CollaborativeDoc,
field: string
): Promise<Markup> {
if (value === undefined) return EmptyMarkup
value = collaborativeDocFromLastVersion(value)
const ydoc = await loadCollaborativeDoc(storage, workspace, value, ctx)
return ydoc !== undefined ? yDocToMarkup(ydoc, field) : EmptyMarkup
}

View File

@ -1,4 +1,5 @@
import { Doc, Hierarchy, ModelDb, Ref, Storage, TxCUD, TxFactory } from '@hcengineering/core' import { Doc, Hierarchy, ModelDb, Ref, Storage, TxCUD, TxFactory, WorkspaceId } from '@hcengineering/core'
import { StorageAdapter } from '@hcengineering/server-core'
export interface DocObjectCache { export interface DocObjectCache {
docs: Map<Ref<Doc>, Doc | null> docs: Map<Ref<Doc>, Doc | null>
@ -10,4 +11,6 @@ export interface ActivityControl {
hierarchy: Hierarchy hierarchy: Hierarchy
txFactory: TxFactory txFactory: TxFactory
modelDb: ModelDb modelDb: ModelDb
storageAdapter: StorageAdapter
workspace: WorkspaceId
} }

View File

@ -131,7 +131,7 @@ async function getRequestNotificationTx (tx: TxCollectionCUD<Doc, Request>, cont
if (doc === undefined) return [] if (doc === undefined) return []
const res: Tx[] = [] const res: Tx[] = []
const messagesTxes = await pushDocUpdateMessages(undefined, control, [], doc, tx) const messagesTxes = await pushDocUpdateMessages(control.ctx, control, [], doc, tx)
if (messagesTxes.length === 0) return [] if (messagesTxes.length === 0) return []

View File

@ -84,7 +84,6 @@ export async function loadCollaborativeDoc (
for (const source of sources) { for (const source of sources) {
const { documentId, versionId } = collaborativeDocParse(source) const { documentId, versionId } = collaborativeDocParse(source)
ctx.info('loading collaborative document', { source })
const ydoc = await loadCollaborativeDocVersion(ctx, storageAdapter, workspace, documentId, versionId) const ydoc = await loadCollaborativeDocVersion(ctx, storageAdapter, workspace, documentId, versionId)
if (ydoc !== undefined) { if (ydoc !== undefined) {