Fix unintendent collaborative content changes (#5388)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-04-17 23:46:30 +07:00 committed by GitHub
parent 12ae2fa0ec
commit 55c1e745b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 235 additions and 54 deletions

View File

@ -17,6 +17,7 @@
import { createEventDispatcher } from 'svelte'
import { Markup } from '@hcengineering/core'
import { IntlString, Asset } from '@hcengineering/platform'
import { EmptyMarkup } from '@hcengineering/text'
import { Label, Icon } from '@hcengineering/ui'
import type { AnySvelteComponent } from '@hcengineering/ui'
import StyledTextBox from './StyledTextBox.svelte'
@ -25,7 +26,7 @@
export let label: IntlString = textEditorPlugin.string.FullDescription
export let icon: Asset | AnySvelteComponent = IconDescription
export let content: Markup = ''
export let content: Markup = EmptyMarkup
export let maxHeight: string = '40vh'
export let enableBackReferences = false

View File

@ -15,6 +15,7 @@
<script lang="ts">
import { Markup } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import { EmptyMarkup, isEmptyMarkup } from '@hcengineering/text'
import {
AnySvelteComponent,
Button,
@ -37,7 +38,7 @@
import { IsEmptyContentExtension } from './extension/isEmptyContent'
import Send from './icons/Send.svelte'
export let content: Markup = ''
export let content: Markup = EmptyMarkup
export let showHeader = false
export let showActions = true
export let showSend = true
@ -63,6 +64,8 @@
$: devSize = $deviceInfo.size
$: shrinkButtons = checkAdaptiveMatching(devSize, 'sm')
$: canSubmit = (!isEmpty || haveAttachment) && !isEmptyMarkup(content) && !loading
function setContent (content: Markup): void {
textEditor?.setContent(content)
}
@ -148,9 +151,9 @@
{autofocus}
{boundary}
on:content={(ev) => {
if (!isEmpty || haveAttachment) {
if (canSubmit) {
dispatch('message', ev.detail)
content = ''
content = EmptyMarkup
textEditor?.clear()
}
}}
@ -207,7 +210,7 @@
{#if showSend}
<Button
{loading}
disabled={(isEmpty && !haveAttachment) || (!isEmpty && !content.replace(/<[^>]*>/g, '').trim()) || loading}
disabled={!canSubmit}
icon={iconSend ?? Send}
iconProps={{ size: buttonSize }}
kind={kindSend}

View File

@ -1,8 +1,9 @@
<script lang="ts">
import { AnyExtension } from '@tiptap/core'
import { Markup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { EmptyMarkup } from '@hcengineering/text'
import { ButtonSize, Label } from '@hcengineering/ui'
import { AnyExtension } from '@tiptap/core'
import { createEventDispatcher } from 'svelte'
import textEditorPlugin from '../plugin'
import StyledTextEditor from './StyledTextEditor.svelte'
@ -10,7 +11,7 @@
import { completionConfig } from './extensions'
export let label: IntlString | undefined = undefined
export let content: string | undefined
export let content: Markup | undefined
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
export let showButtons = true
@ -23,7 +24,7 @@
export let enableBackReferences = false
let rawValue: Markup
let oldContent: Markup = ''
let oldContent: Markup = EmptyMarkup
$: if (content !== undefined && oldContent !== content) {
oldContent = content

View File

@ -2,6 +2,7 @@
import { Markup } from '@hcengineering/core'
import { IntlString, getMetadata } from '@hcengineering/platform'
import presentation, { MessageViewer, getFileUrl, getImageSize } from '@hcengineering/presentation'
import { EmptyMarkup } from '@hcengineering/text'
import {
ActionIcon,
ButtonSize,
@ -63,7 +64,7 @@
export let mode = Mode.View
export function startEdit (): void {
rawValue = content ?? ''
rawValue = content ?? EmptyMarkup
needFocus = true
mode = Mode.Edit
}
@ -82,7 +83,7 @@
let canBlur = true
let focused = false
let rawValue: Markup
let oldContent: Markup = ''
let oldContent: Markup = EmptyMarkup
let modified: boolean = false
let textEditor: StyledTextEditor

View File

@ -13,19 +13,20 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { AnyExtension, mergeAttributes } from '@tiptap/core'
import { Markup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { EmptyMarkup } from '@hcengineering/text'
import { Button, ButtonSize, Scroller } from '@hcengineering/ui'
import { AnyExtension, mergeAttributes } from '@tiptap/core'
import { createEventDispatcher } from 'svelte'
import textEditorPlugin from '../plugin'
import { RefAction, TextEditorHandler, TextFormatCategory } from '../types'
import { defaultRefActions, getModelRefActions } from './editor/actions'
import TextEditor from './TextEditor.svelte'
import { Markup } from '@hcengineering/core'
const dispatch = createEventDispatcher()
export let content: string = ''
export let content: Markup = EmptyMarkup
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
export let showButtons: boolean = true
export let buttonSize: ButtonSize = 'medium'
@ -180,7 +181,7 @@
on:value
on:content={(ev) => {
dispatch('message', ev.detail)
content = ''
content = EmptyMarkup
textEditor?.clear()
}}
on:blur
@ -199,7 +200,7 @@
on:value
on:content={(ev) => {
dispatch('message', ev.detail)
content = ''
content = EmptyMarkup
textEditor?.clear()
}}
on:blur

View File

@ -34,11 +34,10 @@
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
import { SubmitExtension } from './extension/submit'
import { EditorKit } from '../kits/editor-kit'
import { getFileUrl, getImageSize } from '@hcengineering/presentation'
import { FileAttachFunction } from './extension/types'
import { ParseOptions } from '@tiptap/pm/model'
export let content: Markup = ''
export let content: Markup = EmptyMarkup
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
export let extensions: AnyExtension[] = []
export let textFormatCategories: TextFormatCategory[] = []

View File

@ -230,7 +230,10 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
const type = getType(ctype ?? 'other')
if (type === 'image') {
const node = view.state.schema.nodes.image.create({ 'file-id': _file })
const node = view.state.schema.nodes.image.create({
'file-id': _file,
src: getFileUrl(_file, 'full', uploadUrl)
})
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
view.dispatch(transaction)
result = true
@ -326,9 +329,11 @@ async function handleImageUpload (
}
try {
const size = await getImageSize(file, getFileUrl(attached.file, 'full', uploadUrl))
const url = getFileUrl(attached.file, 'full', uploadUrl)
const size = await getImageSize(file, url)
const node = view.state.schema.nodes.image.create({
'file-id': attached.file,
src: url,
width: Math.round(size.width / size.pixelRatio)
})

View File

@ -20,13 +20,13 @@
import { Editor, getSchema } from '@tiptap/core'
import { MarkupMarkType, MarkupNode, MarkupNodeType } from '../model'
import {
EmptyMarkup,
areEqualMarkups,
getMarkup,
htmlToJSON,
htmlToMarkup,
htmlToPmNode,
isEmptyMarkup,
isEmptyNode,
jsonToHTML,
jsonToMarkup,
jsonToText,
@ -54,7 +54,7 @@ const extensions = [ServerKit]
describe('EmptyMarkup', () => {
it('is empty markup', async () => {
const editor = new Editor({ extensions })
expect(getMarkup(editor)).toEqual(EmptyMarkup)
expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy()
})
})
@ -96,6 +96,18 @@ describe('isEmptyMarkup', () => {
const editor = new Editor({ extensions, content: '<p>hello</p>' })
expect(isEmptyMarkup(getMarkup(editor))).toBeFalsy()
})
it('returns true for various empty content', async () => {
expect(isEmptyMarkup(jsonToMarkup({ type: MarkupNodeType.doc }))).toBeTruthy()
expect(isEmptyMarkup(jsonToMarkup({ type: MarkupNodeType.doc, content: [] }))).toBeTruthy()
expect(
isEmptyMarkup(jsonToMarkup({ type: MarkupNodeType.doc, content: [{ type: MarkupNodeType.paragraph }] }))
).toBeTruthy()
expect(
isEmptyMarkup(
jsonToMarkup({ type: MarkupNodeType.doc, content: [{ type: MarkupNodeType.paragraph, content: [] }] })
)
).toBeTruthy()
})
})
describe('areEqualMarkups', () => {
@ -103,17 +115,114 @@ describe('areEqualMarkups', () => {
const markup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}'
expect(areEqualMarkups(markup, markup)).toBeTruthy()
})
it('returns true for the same content with different spaces', async () => {
it('returns true for empty content', async () => {
const markup1 = '{"type":"doc","content":[{"type":"paragraph"}]}'
const markup2 = '{"type":"doc","content":[{"type":"paragraph","content":[]}]}'
expect(areEqualMarkups(markup1, markup2)).toBeTruthy()
})
it('returns true for similar content', async () => {
const markup1 = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}'
const markup2 =
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","content":[],"marks":[],"attrs": {"color": null}}]}]}'
expect(areEqualMarkups(markup1, markup2)).toBeTruthy()
})
it('returns false for the same content with different spaces', async () => {
const markup1 = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}'
const markup2 =
'{"type":"doc","content":[{"type":"hardBreak"},{"type":"paragraph","content":[{"type":"text","text":"hello"}]},{"type":"hardBreak"}]}'
expect(areEqualMarkups(markup1, markup2)).toBeTruthy()
expect(areEqualMarkups(markup1, markup2)).toBeFalsy()
})
it('returns false for different content', async () => {
const markup1 = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}'
const markup2 = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"world"}]}]}'
expect(areEqualMarkups(markup1, markup2)).toBeFalsy()
})
it('returns false for different marks', async () => {
const markup1 =
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","marks":[{"type":"bold"}]}]}]}'
const markup2 =
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello","marks":[{"type":"italic"}]}]}]}'
expect(areEqualMarkups(markup1, markup2)).toBeFalsy()
})
})
describe('isEmptyNode', () => {
it('returns true for empty doc node', () => {
const node: MarkupNode = {
type: MarkupNodeType.doc,
content: []
}
expect(isEmptyNode(node)).toBeTruthy()
})
it('returns true for empty paragraph node', () => {
const node: MarkupNode = {
type: MarkupNodeType.doc,
content: [
{
type: MarkupNodeType.paragraph,
content: []
}
]
}
expect(isEmptyNode(node)).toBeTruthy()
})
it('returns true for empty text node', () => {
const node: MarkupNode = {
type: MarkupNodeType.doc,
content: [
{
type: MarkupNodeType.paragraph,
content: [
{
type: MarkupNodeType.text,
text: ''
}
]
}
]
}
expect(isEmptyNode(node)).toBeTruthy()
})
it('returns false for non-empty text node', () => {
const node: MarkupNode = {
type: MarkupNodeType.paragraph,
content: [
{
type: MarkupNodeType.text,
text: 'Hello, world!'
}
]
}
expect(isEmptyNode(node)).toBeFalsy()
})
it('returns false for non-empty text node', () => {
const node: MarkupNode = {
type: MarkupNodeType.paragraph,
content: [
{
type: MarkupNodeType.horizontal_rule
}
]
}
expect(isEmptyNode(node)).toBeFalsy()
})
it('returns false for non-empty node', () => {
const node: MarkupNode = {
type: MarkupNodeType.paragraph,
content: [
{
type: MarkupNodeType.text,
text: 'Hello, world!'
}
]
}
expect(isEmptyNode(node)).toBeFalsy()
})
})
describe('pmNodeToMarkup', () => {
@ -139,7 +248,7 @@ describe('markupToPmNode', () => {
describe('markupToJSON', () => {
it('with empty content', async () => {
expect(markupToJSON('')).toEqual({ type: 'doc', content: [{ type: 'paragraph' }] })
expect(markupToJSON('')).toEqual({ type: 'doc', content: [{ type: 'paragraph', content: [] }] })
})
it('with some content', async () => {
const markup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}'

View File

@ -66,7 +66,7 @@ export interface MarkupNode {
export function emptyMarkupNode (): MarkupNode {
return {
type: MarkupNodeType.doc,
content: [{ type: MarkupNodeType.paragraph }]
content: [{ type: MarkupNodeType.paragraph, content: [] }]
}
}

View File

@ -19,8 +19,9 @@ import { generateHTML, generateJSON } from '@tiptap/html'
import { Node as ProseMirrorNode, Schema } from '@tiptap/pm/model'
import { defaultExtensions } from '../extensions'
import { MarkupNode, emptyMarkupNode } from './model'
import { MarkupMark, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model'
import { nodeDoc, nodeParagraph, nodeText } from './dsl'
import { deepEqual } from 'fast-equals'
/** @public */
export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode())
@ -35,8 +36,7 @@ export function isEmptyMarkup (markup: Markup | undefined): boolean {
if (markup === undefined || markup === null || markup === '') {
return true
}
const node = markupToPmNode(markup)
return node.textContent.trim() === ''
return isEmptyNode(markupToJSON(markup))
}
/** @public */
@ -44,10 +44,66 @@ export function areEqualMarkups (markup1: Markup, markup2: Markup): boolean {
if (markup1 === markup2) {
return true
}
const node1 = markupToPmNode(markup1)
const node2 = markupToPmNode(markup2)
return node1.textContent.trim() === node2.textContent.trim()
return equalNodes(markupToJSON(markup1), markupToJSON(markup2))
}
/** @public */
export function areEqualJson (json1: MarkupNode, json2: MarkupNode): boolean {
return equalNodes(json1, json2)
}
function equalNodes (node1: MarkupNode, node2: MarkupNode): boolean {
if (node1.type !== node2.type) return false
const text1 = node1.text ?? ''
const text2 = node2.text ?? ''
if (text1 !== text2) return false
if (!equalArrays(node1.content, node2.content, equalNodes)) return false
if (!equalArrays(node1.marks, node2.marks, equalMarks)) return false
if (!equalRecords(node1.attrs, node2.attrs)) return false
return true
}
function equalArrays<T> (a: T[] | undefined, b: T[] | undefined, equal: (a: T, b: T) => boolean): boolean {
if (a === b) return true
const arr1 = a ?? []
const arr2 = b ?? []
if (arr1.length !== arr2.length) return false
return arr1.every((item1, i) => equal(item1, arr2[i]))
}
function equalRecords (a: Record<string, any> | undefined, b: Record<string, any> | undefined): boolean {
if (a === b) return true
a = Object.fromEntries(Object.entries(a ?? {}).filter(([_, v]) => v != null))
b = Object.fromEntries(Object.entries(b ?? {}).filter(([_, v]) => v != null))
return deepEqual(a, b)
}
function equalMarks (a: MarkupMark, b: MarkupMark): boolean {
return a.type === b.type && equalRecords(a.attrs, b.attrs)
}
const emptyNodes = [MarkupNodeType.hard_break]
const nonEmptyNodes = [
MarkupNodeType.horizontal_rule,
MarkupNodeType.image,
MarkupNodeType.reference,
MarkupNodeType.sub,
MarkupNodeType.table
]
/** @public */
export function isEmptyNode (node: MarkupNode): boolean {
if (emptyNodes.includes(node.type)) return true
if (nonEmptyNodes.includes(node.type)) return false
if (node.text !== undefined && node.text?.trim().length > 0) return false
const content = node.content ?? []
return content.every(isEmptyNode)
}
// Markup

View File

@ -18,7 +18,7 @@
import { Account, Class, Doc, generateId, IdMap, Markup, Ref, Space, toIdMap } from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError, Asset } from '@hcengineering/platform'
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import textEditor, { AttachIcon, type RefAction, ReferenceInput } from '@hcengineering/text-editor'
import textEditor, { AttachIcon, EmptyMarkup, type RefAction, ReferenceInput } from '@hcengineering/text-editor'
import { Loading, type AnySvelteComponent } from '@hcengineering/ui'
import { deleteFile, getAttachmentMetadata, uploadFile } from '../utils'
import attachment from '../plugin'
@ -27,7 +27,7 @@
export let objectId: Ref<Doc>
export let space: Ref<Space>
export let _class: Ref<Class<Doc>>
export let content: Markup = ''
export let content: Markup = EmptyMarkup
export let iconSend: Asset | AnySvelteComponent | undefined = undefined
export let labelSend: IntlString | undefined = undefined
export let showSend = true

View File

@ -18,7 +18,7 @@
import { Account, Class, Doc, generateId, Markup, Ref, Space, toIdMap } from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import textEditor, { AttachIcon, type RefAction, StyledTextBox } from '@hcengineering/text-editor'
import textEditor, { AttachIcon, EmptyMarkup, type RefAction, StyledTextBox } from '@hcengineering/text-editor'
import { ButtonSize } from '@hcengineering/ui'
import attachment from '../plugin'
@ -28,7 +28,7 @@
export let objectId: Ref<Doc> | undefined = undefined
export let space: Ref<Space> | undefined = undefined
export let _class: Ref<Class<Doc>> | undefined = undefined
export let content: Markup = ''
export let content: Markup = EmptyMarkup
export let placeholder: IntlString | undefined = undefined
export let alwaysEdit = false
export let showButtons = false

View File

@ -17,7 +17,7 @@
import { Person, PersonAccount } from '@hcengineering/contact'
import { Class, Doc, Markup, Ref, getCurrentAccount } from '@hcengineering/core'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { EmptyMarkup, StyledTextBox } from '@hcengineering/text-editor'
import {
Button,
EditBox,
@ -62,7 +62,7 @@
let reminders = [30 * 60 * 1000]
let description: Markup = ''
let description: Markup = EmptyMarkup
let visibility: Visibility = 'private'
const me = getCurrentAccount()
let space: Ref<Calendar> = `${me._id}_calendar` as Ref<Calendar>

View File

@ -23,7 +23,7 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import setting, { Integration } from '@hcengineering/setting'
import templates, { TemplateDataProvider } from '@hcengineering/templates'
import { StyledTextEditor, isEmptyMarkup } from '@hcengineering/text-editor'
import { EmptyMarkup, StyledTextEditor, isEmptyMarkup } from '@hcengineering/text-editor'
import {
Button,
EditBox,
@ -70,7 +70,7 @@
const attachmentParentId = generateId()
let subject: string = ''
let content: Markup = ''
let content: Markup = EmptyMarkup
let copy: string = ''
let saved = false

View File

@ -21,6 +21,7 @@
import { translate } from '@hcengineering/platform'
import { Card, createQuery, getClient } from '@hcengineering/presentation'
import { EmployeeBox } from '@hcengineering/contact-resources'
import { EmptyMarkup } from '@hcengineering/text-editor'
import ui, {
Button,
DateRangePresenter,
@ -39,7 +40,7 @@
export let docQuery: DocumentQuery<Employee> | undefined
export let employeeRequests: Map<Ref<Staff>, Request[]>
let description: Markup = ''
let description: Markup = EmptyMarkup
let employee: Ref<Employee> = staff._id
const objectId: Ref<Request> = generateId()

View File

@ -50,6 +50,7 @@
"@hcengineering/platform": "^0.6.9",
"@hcengineering/preference": "^0.6.9",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/text": "^0.6.1",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/view-resources": "^0.6.0",

View File

@ -42,6 +42,8 @@
} from '@hcengineering/presentation'
import type { Applicant, Candidate, Vacancy } from '@hcengineering/recruit'
import task, { TaskType, getStates, makeRank } from '@hcengineering/task'
import { TaskKindSelector, selectedTypeStore, typeStore } from '@hcengineering/task-resources'
import { EmptyMarkup } from '@hcengineering/text-editor'
import ui, {
Button,
ColorPopup,
@ -62,12 +64,11 @@
import CandidateCard from './CandidateCard.svelte'
import VacancyCard from './VacancyCard.svelte'
import VacancyOrgPresenter from './VacancyOrgPresenter.svelte'
import { TaskKindSelector, selectedTypeStore, typeStore } from '@hcengineering/task-resources'
export let space: Ref<Vacancy>
export let candidate: Ref<Candidate>
export let assignee: Ref<Employee>
export let comment: Markup = ''
export let comment: Markup = EmptyMarkup
$: _comment = comment

View File

@ -32,7 +32,7 @@
import { UserBox, UserBoxList } from '@hcengineering/contact-resources'
import type { Applicant, Candidate, Review } from '@hcengineering/recruit'
import task from '@hcengineering/task'
import { StyledTextArea } from '@hcengineering/text-editor'
import { EmptyMarkup, StyledTextArea } from '@hcengineering/text-editor'
import { DateRangePresenter, EditBox, Status as StatusControl } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { ObjectSearchBox } from '@hcengineering/view-resources'
@ -55,7 +55,7 @@
let status: Status = OK
let title: string = ''
let description: Markup = ''
let description: Markup = EmptyMarkup
let startDate: number = initDate.getTime()
let dueDate: number = initDate.getTime() + 30 * 60 * 1000
let location: string = ''

View File

@ -19,7 +19,7 @@
import { AttachedData, getCurrentAccount, Markup, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Request, RequestStatus } from '@hcengineering/request'
import { type RefAction, isEmptyMarkup } from '@hcengineering/text-editor'
import { type RefAction, EmptyMarkup, isEmptyMarkup } from '@hcengineering/text-editor'
import { Button } from '@hcengineering/ui'
import request from '../plugin'
@ -53,7 +53,7 @@
})
}
let message: Markup = ''
let message: Markup = EmptyMarkup
let attachments: number | undefined = 0
async function onUpdate (event: CustomEvent<AttachedData<ChatMessage>>) {

View File

@ -41,8 +41,7 @@
MultipleDraftController,
SpaceSelector,
createQuery,
getClient,
reduceCalls
getClient
} from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { TaskType, makeRank } from '@hcengineering/task'
@ -172,7 +171,7 @@
const base: IssueDraft = {
_id: id ?? generateId(),
title: '',
description: '',
description: EmptyMarkup,
kind: '' as Ref<TaskType>,
priority: priority ?? IssuePriority.NoPriority,
space: _space as Ref<Project>,
@ -314,7 +313,7 @@
object = {
...object,
description: description ?? '',
description: description ?? EmptyMarkup,
...templBase,
template: {
template: template._id

View File

@ -33,6 +33,7 @@ import core, {
toWorkspaceString
} from '@hcengineering/core'
import { StorageAdapter } from '@hcengineering/server-core'
import { areEqualMarkups } from '@hcengineering/text'
import { Transformer } from '@hocuspocus/transformer'
import { MongoClient } from 'mongodb'
import { Doc as YDoc } from 'yjs'
@ -276,9 +277,11 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
const content = await ctx.with('transform', {}, () => {
return this.transformer.fromYdoc(document, objectAttr)
})
await ctx.with('update', {}, async () => {
await client.diffUpdate(current, { [objectAttr]: content })
})
if (!areEqualMarkups(content, (current as any)[objectAttr])) {
await ctx.with('update', {}, async () => {
await client.diffUpdate(current, { [objectAttr]: content })
})
}
}
}
}