mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
Fix unintendent collaborative content changes (#5388)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
parent
12ae2fa0ec
commit
55c1e745b7
@ -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
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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[] = []
|
||||
|
@ -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)
|
||||
})
|
||||
|
||||
|
@ -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"}]}]}'
|
||||
|
@ -66,7 +66,7 @@ export interface MarkupNode {
|
||||
export function emptyMarkupNode (): MarkupNode {
|
||||
return {
|
||||
type: MarkupNodeType.doc,
|
||||
content: [{ type: MarkupNodeType.paragraph }]
|
||||
content: [{ type: MarkupNodeType.paragraph, content: [] }]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 = ''
|
||||
|
@ -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>>) {
|
||||
|
@ -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
|
||||
|
@ -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 })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user