mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-6163 Store editor content as JSON (#5069)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
parent
04095fcfc0
commit
ed6fe769a4
File diff suppressed because it is too large
Load Diff
@ -38,6 +38,7 @@
|
||||
"@hcengineering/model-view": "^0.6.0",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/view": "^0.6.9"
|
||||
}
|
||||
|
@ -13,13 +13,18 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Class, type Doc, type Domain, type Ref } from '@hcengineering/core'
|
||||
import { type DocUpdateMessage } from '@hcengineering/activity'
|
||||
import core, { type Class, type Doc, type Domain, type Ref } from '@hcengineering/core'
|
||||
import {
|
||||
type MigrateOperation,
|
||||
type MigrateUpdate,
|
||||
type MigrationClient,
|
||||
type MigrationDocumentQuery,
|
||||
type MigrationIterator,
|
||||
type MigrationUpgradeClient,
|
||||
tryMigrate
|
||||
} from '@hcengineering/model'
|
||||
import { htmlToMarkup } from '@hcengineering/text'
|
||||
|
||||
import activity from './plugin'
|
||||
import { activityId, DOMAIN_ACTIVITY } from './index'
|
||||
@ -35,12 +40,89 @@ async function migrateReactions (client: MigrationClient): Promise<void> {
|
||||
await client.move(DOMAIN_CHUNTER, { _class: activity.class.Reaction }, DOMAIN_ACTIVITY)
|
||||
}
|
||||
|
||||
async function migrateMarkup (client: MigrationClient): Promise<void> {
|
||||
const iterator = await client.traverse<DocUpdateMessage>(
|
||||
DOMAIN_ACTIVITY,
|
||||
{
|
||||
_class: activity.class.DocUpdateMessage,
|
||||
'attributeUpdates.attrClass': {
|
||||
$in: [core.class.TypeMarkup, core.class.TypeCollaborativeMarkup]
|
||||
}
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
_id: 1,
|
||||
attributeUpdates: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
console.log('processing', activity.class.DocUpdateMessage)
|
||||
try {
|
||||
await processMigrateMarkupFor(client, iterator)
|
||||
console.log('processing finished', activity.class.DocUpdateMessage)
|
||||
} finally {
|
||||
await iterator.close()
|
||||
}
|
||||
}
|
||||
|
||||
async function processMigrateMarkupFor (
|
||||
client: MigrationClient,
|
||||
iterator: MigrationIterator<DocUpdateMessage>
|
||||
): Promise<void> {
|
||||
let processed = 0
|
||||
while (true) {
|
||||
const docs = await iterator.next(1000)
|
||||
if (docs === null || docs.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const ops: { filter: MigrationDocumentQuery<DocUpdateMessage>, update: MigrateUpdate<DocUpdateMessage> }[] = []
|
||||
|
||||
for (const doc of docs) {
|
||||
if (doc.attributeUpdates == null) continue
|
||||
if (doc.attributeUpdates.set == null) continue
|
||||
if (doc.attributeUpdates.set.length === 0) continue
|
||||
|
||||
const update: MigrateUpdate<DocUpdateMessage> = {}
|
||||
|
||||
const attributeUpdatesSet = [...doc.attributeUpdates.set]
|
||||
for (let i = 0; i < attributeUpdatesSet.length; i++) {
|
||||
const value = attributeUpdatesSet[i]
|
||||
if (value != null && typeof value === 'string') {
|
||||
attributeUpdatesSet[i] = htmlToMarkup(value)
|
||||
}
|
||||
|
||||
update['attributeUpdates.set'] = attributeUpdatesSet
|
||||
}
|
||||
|
||||
const prevValue = doc.attributeUpdates.prevValue
|
||||
if (prevValue != null && typeof prevValue === 'string') {
|
||||
update['attributeUpdates.prevValue'] = htmlToMarkup(prevValue)
|
||||
}
|
||||
|
||||
ops.push({ filter: { _id: doc._id }, update })
|
||||
}
|
||||
|
||||
if (ops.length > 0) {
|
||||
await client.bulk(DOMAIN_ACTIVITY, ops)
|
||||
}
|
||||
|
||||
processed += docs.length
|
||||
console.log('...processed', processed)
|
||||
}
|
||||
}
|
||||
|
||||
export const activityOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, activityId, [
|
||||
{
|
||||
state: 'reactions',
|
||||
func: migrateReactions
|
||||
},
|
||||
{
|
||||
state: 'markup',
|
||||
func: migrateMarkup
|
||||
}
|
||||
])
|
||||
},
|
||||
|
@ -41,6 +41,7 @@ import { timeOperation } from '@hcengineering/model-time'
|
||||
import { activityOperation } from '@hcengineering/model-activity'
|
||||
import { activityServerOperation } from '@hcengineering/model-server-activity'
|
||||
import { documentOperation } from '@hcengineering/model-document'
|
||||
import { textEditorOperation } from '@hcengineering/model-text-editor'
|
||||
|
||||
export const migrateOperations: [string, MigrateOperation][] = [
|
||||
['core', coreOperation],
|
||||
@ -68,6 +69,7 @@ export const migrateOperations: [string, MigrateOperation][] = [
|
||||
['time', timeOperation],
|
||||
['activityServer', activityServerOperation],
|
||||
['document', documentOperation],
|
||||
['textEditor', textEditorOperation],
|
||||
// We should call it after activityServer and chunter
|
||||
['notification', notificationOperation]
|
||||
]
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
type Doc,
|
||||
type DocumentQuery,
|
||||
type Domain,
|
||||
type Markup,
|
||||
type Ref,
|
||||
type Timestamp,
|
||||
type Tx
|
||||
@ -41,8 +42,8 @@ import {
|
||||
TypeBoolean,
|
||||
TypeDate,
|
||||
TypeIntlString,
|
||||
TypeMarkup,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
UX,
|
||||
type Builder
|
||||
} from '@hcengineering/model'
|
||||
@ -266,8 +267,8 @@ export class TCommonInboxNotification extends TInboxNotification implements Comm
|
||||
|
||||
headerIcon?: Asset
|
||||
|
||||
@Prop(TypeString(), notification.string.Message)
|
||||
messageHtml?: string
|
||||
@Prop(TypeMarkup(), notification.string.Message)
|
||||
messageHtml?: Markup
|
||||
|
||||
props?: Record<string, any>
|
||||
icon?: Asset
|
||||
|
@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import type { Employee, Organization } from '@hcengineering/contact'
|
||||
import { type Domain, IndexKind, type Ref, type Status, type Timestamp } from '@hcengineering/core'
|
||||
import { type Domain, IndexKind, type Markup, type Ref, type Status, type Timestamp } from '@hcengineering/core'
|
||||
import {
|
||||
Collection,
|
||||
Hidden,
|
||||
@ -221,7 +221,7 @@ export class TOpinion extends TAttachedDoc implements Opinion {
|
||||
comments?: number
|
||||
|
||||
@Prop(TypeMarkup(), recruit.string.Description)
|
||||
description!: string
|
||||
description!: Markup
|
||||
|
||||
@Prop(TypeString(), recruit.string.OpinionValue)
|
||||
value!: string
|
||||
|
@ -14,8 +14,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Domain, DOMAIN_MODEL, IndexKind, type Ref } from '@hcengineering/core'
|
||||
import { type Builder, Index, Model, Prop, TypeString, UX } from '@hcengineering/model'
|
||||
import { type Domain, DOMAIN_MODEL, IndexKind, type Ref, type Markup } from '@hcengineering/core'
|
||||
import { type Builder, Index, Model, Prop, TypeString, UX, TypeMarkup } from '@hcengineering/model'
|
||||
import core, { TDoc, TSpace } from '@hcengineering/model-core'
|
||||
import textEditor from '@hcengineering/model-text-editor'
|
||||
import tracker from '@hcengineering/model-tracker'
|
||||
@ -43,9 +43,9 @@ export class TMessageTemplate extends TDoc implements MessageTemplate {
|
||||
@Index(IndexKind.FullText)
|
||||
title!: string
|
||||
|
||||
@Prop(TypeString(), templates.string.Message)
|
||||
@Prop(TypeMarkup(), templates.string.Message)
|
||||
@Index(IndexKind.FullText)
|
||||
message!: string
|
||||
message!: Markup
|
||||
}
|
||||
|
||||
@Model(templates.class.TemplateCategory, core.class.Space)
|
||||
|
@ -32,6 +32,7 @@
|
||||
"@hcengineering/model": "^0.6.7",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"@hcengineering/text-editor": "^0.6.0",
|
||||
"@hcengineering/model-core": "^0.6.0"
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import type { Asset, IntlString, Resource } from '@hcengineering/platform'
|
||||
import { type RefInputAction, type RefInputActionItem } from '@hcengineering/text-editor/src/types'
|
||||
import textEditor from './plugin'
|
||||
|
||||
export { textEditorOperation } from './migration'
|
||||
export { textEditorId } from '@hcengineering/text-editor/src/plugin'
|
||||
export { default } from './plugin'
|
||||
export type { RefInputAction, RefInputActionItem }
|
||||
|
104
models/text-editor/src/migration.ts
Normal file
104
models/text-editor/src/migration.ts
Normal file
@ -0,0 +1,104 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import core, { type AnyAttribute, type Doc, type Domain } from '@hcengineering/core'
|
||||
import {
|
||||
type MigrateOperation,
|
||||
type MigrateUpdate,
|
||||
type MigrationClient,
|
||||
type MigrationDocumentQuery,
|
||||
type MigrationIterator,
|
||||
type MigrationUpgradeClient,
|
||||
tryMigrate
|
||||
} from '@hcengineering/model'
|
||||
import { htmlToMarkup } from '@hcengineering/text'
|
||||
|
||||
async function migrateMarkup (client: MigrationClient): Promise<void> {
|
||||
const hierarchy = client.hierarchy
|
||||
const classes = hierarchy.getDescendants(core.class.Doc)
|
||||
for (const _class of classes) {
|
||||
const domain = hierarchy.findDomain(_class)
|
||||
if (domain === undefined) continue
|
||||
|
||||
const attributes = hierarchy.getAllAttributes(_class)
|
||||
const filtered = Array.from(attributes.values()).filter((attribute) => {
|
||||
return (
|
||||
hierarchy.isDerived(attribute.type._class, core.class.TypeMarkup) ||
|
||||
hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeMarkup)
|
||||
)
|
||||
})
|
||||
if (filtered.length === 0) continue
|
||||
|
||||
console.log('processing', _class, filtered.length, 'attributes')
|
||||
const iterator = await client.traverse(domain, { _class })
|
||||
try {
|
||||
await processMigrateMarkupFor(domain, filtered, client, iterator)
|
||||
console.log('processing finished', _class)
|
||||
} finally {
|
||||
await iterator.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function processMigrateMarkupFor (
|
||||
domain: Domain,
|
||||
attributes: AnyAttribute[],
|
||||
client: MigrationClient,
|
||||
iterator: MigrationIterator<Doc>
|
||||
): Promise<void> {
|
||||
let processed = 0
|
||||
while (true) {
|
||||
const docs = await iterator.next(1000)
|
||||
if (docs === null || docs.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const operations: { filter: MigrationDocumentQuery<Doc>, update: MigrateUpdate<Doc> }[] = []
|
||||
|
||||
for (const doc of docs) {
|
||||
const update: MigrateUpdate<Doc> = {}
|
||||
|
||||
for (const attribute of attributes) {
|
||||
const value = (doc as any)[attribute.name]
|
||||
if (value != null) {
|
||||
update[attribute.name] = htmlToMarkup(value)
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
operations.push({ filter: { _id: doc._id }, update })
|
||||
}
|
||||
}
|
||||
|
||||
if (operations.length > 0) {
|
||||
await client.bulk(domain, operations)
|
||||
}
|
||||
|
||||
processed += docs.length
|
||||
console.log('...processed', processed)
|
||||
}
|
||||
}
|
||||
|
||||
export const textEditorOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
await tryMigrate(client, 'text-editor', [
|
||||
{
|
||||
state: 'markup',
|
||||
func: migrateMarkup
|
||||
}
|
||||
])
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {}
|
||||
}
|
@ -530,8 +530,8 @@ export interface DocIndexState extends Doc {
|
||||
attributes: Record<string, any>
|
||||
mixins?: Ref<Class<Doc>>[]
|
||||
// Full Summary
|
||||
fullSummary?: Markup | null
|
||||
shortSummary?: Markup | null
|
||||
fullSummary?: string | null
|
||||
shortSummary?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,6 +45,7 @@
|
||||
"@hcengineering/query": "^0.6.8",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/view": "^0.6.9",
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"svelte": "^4.2.12",
|
||||
"@hcengineering/client": "^0.6.14",
|
||||
"@hcengineering/collaborator-client": "^0.6.0",
|
||||
|
25
packages/presentation/src/components/HTMLViewer.svelte
Normal file
25
packages/presentation/src/components/HTMLViewer.svelte
Normal file
@ -0,0 +1,25 @@
|
||||
<!--
|
||||
// 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 { htmlToJSON } from '@hcengineering/text'
|
||||
import Node from './markup/Node.svelte'
|
||||
|
||||
export let value: string
|
||||
export let preview = false
|
||||
|
||||
$: node = htmlToJSON(value)
|
||||
</script>
|
||||
|
||||
<Node {node} {preview} />
|
@ -16,8 +16,7 @@
|
||||
import { translate, type IntlString } from '@hcengineering/platform'
|
||||
import { Button, FocusHandler, Label, createFocusManager } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import presentation from '..'
|
||||
import MessageViewer from './MessageViewer.svelte'
|
||||
import presentation, { HTMLViewer } from '..'
|
||||
|
||||
export let label: IntlString
|
||||
export let labelProps: IntlString
|
||||
@ -41,7 +40,7 @@
|
||||
<div class="message">
|
||||
{#if richMessage}
|
||||
{#await translate(message, params) then msg}
|
||||
<MessageViewer message={msg} />
|
||||
<HTMLViewer value={msg} />
|
||||
{/await}
|
||||
{:else}
|
||||
<Label label={message} {params} />
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
// Copyright © 2021, 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
|
||||
@ -14,21 +14,18 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Nodes from './message/Nodes.svelte'
|
||||
import { isEmptyMarkup, markupToJSON } from '@hcengineering/text'
|
||||
import Node from './markup/Node.svelte'
|
||||
|
||||
export let message: string
|
||||
export let preview = false
|
||||
|
||||
let dom: HTMLElement
|
||||
$: node = markupToJSON(message)
|
||||
$: empty = isEmptyMarkup(message)
|
||||
|
||||
const parser = new DOMParser()
|
||||
|
||||
export function isEmpty () {
|
||||
return doc.documentElement.innerText.length === 0
|
||||
export function isEmpty (): boolean {
|
||||
return empty
|
||||
}
|
||||
|
||||
$: doc = parser.parseFromString(message, 'text/html')
|
||||
$: dom = doc.firstChild?.childNodes[1] as HTMLElement
|
||||
</script>
|
||||
|
||||
<Nodes nodes={dom.childNodes} {preview} />
|
||||
<Node {node} {preview} />
|
||||
|
63
packages/presentation/src/components/markup/Mark.svelte
Normal file
63
packages/presentation/src/components/markup/Mark.svelte
Normal file
@ -0,0 +1,63 @@
|
||||
<!--
|
||||
// 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 { getMetadata } from '@hcengineering/platform'
|
||||
import { MarkupMark, MarkupMarkType } from '@hcengineering/text'
|
||||
import { navigate, parseLocation } from '@hcengineering/ui'
|
||||
|
||||
import presentation from '../../plugin'
|
||||
|
||||
export let mark: MarkupMark
|
||||
|
||||
function handleLink (e: MouseEvent): void {
|
||||
try {
|
||||
const href = mark.attrs.href
|
||||
if (href != null && href !== '') {
|
||||
const url = new URL(href)
|
||||
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
|
||||
|
||||
if (url.origin === frontUrl) {
|
||||
e.preventDefault()
|
||||
navigate(parseLocation(url))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to handle link', mark, err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if mark}
|
||||
{@const attrs = mark.attrs ?? {}}
|
||||
|
||||
{#if mark.type === MarkupMarkType.bold}
|
||||
<strong><slot /></strong>
|
||||
{:else if mark.type === MarkupMarkType.code}
|
||||
<pre class="proseCode"><slot /></pre>
|
||||
{:else if mark.type === MarkupMarkType.em}
|
||||
<em><slot /></em>
|
||||
{:else if mark.type === MarkupMarkType.link}
|
||||
<a href={attrs.href} target={attrs.target} on:click={handleLink}>
|
||||
<slot />
|
||||
</a>
|
||||
{:else if mark.type === MarkupMarkType.strike}
|
||||
<s><slot /></s>
|
||||
{:else if mark.type === MarkupMarkType.underline}
|
||||
<u><slot /></u>
|
||||
{:else}
|
||||
unknown mark: "{mark.type}"
|
||||
<slot />
|
||||
{/if}
|
||||
{/if}
|
31
packages/presentation/src/components/markup/Node.svelte
Normal file
31
packages/presentation/src/components/markup/Node.svelte
Normal file
@ -0,0 +1,31 @@
|
||||
<!--
|
||||
// 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 { MarkupNode } from '@hcengineering/text'
|
||||
|
||||
import NodeMarks from './NodeMarks.svelte'
|
||||
import NodeContent from './NodeContent.svelte'
|
||||
|
||||
export let node: MarkupNode
|
||||
export let preview = false
|
||||
</script>
|
||||
|
||||
{#if node}
|
||||
{@const marks = node.marks ?? []}
|
||||
|
||||
<NodeMarks {marks}>
|
||||
<NodeContent {node} {preview} />
|
||||
</NodeMarks>
|
||||
{/if}
|
151
packages/presentation/src/components/markup/NodeContent.svelte
Normal file
151
packages/presentation/src/components/markup/NodeContent.svelte
Normal file
@ -0,0 +1,151 @@
|
||||
<!--
|
||||
// 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 { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { MarkupNode, MarkupNodeType } from '@hcengineering/text'
|
||||
|
||||
import MarkupNodes from './Nodes.svelte'
|
||||
import ObjectNode from './ObjectNode.svelte'
|
||||
|
||||
export let node: MarkupNode
|
||||
export let preview = false
|
||||
|
||||
function toRef (objectId: string): Ref<Doc> {
|
||||
return objectId as Ref<Doc>
|
||||
}
|
||||
|
||||
function toClassRef (objectClass: string): Ref<Class<Doc>> {
|
||||
if (objectClass === 'contact:class:Employee') {
|
||||
return 'contact:mixin:Employee' as Ref<Class<Doc>>
|
||||
}
|
||||
return objectClass as Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
function toString (value: string | number | undefined): string | undefined {
|
||||
return value !== undefined ? `${value}` : undefined
|
||||
}
|
||||
|
||||
function toNumber (value: string | number | undefined): number | undefined {
|
||||
return value !== undefined ? (typeof value === 'string' ? parseInt(value) : value) : undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if node}
|
||||
{@const attrs = node.attrs ?? {}}
|
||||
{@const nodes = node.content ?? []}
|
||||
|
||||
{#if node.type === MarkupNodeType.doc}
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
{:else if node.type === MarkupNodeType.text}
|
||||
{node.text}
|
||||
{:else if node.type === MarkupNodeType.paragraph}
|
||||
<p class="p-inline contrast" class:overflow-label={preview}>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</p>
|
||||
{:else if node.type === MarkupNodeType.blockquote}
|
||||
<blockquote class="proseBlockQuote" style:margin={preview ? '0' : null}>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</blockquote>
|
||||
{:else if node.type === MarkupNodeType.horizontal_rule}
|
||||
<hr />
|
||||
{:else if node.type === MarkupNodeType.heading}
|
||||
{@const level = toNumber(node.attrs?.level) ?? 1}
|
||||
{@const element = `h${level}`}
|
||||
<svelte:element this={element}>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</svelte:element>
|
||||
{:else if node.type === MarkupNodeType.code_block}
|
||||
<pre class="proseCodeBlock" style:margin={preview ? '0' : null}><code><MarkupNodes {nodes} {preview} /></code></pre>
|
||||
{:else if node.type === MarkupNodeType.image}
|
||||
{@const src = toString(attrs.src)}
|
||||
{@const alt = toString(attrs.alt)}
|
||||
{@const width = toString(attrs.width)}
|
||||
{@const height = toString(attrs.height)}
|
||||
<div class="imgContainer max-h-60 max-w-60">
|
||||
<img {src} {alt} {width} {height} />
|
||||
</div>
|
||||
{:else if node.type === MarkupNodeType.reference}
|
||||
{@const objectId = toString(attrs.id)}
|
||||
{@const objectClass = toString(attrs.objectclass)}
|
||||
{@const objectLabel = toString(attrs.label)}
|
||||
|
||||
{#if objectClass !== undefined && objectId !== undefined}
|
||||
<ObjectNode _id={toRef(objectId)} _class={toClassRef(objectClass)} title={objectLabel} />
|
||||
{:else}
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
{/if}
|
||||
{:else if node.type === MarkupNodeType.hard_break}
|
||||
<br />
|
||||
{:else if node.type === MarkupNodeType.ordered_list}
|
||||
<ol style:margin={preview ? '0' : null}>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</ol>
|
||||
{:else if node.type === MarkupNodeType.bullet_list}
|
||||
<ul style:margin={preview ? '0' : null}>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</ul>
|
||||
{:else if node.type === MarkupNodeType.list_item}
|
||||
<li>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</li>
|
||||
{:else if node.type === MarkupNodeType.taskList}
|
||||
<!-- TODO not implemented -->
|
||||
{:else if node.type === MarkupNodeType.taskItem}
|
||||
<!-- TODO not implemented -->
|
||||
{:else if node.type === MarkupNodeType.sub}
|
||||
<sub>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</sub>
|
||||
{:else if node.type === MarkupNodeType.table}
|
||||
<table class="proseTable">
|
||||
<tbody>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</tbody>
|
||||
</table>
|
||||
{:else if node.type === MarkupNodeType.table_row}
|
||||
<tr>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</tr>
|
||||
{:else if node.type === MarkupNodeType.table_cell}
|
||||
{@const colspan = toNumber(attrs.colspan)}
|
||||
{@const rowspan = toNumber(attrs.rowspan)}
|
||||
<td {colspan} {rowspan}>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</td>
|
||||
{:else if node.type === MarkupNodeType.table_header}
|
||||
{@const colspan = toNumber(attrs.colspan)}
|
||||
{@const rowspan = toNumber(attrs.rowspan)}
|
||||
<th {colspan} {rowspan}>
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
</th>
|
||||
{:else}
|
||||
unknown node: "{node.type}"
|
||||
<MarkupNodes {nodes} {preview} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.imgContainer {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.img {
|
||||
:global(img) {
|
||||
object-fit: contain;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
39
packages/presentation/src/components/markup/NodeMarks.svelte
Normal file
39
packages/presentation/src/components/markup/NodeMarks.svelte
Normal file
@ -0,0 +1,39 @@
|
||||
<!--
|
||||
// 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 { MarkupMark } from '@hcengineering/text'
|
||||
import Mark from './Mark.svelte'
|
||||
|
||||
export let marks: MarkupMark[]
|
||||
</script>
|
||||
|
||||
{#if marks.length > 0}
|
||||
{@const mark = marks[0]}
|
||||
{@const others = marks.slice(1)}
|
||||
|
||||
{#if others.length > 0}
|
||||
<Mark {mark}>
|
||||
<svelte:self marks={others}>
|
||||
<slot />
|
||||
</svelte:self>
|
||||
</Mark>
|
||||
{:else}
|
||||
<Mark {mark}>
|
||||
<slot />
|
||||
</Mark>
|
||||
{/if}
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
27
packages/presentation/src/components/markup/Nodes.svelte
Normal file
27
packages/presentation/src/components/markup/Nodes.svelte
Normal file
@ -0,0 +1,27 @@
|
||||
<!--
|
||||
// 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 { MarkupNode } from '@hcengineering/text'
|
||||
import Node from './Node.svelte'
|
||||
|
||||
export let nodes: MarkupNode[]
|
||||
export let preview = false
|
||||
</script>
|
||||
|
||||
{#if nodes}
|
||||
{#each nodes as node}
|
||||
<Node {node} {preview} />
|
||||
{/each}
|
||||
{/if}
|
@ -1,193 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { CheckBox, navigate, parseLocation } from '@hcengineering/ui'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
|
||||
import presentation from '../../plugin'
|
||||
import ObjectNode from './ObjectNode.svelte'
|
||||
|
||||
export let nodes: NodeListOf<any>
|
||||
export let preview = false
|
||||
|
||||
function prevName (pos: number, nodes: NodeListOf<any>): string | undefined {
|
||||
while (true) {
|
||||
if (nodes[pos - 1]?.nodeName === '#text' && (nodes[pos - 1]?.data ?? '').trim() === '') {
|
||||
pos--
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return nodes[pos - 1]?.nodeName
|
||||
}
|
||||
|
||||
function handleLink (node: HTMLElement, e: MouseEvent) {
|
||||
try {
|
||||
const href = node.getAttribute('href')
|
||||
if (href) {
|
||||
const url = new URL(href)
|
||||
const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin
|
||||
|
||||
if (url.origin === frontUrl) {
|
||||
e.preventDefault()
|
||||
navigate(parseLocation(url))
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
function correctClass (clName: string): Ref<Class<Doc>> {
|
||||
if (clName === 'contact:class:Employee') {
|
||||
return 'contact:mixin:Employee' as Ref<Class<Doc>>
|
||||
}
|
||||
return clName as Ref<Class<Doc>>
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if nodes}
|
||||
{#each nodes as node, ni}
|
||||
{#if node.nodeType === Node.TEXT_NODE}
|
||||
{node.data}
|
||||
{:else if node.nodeName === 'EM'}
|
||||
<em><svelte:self nodes={node.childNodes} {preview} /></em>
|
||||
{:else if node.nodeName === 'STRONG' || node.nodeName === 'B'}
|
||||
<strong><svelte:self nodes={node.childNodes} {preview} /></strong>
|
||||
{:else if node.nodeName === 'U'}
|
||||
<u><svelte:self nodes={node.childNodes} {preview} /></u>
|
||||
{:else if node.nodeName === 'P'}
|
||||
{#if node.childNodes.length > 0}
|
||||
<p class="p-inline contrast" class:overflow-label={preview}>
|
||||
<svelte:self nodes={node.childNodes} />
|
||||
</p>
|
||||
{/if}
|
||||
{:else if node.nodeName === 'BLOCKQUOTE'}
|
||||
<blockquote style:margin={preview ? '0' : null}><svelte:self nodes={node.childNodes} {preview} /></blockquote>
|
||||
{:else if node.nodeName === 'CODE'}
|
||||
<pre class="proseCode"><svelte:self nodes={node.childNodes} {preview} /></pre>
|
||||
{:else if node.nodeName === 'PRE'}
|
||||
<pre class="proseCodeBlock" style:margin={preview ? '0' : null}><svelte:self
|
||||
nodes={node.childNodes}
|
||||
{preview}
|
||||
/></pre>
|
||||
{:else if node.nodeName === 'BR'}
|
||||
{@const pName = prevName(ni, nodes)}
|
||||
{#if pName !== 'P' && pName !== 'BR' && pName !== undefined}
|
||||
<br />
|
||||
{/if}
|
||||
{:else if node.nodeName === 'HR'}
|
||||
<hr />
|
||||
{:else if node.nodeName === 'IMG'}
|
||||
<div class="imgContainer max-h-60 max-w-60">{@html node.outerHTML}</div>
|
||||
{:else if node.nodeName === 'H1'}
|
||||
<h1><svelte:self nodes={node.childNodes} {preview} /></h1>
|
||||
{:else if node.nodeName === 'H2'}
|
||||
<h2><svelte:self nodes={node.childNodes} {preview} /></h2>
|
||||
{:else if node.nodeName === 'H3'}
|
||||
<h3><svelte:self nodes={node.childNodes} {preview} /></h3>
|
||||
{:else if node.nodeName === 'H4'}
|
||||
<h4><svelte:self nodes={node.childNodes} {preview} /></h4>
|
||||
{:else if node.nodeName === 'H5'}
|
||||
<h5><svelte:self nodes={node.childNodes} {preview} /></h5>
|
||||
{:else if node.nodeName === 'H6'}
|
||||
<h6><svelte:self nodes={node.childNodes} {preview} /></h6>
|
||||
{:else if node.nodeName === 'UL' || node.nodeName === 'LIST'}
|
||||
<ul style:margin={preview ? '0' : null}><svelte:self nodes={node.childNodes} {preview} /></ul>
|
||||
{:else if node.nodeName === 'OL' || node.nodeName === 'LIST=1'}
|
||||
<ol style:margin={preview ? '0' : null}><svelte:self nodes={node.childNodes} {preview} /></ol>
|
||||
{:else if node.nodeName === 'LI'}
|
||||
<li class={node.className}><svelte:self nodes={node.childNodes} {preview} /></li>
|
||||
{:else if node.nodeName === 'DIV'}
|
||||
<div><svelte:self nodes={node.childNodes} {preview} /></div>
|
||||
{:else if node.nodeName === 'A'}
|
||||
<a
|
||||
href={node.getAttribute('href')}
|
||||
target={node.getAttribute('target')}
|
||||
on:click={(e) => {
|
||||
handleLink(node, e)
|
||||
}}
|
||||
>
|
||||
<svelte:self nodes={node.childNodes} {preview} />
|
||||
</a>
|
||||
{:else if node.nodeName === 'LABEL'}
|
||||
<svelte:self nodes={node.childNodes} {preview} />
|
||||
{:else if node.nodeName === 'INPUT'}
|
||||
{#if node.type?.toLowerCase() === 'checkbox'}
|
||||
<div class="checkboxContainer">
|
||||
<CheckBox readonly checked={node.checked} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else if node.nodeName === 'SPAN'}
|
||||
{@const objectId = node.getAttribute('data-id')}
|
||||
{@const objectClass = node.getAttribute('data-objectclass')}
|
||||
|
||||
{#if objectClass !== undefined && objectId !== undefined}
|
||||
<ObjectNode _id={objectId} _class={correctClass(objectClass)} title={node.getAttribute('data-label')} />
|
||||
{:else}
|
||||
<svelte:self nodes={node.childNodes} {preview} />
|
||||
{/if}
|
||||
{:else if node.nodeName === 'TABLE'}
|
||||
<table class={node.className}><svelte:self nodes={node.childNodes} {preview} /></table>
|
||||
{:else if node.nodeName === 'TBODY'}
|
||||
<tbody><svelte:self nodes={node.childNodes} {preview} /></tbody>
|
||||
{:else if node.nodeName === 'TR'}
|
||||
<tr><svelte:self nodes={node.childNodes} {preview} /></tr>
|
||||
{:else if node.nodeName === 'TH'}
|
||||
<th><svelte:self nodes={node.childNodes} {preview} /></th>
|
||||
{:else if node.nodeName === 'TD'}
|
||||
<td><svelte:self nodes={node.childNodes} {preview} /></td>
|
||||
{:else if node.nodeName === 'S'}
|
||||
<s><svelte:self nodes={node.childNodes} {preview} /></s>
|
||||
{:else}
|
||||
unknown: "{node.nodeName}"
|
||||
<svelte:self nodes={node.childNodes} {preview} />
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.imgContainer {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.img {
|
||||
:global(img) {
|
||||
object-fit: contain;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.checkboxContainer {
|
||||
padding-top: 0.125rem;
|
||||
}
|
||||
|
||||
em,
|
||||
strong,
|
||||
blockquote,
|
||||
pre,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
ol,
|
||||
li,
|
||||
.checkboxContainer,
|
||||
s {
|
||||
color: var(--global-primary-TextColor);
|
||||
}
|
||||
</style>
|
@ -23,6 +23,7 @@ export { default as InlineAttributeBarEditor } from './components/InlineAttribut
|
||||
export { default as InlineAttributeBar } from './components/InlineAttributeBar.svelte'
|
||||
export { default as AttributesBar } from './components/AttributesBar.svelte'
|
||||
export { default as Card } from './components/Card.svelte'
|
||||
export { default as HTMLViewer } from './components/HTMLViewer.svelte'
|
||||
export { default as MessageBox } from './components/MessageBox.svelte'
|
||||
export { default as MessageViewer } from './components/MessageViewer.svelte'
|
||||
export { default as ObjectPopup } from './components/ObjectPopup.svelte'
|
||||
|
@ -47,31 +47,31 @@
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"@hcengineering/collaborator-client": "^0.6.0",
|
||||
"svelte": "^4.2.12",
|
||||
"@tiptap/core": "^2.1.12",
|
||||
"@tiptap/pm": "^2.1.12",
|
||||
"@tiptap/starter-kit": "^2.1.12",
|
||||
"@tiptap/suggestion": "^2.1.12",
|
||||
"@tiptap/extension-highlight": "^2.1.12",
|
||||
"@tiptap/extension-placeholder": "^2.1.12",
|
||||
"@tiptap/extension-mention": "^2.1.12",
|
||||
"@tiptap/extension-typography": "^2.1.12",
|
||||
"@tiptap/extension-link": "^2.1.12",
|
||||
"@tiptap/extension-task-list": "^2.1.12",
|
||||
"@tiptap/extension-task-item": "^2.1.12",
|
||||
"@tiptap/extension-collaboration": "^2.1.12",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.1.12",
|
||||
"@tiptap/extension-code-block": "^2.1.12",
|
||||
"@tiptap/extension-gapcursor": "^2.1.12",
|
||||
"@tiptap/extension-heading": "^2.1.12",
|
||||
"@tiptap/extension-history": "^2.1.12",
|
||||
"@tiptap/extension-table": "^2.1.12",
|
||||
"@tiptap/extension-table-cell": "^2.1.12",
|
||||
"@tiptap/extension-table-header": "^2.1.12",
|
||||
"@tiptap/extension-table-row": "^2.1.12",
|
||||
"@tiptap/extension-code": "^2.1.12",
|
||||
"@tiptap/extension-bubble-menu": "^2.1.12",
|
||||
"@tiptap/extension-underline": "^2.1.12",
|
||||
"@tiptap/extension-list-keymap": "^2.1.12",
|
||||
"@tiptap/core": "^2.2.4",
|
||||
"@tiptap/pm": "^2.2.4",
|
||||
"@tiptap/starter-kit": "^2.2.4",
|
||||
"@tiptap/suggestion": "^2.2.4",
|
||||
"@tiptap/extension-highlight": "^2.2.4",
|
||||
"@tiptap/extension-placeholder": "^2.2.4",
|
||||
"@tiptap/extension-mention": "^2.2.4",
|
||||
"@tiptap/extension-typography": "^2.2.4",
|
||||
"@tiptap/extension-link": "^2.2.4",
|
||||
"@tiptap/extension-task-list": "^2.2.4",
|
||||
"@tiptap/extension-task-item": "^2.2.4",
|
||||
"@tiptap/extension-collaboration": "^2.2.4",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.2.4",
|
||||
"@tiptap/extension-code-block": "^2.2.4",
|
||||
"@tiptap/extension-gapcursor": "^2.2.4",
|
||||
"@tiptap/extension-heading": "^2.2.4",
|
||||
"@tiptap/extension-history": "^2.2.4",
|
||||
"@tiptap/extension-table": "^2.2.4",
|
||||
"@tiptap/extension-table-cell": "^2.2.4",
|
||||
"@tiptap/extension-table-header": "^2.2.4",
|
||||
"@tiptap/extension-table-row": "^2.2.4",
|
||||
"@tiptap/extension-code": "^2.2.4",
|
||||
"@tiptap/extension-bubble-menu": "^2.2.4",
|
||||
"@tiptap/extension-underline": "^2.2.4",
|
||||
"@tiptap/extension-list-keymap": "^2.2.4",
|
||||
"@hocuspocus/provider": "^2.9.0",
|
||||
"prosemirror-codemark": "^0.4.2",
|
||||
"yjs": "^13.5.52",
|
||||
@ -80,6 +80,7 @@
|
||||
"rfc6902": "^5.0.1",
|
||||
"diff": "^5.1.0",
|
||||
"slugify": "^1.6.6",
|
||||
"lib0": "^0.2.88"
|
||||
"lib0": "^0.2.88",
|
||||
"fast-equals": "^2.0.3"
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
//
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { MarkupNode } from '@hcengineering/text'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { Doc as Ydoc } from 'yjs'
|
||||
|
||||
@ -38,7 +39,7 @@
|
||||
let editor: Editor
|
||||
|
||||
let _decoration = DecorationSet.empty
|
||||
let oldContent = ''
|
||||
let oldContent: MarkupNode | undefined
|
||||
|
||||
function updateEditor (editor: Editor, ydoc: Ydoc, field?: string): void {
|
||||
const r = calculateDecorations(editor, oldContent, createYdocDocument(editor.schema, ydoc, field))
|
||||
|
@ -19,6 +19,7 @@
|
||||
import { type DocumentId, type PlatformDocumentId } from '@hcengineering/collaborator-client'
|
||||
import { IntlString, getMetadata, translate } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { markupToJSON } from '@hcengineering/text'
|
||||
import { Button, IconSize, Loading, themeStore } from '@hcengineering/ui'
|
||||
import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||
import Collaboration, { isChangeOrigin } from '@tiptap/extension-collaboration'
|
||||
@ -155,8 +156,11 @@
|
||||
insertText: (text) => {
|
||||
editor?.commands.insertContent(text)
|
||||
},
|
||||
insertTemplate: (name, text) => {
|
||||
editor?.commands.insertContent(text)
|
||||
insertMarkup: (markup) => {
|
||||
editor?.commands.insertContent(markupToJSON(markup))
|
||||
},
|
||||
insertTemplate: (name, markup) => {
|
||||
editor?.commands.insertContent(markupToJSON(markup))
|
||||
},
|
||||
focus: () => {
|
||||
focus()
|
||||
|
@ -15,6 +15,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { IntlString, Asset } from '@hcengineering/platform'
|
||||
import { Label, Icon } from '@hcengineering/ui'
|
||||
import type { AnySvelteComponent } from '@hcengineering/ui'
|
||||
@ -24,7 +25,7 @@
|
||||
|
||||
export let label: IntlString = textEditorPlugin.string.FullDescription
|
||||
export let icon: Asset | AnySvelteComponent = IconDescription
|
||||
export let content: string = ''
|
||||
export let content: Markup = ''
|
||||
export let maxHeight: string = '40vh'
|
||||
export let enableBackReferences = false
|
||||
|
||||
|
@ -19,30 +19,31 @@
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { DecorationSet } from '@tiptap/pm/view'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import { MarkupNode, ReferenceNode, jsonToPmNode } from '@hcengineering/text'
|
||||
|
||||
import { calculateDecorations, createMarkupDocument } from './diff/decorations'
|
||||
import { calculateDecorations } from './diff/decorations'
|
||||
import { defaultEditorAttributes } from './editor/editorProps'
|
||||
import { ImageExtension } from './extension/imageExt'
|
||||
import { EditorKit } from '../kits/editor-kit'
|
||||
|
||||
export let content: Markup
|
||||
export let comparedVersion: Markup | undefined = undefined
|
||||
export let content: MarkupNode
|
||||
export let comparedVersion: MarkupNode | undefined = undefined
|
||||
|
||||
let element: HTMLElement
|
||||
let editor: Editor
|
||||
|
||||
let _decoration = DecorationSet.empty
|
||||
let oldContent = ''
|
||||
let oldContent: MarkupNode | undefined
|
||||
|
||||
function updateEditor (editor: Editor, comparedVersion?: Markup): void {
|
||||
function updateEditor (editor: Editor, comparedVersion?: MarkupNode): void {
|
||||
if (comparedVersion === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const r = calculateDecorations(editor, oldContent, createMarkupDocument(editor.schema, comparedVersion))
|
||||
const node = jsonToPmNode(comparedVersion, editor.schema)
|
||||
const r = calculateDecorations(editor, oldContent, node)
|
||||
if (r !== undefined) {
|
||||
oldContent = r.oldContent
|
||||
_decoration = r.decorations
|
||||
@ -83,6 +84,7 @@
|
||||
editable: false,
|
||||
extensions: [
|
||||
EditorKit,
|
||||
ReferenceNode,
|
||||
ImageExtension.configure({
|
||||
uploadUrl: getMetadata(presentation.metadata.UploadURL)
|
||||
}),
|
||||
|
@ -13,6 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
@ -36,7 +37,7 @@
|
||||
import { IsEmptyContentExtension } from './extension/isEmptyContent'
|
||||
import Send from './icons/Send.svelte'
|
||||
|
||||
export let content: string = ''
|
||||
export let content: Markup = ''
|
||||
export let showHeader = false
|
||||
export let showActions = true
|
||||
export let showSend = true
|
||||
@ -62,7 +63,7 @@
|
||||
$: devSize = $deviceInfo.size
|
||||
$: shrinkButtons = checkAdaptiveMatching(devSize, 'sm')
|
||||
|
||||
function setContent (content: string): void {
|
||||
function setContent (content: Markup): void {
|
||||
textEditor?.setContent(content)
|
||||
}
|
||||
|
||||
@ -70,8 +71,11 @@
|
||||
insertText: (text) => {
|
||||
textEditor?.insertText(text)
|
||||
},
|
||||
insertTemplate: (name, text) => {
|
||||
textEditor?.insertText(text)
|
||||
insertMarkup: (markup) => {
|
||||
textEditor?.insertMarkup(markup)
|
||||
},
|
||||
insertTemplate: (name, markup) => {
|
||||
textEditor?.insertMarkup(markup)
|
||||
},
|
||||
focus: () => {
|
||||
textEditor?.focus()
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { AnyExtension } from '@tiptap/core'
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { ButtonSize, Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@ -21,8 +22,8 @@
|
||||
export let required = false
|
||||
export let enableBackReferences = false
|
||||
|
||||
let rawValue: string
|
||||
let oldContent = ''
|
||||
let rawValue: Markup
|
||||
let oldContent: Markup = ''
|
||||
|
||||
$: if (content !== undefined && oldContent !== content) {
|
||||
oldContent = content
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { IntlString, getMetadata } from '@hcengineering/platform'
|
||||
import presentation, { MessageViewer } from '@hcengineering/presentation'
|
||||
import {
|
||||
@ -27,7 +28,7 @@
|
||||
import { RefAction } from '../types'
|
||||
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let content: string
|
||||
export let content: Markup
|
||||
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
||||
|
||||
export let kind: 'normal' | 'emphasized' | 'indented' = 'normal'
|
||||
@ -74,8 +75,8 @@
|
||||
|
||||
let canBlur = true
|
||||
let focused = false
|
||||
let rawValue: string
|
||||
let oldContent = ''
|
||||
let rawValue: Markup
|
||||
let oldContent: Markup = ''
|
||||
let modified: boolean = false
|
||||
|
||||
let textEditor: StyledTextEditor
|
||||
|
@ -21,6 +21,7 @@
|
||||
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()
|
||||
|
||||
@ -63,10 +64,10 @@
|
||||
export function setEditable (editable: boolean): void {
|
||||
textEditor?.setEditable(editable)
|
||||
}
|
||||
export function getContent (): string {
|
||||
export function getContent (): Markup {
|
||||
return content
|
||||
}
|
||||
export function setContent (data: string): void {
|
||||
export function setContent (data: Markup): void {
|
||||
textEditor?.setContent(data)
|
||||
}
|
||||
export function insertText (text: string): void {
|
||||
@ -86,8 +87,11 @@
|
||||
insertText: (text) => {
|
||||
textEditor?.insertText(text)
|
||||
},
|
||||
insertTemplate: (name, text) => {
|
||||
textEditor?.insertText(text)
|
||||
insertMarkup: (markup) => {
|
||||
textEditor?.insertMarkup(markup)
|
||||
},
|
||||
insertTemplate: (name, markup) => {
|
||||
textEditor?.insertMarkup(markup)
|
||||
dispatch('template', name)
|
||||
},
|
||||
focus: () => {
|
||||
|
@ -14,7 +14,9 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { EmptyMarkup, getMarkup, markupToJSON } from '@hcengineering/text'
|
||||
import { themeStore } from '@hcengineering/ui'
|
||||
|
||||
import { AnyExtension, Editor, FocusPosition, mergeAttributes } from '@tiptap/core'
|
||||
@ -33,7 +35,7 @@
|
||||
import { SubmitExtension } from './extension/submit'
|
||||
import { EditorKit } from '../kits/editor-kit'
|
||||
|
||||
export let content: string = ''
|
||||
export let content: Markup = ''
|
||||
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
||||
export let extensions: AnyExtension[] = []
|
||||
export let textFormatCategories: TextFormatCategory[] = []
|
||||
@ -62,24 +64,32 @@
|
||||
}
|
||||
}
|
||||
export function submit (): void {
|
||||
content = editor.getHTML()
|
||||
content = getContent()
|
||||
dispatch('content', content)
|
||||
}
|
||||
export function setContent (newContent: string): void {
|
||||
export function getContent (): Markup {
|
||||
return getMarkup(editor)
|
||||
}
|
||||
export function setContent (newContent: Markup): void {
|
||||
if (content !== newContent) {
|
||||
content = newContent
|
||||
editor.commands.setContent(content)
|
||||
editor.commands.setContent(markupToJSON(content))
|
||||
}
|
||||
}
|
||||
export function clear (): void {
|
||||
content = ''
|
||||
content = EmptyMarkup
|
||||
|
||||
editor.commands.clearContent(true)
|
||||
}
|
||||
|
||||
export function insertText (text: string): void {
|
||||
editor.commands.insertContent(text)
|
||||
}
|
||||
|
||||
export function insertMarkup (markup: Markup): void {
|
||||
editor.commands.insertContent(markupToJSON(markup))
|
||||
}
|
||||
|
||||
let needFocus = false
|
||||
let focused = false
|
||||
let posFocus: FocusPosition | undefined = undefined
|
||||
@ -123,7 +133,7 @@
|
||||
editor = new Editor({
|
||||
element,
|
||||
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes) },
|
||||
content,
|
||||
content: markupToJSON(content),
|
||||
autofocus,
|
||||
extensions: [
|
||||
EditorKit,
|
||||
@ -169,7 +179,7 @@
|
||||
dispatch('focus')
|
||||
},
|
||||
onUpdate: () => {
|
||||
content = editor.getHTML()
|
||||
content = getContent()
|
||||
dispatch('value', content)
|
||||
dispatch('update', content)
|
||||
}
|
||||
|
@ -13,19 +13,20 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Markup } from '@hcengineering/core'
|
||||
import { type MarkupNode } from '@hcengineering/text'
|
||||
import { type Editor } from '@tiptap/core'
|
||||
import { ChangeSet } from '@tiptap/pm/changeset'
|
||||
import { DOMParser, type Node, type Schema } from '@tiptap/pm/model'
|
||||
import { type Node as ProseMirrorNode, type Schema } from '@tiptap/pm/model'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { yDocToProsemirrorJSON } from 'y-prosemirror'
|
||||
import { Doc as Ydoc, applyUpdate } from 'yjs'
|
||||
import { type Doc as Ydoc } from 'yjs'
|
||||
import { recreateTransform } from './recreate'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function createYdocDocument (schema: Schema, ydoc: Ydoc, field?: string): Node {
|
||||
export function createYdocDocument (schema: Schema, ydoc: Ydoc, field?: string): ProseMirrorNode {
|
||||
try {
|
||||
const body = yDocToProsemirrorJSON(ydoc, field)
|
||||
return schema.nodeFromJSON(body)
|
||||
@ -35,42 +36,17 @@ export function createYdocDocument (schema: Schema, ydoc: Ydoc, field?: string):
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function createMarkupDocument (schema: Schema, content: Markup | ArrayBuffer, field?: string): Node {
|
||||
if (typeof content === 'string') {
|
||||
const wrappedValue = `<body>${content}</body>`
|
||||
|
||||
const body = new window.DOMParser().parseFromString(wrappedValue, 'text/html').body
|
||||
|
||||
return DOMParser.fromSchema(schema).parse(body)
|
||||
} else {
|
||||
try {
|
||||
const ydoc = new Ydoc()
|
||||
const uint8arr = new Uint8Array(content)
|
||||
applyUpdate(ydoc, uint8arr)
|
||||
|
||||
const body = yDocToProsemirrorJSON(ydoc, field)
|
||||
return schema.nodeFromJSON(body)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
return schema.node(schema.topNodeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function calculateDecorations (
|
||||
editor?: Editor,
|
||||
oldContent?: string,
|
||||
comparedDoc?: Node
|
||||
oldContent?: MarkupNode,
|
||||
comparedDoc?: ProseMirrorNode
|
||||
):
|
||||
| {
|
||||
decorations: DecorationSet
|
||||
oldContent: string
|
||||
oldContent: MarkupNode
|
||||
}
|
||||
| undefined {
|
||||
try {
|
||||
@ -82,8 +58,8 @@ export function calculateDecorations (
|
||||
}
|
||||
const docNew = editor.state.doc
|
||||
|
||||
const c = editor.getHTML()
|
||||
if (c === oldContent) {
|
||||
const c = editor.getJSON() as MarkupNode
|
||||
if (deepEqual(c, oldContent)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,15 @@ export { default } from './plugin'
|
||||
export * from './types'
|
||||
export * from './utils'
|
||||
|
||||
export {
|
||||
EmptyMarkup,
|
||||
areEqualMarkups,
|
||||
getMarkup,
|
||||
isEmptyMarkup,
|
||||
pmNodeToMarkup,
|
||||
markupToPmNode
|
||||
} from '@hcengineering/text'
|
||||
|
||||
export { FocusExtension, type FocusOptions, type FocusStorage } from './components/extension/focus'
|
||||
export { HeadingsExtension, type HeadingsOptions, type HeadingsStorage } from './components/extension/headings'
|
||||
export {
|
||||
|
@ -20,6 +20,7 @@ import type { Level } from '@tiptap/extension-heading'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import Typography from '@tiptap/extension-typography'
|
||||
import Underline from '@tiptap/extension-underline'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
|
||||
export interface DefaultKitOptions {
|
||||
@ -41,11 +42,17 @@ export const DefaultKit = Extension.create<DefaultKitOptions>({
|
||||
addExtensions () {
|
||||
return [
|
||||
StarterKit.configure({
|
||||
blockquote: {
|
||||
HTMLAttributes: {
|
||||
class: 'proseBlockQuote'
|
||||
}
|
||||
},
|
||||
code: codeOptions,
|
||||
codeBlock: this.options.codeBlock ?? codeBlockOptions,
|
||||
heading: this.options.heading,
|
||||
history: this.options.history
|
||||
}),
|
||||
Underline,
|
||||
Highlight.configure({
|
||||
multicolor: false
|
||||
}),
|
||||
|
@ -19,7 +19,6 @@ import ListKeymap from '@tiptap/extension-list-keymap'
|
||||
import TableHeader from '@tiptap/extension-table-header'
|
||||
import TaskItem from '@tiptap/extension-task-item'
|
||||
import TaskList from '@tiptap/extension-task-list'
|
||||
import Underline from '@tiptap/extension-underline'
|
||||
|
||||
import { DefaultKit, type DefaultKitOptions } from './default-kit'
|
||||
|
||||
@ -70,7 +69,6 @@ export const EditorKit = Extension.create<EditorKitOptions>({
|
||||
}),
|
||||
CodeBlockExtension.configure(codeBlockOptions),
|
||||
CodemarkExtension,
|
||||
Underline,
|
||||
ListKeymap.configure({
|
||||
listTypes: [
|
||||
{
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
|
||||
import { type Account, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { type Account, type Doc, type Markup, type Ref } from '@hcengineering/core'
|
||||
import type { AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { type Editor, type SingleCommands } from '@tiptap/core'
|
||||
|
||||
@ -8,7 +8,8 @@ import { type Editor, type SingleCommands } from '@tiptap/core'
|
||||
*/
|
||||
export interface TextEditorHandler {
|
||||
insertText: (html: string) => void
|
||||
insertTemplate: (name: string, html: string) => void
|
||||
insertMarkup: (markup: Markup) => void
|
||||
insertTemplate: (name: string, markup: string) => void
|
||||
focus: () => void
|
||||
}
|
||||
/**
|
||||
|
@ -28,31 +28,34 @@
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@tiptap/core": "^2.1.12",
|
||||
"@tiptap/html": "^2.1.12",
|
||||
"@tiptap/pm": "^2.1.12",
|
||||
"@tiptap/starter-kit": "^2.1.12",
|
||||
"@tiptap/extension-gapcursor": "^2.1.12",
|
||||
"@tiptap/extension-heading": "^2.1.12",
|
||||
"@tiptap/extension-highlight": "^2.1.12",
|
||||
"@tiptap/extension-history": "^2.1.12",
|
||||
"@tiptap/extension-link": "^2.1.12",
|
||||
"@tiptap/extension-mention": "^2.1.12",
|
||||
"@tiptap/extension-table": "^2.1.12",
|
||||
"@tiptap/extension-table-cell": "^2.1.12",
|
||||
"@tiptap/extension-table-header": "^2.1.12",
|
||||
"@tiptap/extension-table-row": "^2.1.12",
|
||||
"@tiptap/extension-task-item": "^2.1.12",
|
||||
"@tiptap/extension-task-list": "^2.1.12",
|
||||
"@tiptap/extension-typography": "^2.1.12",
|
||||
"@tiptap/extension-code-block": "^2.1.12",
|
||||
"@tiptap/suggestion": "^2.1.12",
|
||||
"prosemirror-model": "^1.19.2",
|
||||
"@tiptap/core": "^2.2.4",
|
||||
"@tiptap/html": "^2.2.4",
|
||||
"@tiptap/pm": "^2.2.4",
|
||||
"@tiptap/starter-kit": "^2.2.4",
|
||||
"@tiptap/extension-gapcursor": "^2.2.4",
|
||||
"@tiptap/extension-heading": "^2.2.4",
|
||||
"@tiptap/extension-highlight": "^2.2.4",
|
||||
"@tiptap/extension-history": "^2.2.4",
|
||||
"@tiptap/extension-link": "^2.2.4",
|
||||
"@tiptap/extension-mention": "^2.2.4",
|
||||
"@tiptap/extension-table": "^2.2.4",
|
||||
"@tiptap/extension-table-cell": "^2.2.4",
|
||||
"@tiptap/extension-table-header": "^2.2.4",
|
||||
"@tiptap/extension-table-row": "^2.2.4",
|
||||
"@tiptap/extension-task-item": "^2.2.4",
|
||||
"@tiptap/extension-task-list": "^2.2.4",
|
||||
"@tiptap/extension-typography": "^2.2.4",
|
||||
"@tiptap/extension-code-block": "^2.2.4",
|
||||
"@tiptap/extension-underline": "^2.2.4",
|
||||
"@tiptap/suggestion": "^2.2.4",
|
||||
"prosemirror-model": "^1.19.4",
|
||||
"fast-equals": "^2.0.3",
|
||||
"yjs": "^13.5.52",
|
||||
"y-prosemirror": "^1.2.1"
|
||||
},
|
||||
|
@ -13,11 +13,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import { ServerKit } from './kits/server-kit'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getText (node: ProseMirrorNode): string {
|
||||
return node.textBetween(0, node.content.size, '\n', '')
|
||||
}
|
||||
export const defaultExtensions = [ServerKit]
|
@ -1,86 +0,0 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import { Extensions, getSchema } from '@tiptap/core'
|
||||
import { generateJSON, generateHTML } from '@tiptap/html'
|
||||
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
|
||||
import { ServerKit } from './kits/server-kit'
|
||||
|
||||
const defaultExtensions = [ServerKit]
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getHTML (node: ProseMirrorNode, extensions: Extensions): string {
|
||||
return generateHTML(node.toJSON(), extensions)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function parseHTML (content: string, extensions?: Extensions): ProseMirrorNode {
|
||||
extensions = extensions ?? defaultExtensions
|
||||
|
||||
const schema = getSchema(extensions)
|
||||
const json = generateJSON(content, extensions)
|
||||
|
||||
return ProseMirrorNode.fromJSON(schema, json)
|
||||
}
|
||||
|
||||
const ELLIPSIS_CHAR = '…'
|
||||
const WHITESPACE = ' '
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function stripTags (htmlString: string, textLimit = 0, extensions: Extensions | undefined = undefined): string {
|
||||
const effectiveExtensions = extensions ?? defaultExtensions
|
||||
const parsed = parseHTML(htmlString, effectiveExtensions)
|
||||
|
||||
const textParts: string[] = []
|
||||
let charCount = 0
|
||||
let isHardStop = false
|
||||
|
||||
parsed.descendants((node, _pos, parent): boolean => {
|
||||
if (isHardStop) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (node.type.isText) {
|
||||
const text = node.text ?? ''
|
||||
if (textLimit > 0 && charCount + text.length > textLimit) {
|
||||
const toAddCount = textLimit - charCount
|
||||
const textPart = text.substring(0, toAddCount)
|
||||
textParts.push(textPart)
|
||||
textParts.push(ELLIPSIS_CHAR)
|
||||
isHardStop = true
|
||||
} else {
|
||||
textParts.push(text)
|
||||
charCount += text.length
|
||||
}
|
||||
return false
|
||||
} else if (node.type.isBlock) {
|
||||
if (textParts.length > 0 && textParts[textParts.length - 1] !== WHITESPACE) {
|
||||
textParts.push(WHITESPACE)
|
||||
charCount++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const result = textParts.join('')
|
||||
return result
|
||||
}
|
@ -13,10 +13,13 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
export * from './html'
|
||||
export * from './node'
|
||||
export * from './extensions'
|
||||
export * from './markup/dsl'
|
||||
export * from './markup/model'
|
||||
export * from './markup/traverse'
|
||||
export * from './markup/utils'
|
||||
export * from './nodes'
|
||||
export * from './text'
|
||||
export * from './ydoc'
|
||||
|
||||
export * from './kits/default-kit'
|
||||
export * from './kits/server-kit'
|
||||
|
@ -19,6 +19,7 @@ import { Level } from '@tiptap/extension-heading'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import Typography from '@tiptap/extension-typography'
|
||||
import Underline from '@tiptap/extension-underline'
|
||||
import StarterKit, { StarterKitOptions } from '@tiptap/starter-kit'
|
||||
|
||||
export interface DefaultKitOptions {
|
||||
@ -49,10 +50,16 @@ export const DefaultKit = Extension.create<DefaultKitOptions>({
|
||||
class: 'proseCode'
|
||||
}
|
||||
},
|
||||
blockquote: {
|
||||
HTMLAttributes: {
|
||||
class: 'proseBlockQuote'
|
||||
}
|
||||
},
|
||||
codeBlock,
|
||||
heading: this.options.heading,
|
||||
history: this.options.history
|
||||
}),
|
||||
Underline,
|
||||
Highlight.configure({
|
||||
multicolor: false
|
||||
}),
|
||||
|
98
packages/text/src/markup/__tests__/dsl.test.ts
Normal file
98
packages/text/src/markup/__tests__/dsl.test.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { nodeDoc, nodeImage, nodeParagraph, nodeReference, nodeText, markLink, markUnderline } from '../dsl'
|
||||
import { MarkupNodeType } from '../model'
|
||||
import { jsonToHTML } from '../utils'
|
||||
|
||||
describe('dsl', () => {
|
||||
it('returns a MarkupNode for complex doc', () => {
|
||||
const doc = nodeDoc(
|
||||
nodeParagraph(nodeText('Hello, '), nodeReference({ id: '123', label: 'World', objectclass: 'world' })),
|
||||
nodeParagraph(
|
||||
nodeText('Check out '),
|
||||
markLink({ href: 'https://example.com', title: 'this link' }, markUnderline(nodeText('this link'))),
|
||||
nodeText('.')
|
||||
)
|
||||
)
|
||||
expect(jsonToHTML(doc)).toEqual(
|
||||
'<p>Hello, <span data-type="reference" data-id="123" data-objectclass="world" data-label="World">@World</span></p><p>Check out <a target="_blank" rel="noopener noreferrer" class="cursor-pointer" href="https://example.com"><u>this link</u></a>.</p>'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeDoc', () => {
|
||||
it('returns a MarkupNode with type "doc"', () => {
|
||||
const result = nodeDoc()
|
||||
expect(result.type).toEqual(MarkupNodeType.doc)
|
||||
})
|
||||
|
||||
it('returns a MarkupNode with the provided content', () => {
|
||||
const content = [
|
||||
{ type: MarkupNodeType.paragraph, content: [{ type: MarkupNodeType.text, text: 'Hello' }] },
|
||||
{ type: MarkupNodeType.paragraph, content: [{ type: MarkupNodeType.text, text: 'World' }] }
|
||||
]
|
||||
const result = nodeDoc(...content)
|
||||
expect(result.content).toEqual(content)
|
||||
})
|
||||
|
||||
it('returns an empty MarkupNode if no content is provided', () => {
|
||||
const result = nodeDoc()
|
||||
expect(result.content).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeParagraph', () => {
|
||||
it('returns a MarkupNode with type "paragraph"', () => {
|
||||
const result = nodeParagraph()
|
||||
expect(result.type).toEqual(MarkupNodeType.paragraph)
|
||||
})
|
||||
|
||||
it('returns a MarkupNode with the provided content', () => {
|
||||
const content = [{ type: MarkupNodeType.text, text: 'Hello' }]
|
||||
const result = nodeParagraph(...content)
|
||||
expect(result.content).toEqual(content)
|
||||
})
|
||||
|
||||
it('returns an empty MarkupNode if no content is provided', () => {
|
||||
const result = nodeParagraph()
|
||||
expect(result.content).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeText', () => {
|
||||
it('returns a MarkupNode with type "text"', () => {
|
||||
const result = nodeText('Hello')
|
||||
expect(result.type).toEqual(MarkupNodeType.text)
|
||||
})
|
||||
|
||||
it('returns a MarkupNode with the provided text', () => {
|
||||
const result = nodeText('Hello')
|
||||
expect(result.text).toEqual('Hello')
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeImage', () => {
|
||||
it('returns a MarkupNode with type "image"', () => {
|
||||
const attrs = { src: 'image.jpg' }
|
||||
const result = nodeImage(attrs)
|
||||
expect(result.type).toEqual(MarkupNodeType.image)
|
||||
})
|
||||
|
||||
it('returns a MarkupNode with the provided attributes', () => {
|
||||
const attrs = { src: 'image.jpg', alt: 'Image', width: 500, height: 300 }
|
||||
const result = nodeImage(attrs)
|
||||
expect(result.attrs).toEqual(attrs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeReference', () => {
|
||||
it('returns a MarkupNode with type "reference"', () => {
|
||||
const attrs = { id: '123', label: 'Reference', objectclass: 'class' }
|
||||
const result = nodeReference(attrs)
|
||||
expect(result.type).toEqual(MarkupNodeType.reference)
|
||||
})
|
||||
|
||||
it('returns a MarkupNode with the provided attributes', () => {
|
||||
const attrs = { id: '123', label: 'Reference', objectclass: 'class' }
|
||||
const result = nodeReference(attrs)
|
||||
expect(result.attrs).toEqual(attrs)
|
||||
})
|
||||
})
|
97
packages/text/src/markup/__tests__/traverse.test.ts
Normal file
97
packages/text/src/markup/__tests__/traverse.test.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { MarkupNode, MarkupNodeType } from '../model'
|
||||
import { traverseAllMarks, traverseNode, traverseNodeMarks } from '../traverse'
|
||||
|
||||
describe('traverseNode', () => {
|
||||
it('should call the callback function for each node', () => {
|
||||
const callback = jest.fn()
|
||||
const node = {
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Hello, world!'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
traverseNode(node as MarkupNode, callback)
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(2)
|
||||
expect(callback).toHaveBeenCalledWith(node)
|
||||
expect(callback).toHaveBeenCalledWith(node.content[0])
|
||||
})
|
||||
|
||||
it('should stop traversing if the callback returns false', () => {
|
||||
const callback = jest.fn((node) => {
|
||||
if (node.type === MarkupNodeType.paragraph) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const node = {
|
||||
type: MarkupNodeType.paragraph,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: 'Hello, world!'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
traverseNode(node, callback)
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(1)
|
||||
expect(callback).toHaveBeenCalledWith(node)
|
||||
})
|
||||
})
|
||||
|
||||
describe('traverseNodeMarks', () => {
|
||||
it('should call the callback function for each mark', () => {
|
||||
const callback = jest.fn()
|
||||
const node = {
|
||||
type: 'paragraph',
|
||||
marks: [{ type: 'bold' }, { type: 'italic' }, { type: 'underline' }]
|
||||
}
|
||||
|
||||
traverseNodeMarks(node as MarkupNode, callback)
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(3)
|
||||
expect(callback).toHaveBeenCalledWith(node.marks[0])
|
||||
expect(callback).toHaveBeenCalledWith(node.marks[1])
|
||||
expect(callback).toHaveBeenCalledWith(node.marks[2])
|
||||
})
|
||||
|
||||
it('should not call the callback function if marks are not present', () => {
|
||||
const callback = jest.fn()
|
||||
const node = {
|
||||
type: MarkupNodeType.paragraph
|
||||
}
|
||||
|
||||
traverseNodeMarks(node, callback)
|
||||
|
||||
expect(callback).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('traverseAllMarks', () => {
|
||||
it('should traverse all marks and call the callback function', () => {
|
||||
const callback = jest.fn()
|
||||
const node = {
|
||||
type: 'paragraph',
|
||||
marks: [{ type: 'bold' }],
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: 'Hello, world!',
|
||||
marks: [{ type: 'italic' }, { type: 'underline' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
traverseAllMarks(node as MarkupNode, callback)
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(3)
|
||||
expect(callback).toHaveBeenCalledWith(node, node.marks[0])
|
||||
expect(callback).toHaveBeenCalledWith(node.content[0], node.content[0].marks[0])
|
||||
expect(callback).toHaveBeenCalledWith(node.content[0], node.content[0].marks[1])
|
||||
})
|
||||
})
|
338
packages/text/src/markup/__tests__/utils.test.ts
Normal file
338
packages/text/src/markup/__tests__/utils.test.ts
Normal file
@ -0,0 +1,338 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import { Editor, getSchema } from '@tiptap/core'
|
||||
import { MarkupMarkType, MarkupNode, MarkupNodeType } from '../model'
|
||||
import {
|
||||
EmptyMarkup,
|
||||
areEqualMarkups,
|
||||
getMarkup,
|
||||
htmlToJSON,
|
||||
htmlToMarkup,
|
||||
htmlToPmNode,
|
||||
isEmptyMarkup,
|
||||
jsonToHTML,
|
||||
jsonToMarkup,
|
||||
jsonToText,
|
||||
markupToHTML,
|
||||
markupToJSON,
|
||||
markupToPmNode,
|
||||
pmNodeToHTML,
|
||||
pmNodeToJSON,
|
||||
pmNodeToMarkup
|
||||
} from '../utils'
|
||||
import { ServerKit } from '../../kits/server-kit'
|
||||
import { nodeDoc, nodeParagraph, nodeText } from '../dsl'
|
||||
|
||||
// mock tiptap functions
|
||||
jest.mock('@tiptap/html', () => ({
|
||||
generateHTML: jest.fn(() => '<p>hello</p>'),
|
||||
generateJSON: jest.fn(() => ({
|
||||
type: 'doc',
|
||||
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'hello' }] }]
|
||||
}))
|
||||
}))
|
||||
|
||||
const extensions = [ServerKit]
|
||||
|
||||
describe('EmptyMarkup', () => {
|
||||
it('is empty markup', async () => {
|
||||
const editor = new Editor({ extensions })
|
||||
expect(getMarkup(editor)).toEqual(EmptyMarkup)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMarkup', () => {
|
||||
it('with empty content', async () => {
|
||||
const editor = new Editor({ extensions })
|
||||
expect(getMarkup(editor)).toEqual('{"type":"doc","content":[{"type":"paragraph"}]}')
|
||||
})
|
||||
it('with some content', async () => {
|
||||
const editor = new Editor({ extensions, content: '<p>hello</p>' })
|
||||
expect(getMarkup(editor)).toEqual(
|
||||
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}'
|
||||
)
|
||||
})
|
||||
it('with empty paragraphs as content', async () => {
|
||||
const editor = new Editor({ extensions, content: '<p></p><p></p>' })
|
||||
expect(getMarkup(editor)).toEqual('{"type":"doc","content":[{"type":"paragraph"},{"type":"paragraph"}]}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isEmptyMarkup', () => {
|
||||
it('returns true for undefined content', async () => {
|
||||
expect(isEmptyMarkup(undefined)).toBeTruthy()
|
||||
expect(isEmptyMarkup('')).toBeTruthy()
|
||||
})
|
||||
it('returns true for empty content', async () => {
|
||||
const editor = new Editor({ extensions })
|
||||
expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy()
|
||||
})
|
||||
it('returns true for empty paragraphs content', async () => {
|
||||
const editor = new Editor({ extensions, content: '<p></p><p></p><p></p>' })
|
||||
expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy()
|
||||
})
|
||||
it('returns true for empty paragraphs content with spaces', async () => {
|
||||
const editor = new Editor({ extensions, content: '<p> </p><p> </p><p> </p>' })
|
||||
expect(isEmptyMarkup(getMarkup(editor))).toBeTruthy()
|
||||
})
|
||||
it('returns false for not empty content', async () => {
|
||||
const editor = new Editor({ extensions, content: '<p>hello</p>' })
|
||||
expect(isEmptyMarkup(getMarkup(editor))).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('areEqualMarkups', () => {
|
||||
it('returns true for the same content', async () => {
|
||||
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 () => {
|
||||
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()
|
||||
})
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
describe('pmNodeToMarkup', () => {
|
||||
it('converts ProseMirrorNode to Markup', () => {
|
||||
const schema = getSchema(extensions)
|
||||
const node = schema.node('paragraph', {}, [schema.text('Hello, world!')])
|
||||
|
||||
expect(pmNodeToMarkup(node)).toEqual('{"type":"paragraph","content":[{"type":"text","text":"Hello, world!"}]}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('markupToPmNode', () => {
|
||||
it('converts markup to ProseMirrorNode', () => {
|
||||
const markup = '{"type":"paragraph","content":[{"type":"text","text":"Hello, world!"}]}'
|
||||
const node = markupToPmNode(markup)
|
||||
|
||||
expect(node.type.name).toEqual('paragraph')
|
||||
expect(node.content.childCount).toEqual(1)
|
||||
expect(node.content.child(0).type.name).toEqual('text')
|
||||
expect(node.content.child(0).text).toEqual('Hello, world!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('markupToJSON', () => {
|
||||
it('with empty content', async () => {
|
||||
expect(markupToJSON('')).toEqual({ type: 'doc', content: [{ type: 'paragraph' }] })
|
||||
})
|
||||
it('with some content', async () => {
|
||||
const markup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}'
|
||||
expect(markupToJSON(markup)).toEqual({
|
||||
type: 'doc',
|
||||
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'hello' }] }]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('jsonToMarkup', () => {
|
||||
it('with some content', async () => {
|
||||
const json: MarkupNode = {
|
||||
type: MarkupNodeType.doc,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.paragraph,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: 'hello'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
expect(jsonToMarkup(json)).toEqual(
|
||||
'{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pmNodeToJSON', () => {
|
||||
it('converts ProseMirrorNode to Markup', () => {
|
||||
const schema = getSchema(extensions)
|
||||
const node = schema.node('paragraph', {}, [schema.text('Hello, world!')])
|
||||
|
||||
const json = nodeParagraph(nodeText('Hello, world!'))
|
||||
expect(pmNodeToJSON(node)).toEqual(json)
|
||||
})
|
||||
})
|
||||
|
||||
describe('jsonToPmNode', () => {
|
||||
it('converts json to ProseMirrorNode', () => {
|
||||
const markup = '{"type":"paragraph","content":[{"type":"text","text":"Hello, world!"}]}'
|
||||
const node = markupToPmNode(markup)
|
||||
|
||||
expect(node.type.name).toEqual('paragraph')
|
||||
expect(node.content.childCount).toEqual(1)
|
||||
expect(node.content.child(0).type.name).toEqual('text')
|
||||
expect(node.content.child(0).text).toEqual('Hello, world!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('htmlToMarkup', () => {
|
||||
it('converts HTML to Markup', () => {
|
||||
const html = '<p>hello</p>'
|
||||
const expectedMarkup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}'
|
||||
expect(htmlToMarkup(html)).toEqual(expectedMarkup)
|
||||
})
|
||||
})
|
||||
|
||||
describe('markupToHTML', () => {
|
||||
it('converts markup to HTML', () => {
|
||||
const markup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"hello"}]}]}'
|
||||
const expectedHtml = '<p>hello</p>'
|
||||
expect(markupToHTML(markup)).toEqual(expectedHtml)
|
||||
})
|
||||
})
|
||||
|
||||
describe('htmlToJSON', () => {
|
||||
it('converts HTML to JSON', () => {
|
||||
const html = '<p>hello</p>'
|
||||
const json = nodeDoc(nodeParagraph(nodeText('hello')))
|
||||
expect(htmlToJSON(html)).toEqual(json)
|
||||
})
|
||||
})
|
||||
|
||||
describe('jsonToHTML', () => {
|
||||
it('converts JSON to HTML', () => {
|
||||
const json = nodeDoc(nodeParagraph(nodeText('hello')))
|
||||
const html = '<p>hello</p>'
|
||||
expect(jsonToHTML(json)).toEqual(html)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pmNodeToHTML', () => {
|
||||
it('converts ProseMirrorNode to HTML', () => {
|
||||
const schema = getSchema(extensions)
|
||||
const node = schema.node('paragraph', {}, [schema.text('hello')])
|
||||
|
||||
expect(pmNodeToHTML(node)).toEqual('<p>hello</p>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('htmlToPmNode', () => {
|
||||
it('converts html to ProseMirrorNode', () => {
|
||||
const node = htmlToPmNode('<p>hello</p>')
|
||||
|
||||
expect(node.type.name).toEqual('doc')
|
||||
expect(node.content.childCount).toEqual(1)
|
||||
expect(node.content.child(0).type.name).toEqual('paragraph')
|
||||
expect(node.content.child(0).childCount).toEqual(1)
|
||||
expect(node.content.child(0).child(0).type.name).toEqual('text')
|
||||
expect(node.content.child(0).child(0).text).toEqual('hello')
|
||||
})
|
||||
})
|
||||
|
||||
describe('jsonToText', () => {
|
||||
it('returns text for text node', () => {
|
||||
const node: MarkupNode = {
|
||||
type: MarkupNodeType.paragraph,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: 'Hello, world!'
|
||||
}
|
||||
]
|
||||
}
|
||||
expect(jsonToText(node)).toEqual('Hello, world!')
|
||||
})
|
||||
it('returns concatenated text for block node with multiple children', () => {
|
||||
const node: MarkupNode = {
|
||||
type: MarkupNodeType.paragraph,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: 'Hello '
|
||||
},
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: 'world!'
|
||||
}
|
||||
]
|
||||
}
|
||||
expect(jsonToText(node)).toEqual('Hello world!')
|
||||
})
|
||||
it('returns text for node with link', () => {
|
||||
const node: MarkupNode = {
|
||||
type: MarkupNodeType.paragraph,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: 'Hello! Check out '
|
||||
},
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: 'this page',
|
||||
marks: [
|
||||
{
|
||||
type: MarkupMarkType.link,
|
||||
attrs: {
|
||||
href: 'http://example.com/'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: '!'
|
||||
}
|
||||
]
|
||||
}
|
||||
expect(jsonToText(node)).toEqual('Hello! Check out this page!')
|
||||
})
|
||||
it('returns empty string for block node with no children', () => {
|
||||
const node: MarkupNode = {
|
||||
type: MarkupNodeType.paragraph,
|
||||
content: []
|
||||
}
|
||||
expect(jsonToText(node)).toEqual('')
|
||||
})
|
||||
it('returns error for text node with no text', () => {
|
||||
const node: MarkupNode = {
|
||||
type: MarkupNodeType.text,
|
||||
text: ''
|
||||
}
|
||||
expect(() => jsonToText(node)).toThrow('Empty text nodes are not allowed')
|
||||
})
|
||||
it('returns error for block node with empty children', () => {
|
||||
const node: MarkupNode = {
|
||||
type: MarkupNodeType.paragraph,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: ''
|
||||
},
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
expect(() => jsonToText(node)).toThrow('Empty text nodes are not allowed')
|
||||
})
|
||||
})
|
81
packages/text/src/markup/dsl.ts
Normal file
81
packages/text/src/markup/dsl.ts
Normal file
@ -0,0 +1,81 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import { MarkupMark, MarkupMarkType, MarkupNode, MarkupNodeType } from './model'
|
||||
|
||||
// Nodes
|
||||
|
||||
export function nodeDoc (...content: MarkupNode[]): MarkupNode {
|
||||
return node(MarkupNodeType.doc, ...content)
|
||||
}
|
||||
|
||||
export function nodeParagraph (...content: MarkupNode[]): MarkupNode {
|
||||
return node(MarkupNodeType.paragraph, ...content)
|
||||
}
|
||||
|
||||
export function nodeText (text: string): MarkupNode {
|
||||
return { type: MarkupNodeType.text, text }
|
||||
}
|
||||
|
||||
export function nodeImage (attrs: { src: string, alt?: string, width?: number, height?: number }): MarkupNode {
|
||||
return { type: MarkupNodeType.image, attrs }
|
||||
}
|
||||
|
||||
export function nodeReference (attrs: { id: string, label: string, objectclass: string }): MarkupNode {
|
||||
return { type: MarkupNodeType.reference, attrs }
|
||||
}
|
||||
|
||||
// Marks
|
||||
|
||||
export function markBold (node: MarkupNode): MarkupNode {
|
||||
return withMarks(node, mark(MarkupMarkType.bold))
|
||||
}
|
||||
|
||||
export function markCode (node: MarkupNode): MarkupNode {
|
||||
return withMarks(node, mark(MarkupMarkType.code))
|
||||
}
|
||||
|
||||
export function markItalic (node: MarkupNode): MarkupNode {
|
||||
return withMarks(node, mark(MarkupMarkType.em))
|
||||
}
|
||||
|
||||
export function markStrike (node: MarkupNode): MarkupNode {
|
||||
return withMarks(node, mark(MarkupMarkType.strike))
|
||||
}
|
||||
|
||||
export function markUnderline (node: MarkupNode): MarkupNode {
|
||||
return withMarks(node, mark(MarkupMarkType.underline))
|
||||
}
|
||||
|
||||
export function markLink (attrs: { href: string, title: string }, node: MarkupNode): MarkupNode {
|
||||
return withMarks(node, mark(MarkupMarkType.link, attrs))
|
||||
}
|
||||
|
||||
// Utility
|
||||
|
||||
function node (type: MarkupNodeType, ...content: MarkupNode[]): MarkupNode {
|
||||
return { type, content }
|
||||
}
|
||||
|
||||
function mark (type: MarkupMarkType, attrs?: Record<string, any>): MarkupMark {
|
||||
return { type, attrs: attrs ?? {} }
|
||||
}
|
||||
|
||||
function withMarks (node: MarkupNode, ...marks: MarkupMark[]): MarkupNode {
|
||||
const current = node.marks ?? []
|
||||
current.push(...marks)
|
||||
|
||||
return { ...node, marks: current }
|
||||
}
|
82
packages/text/src/markup/model.ts
Normal file
82
packages/text/src/markup/model.ts
Normal file
@ -0,0 +1,82 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
/** @public */
|
||||
export enum MarkupNodeType {
|
||||
doc = 'doc',
|
||||
paragraph = 'paragraph',
|
||||
blockquote = 'blockquote',
|
||||
horizontal_rule = 'horizontalRule',
|
||||
heading = 'heading',
|
||||
code_block = 'codeBlock',
|
||||
text = 'text',
|
||||
image = 'image',
|
||||
reference = 'reference',
|
||||
hard_break = 'hardBreak',
|
||||
ordered_list = 'orderedList',
|
||||
bullet_list = 'bulletList',
|
||||
list_item = 'listItem',
|
||||
taskList = 'taskList',
|
||||
taskItem = 'taskItem',
|
||||
sub = 'sub',
|
||||
table = 'table',
|
||||
table_row = 'tableRow',
|
||||
table_cell = 'tableCell',
|
||||
table_header = 'tableHeader'
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export enum MarkupMarkType {
|
||||
link = 'link',
|
||||
em = 'italic',
|
||||
bold = 'bold',
|
||||
code = 'code',
|
||||
strike = 'strike',
|
||||
underline = 'underline'
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface MarkupMark {
|
||||
type: MarkupMarkType
|
||||
attrs: Record<string, any> // A map of attributes
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface MarkupNode {
|
||||
type: MarkupNodeType
|
||||
content?: MarkupNode[] // A list of child nodes
|
||||
marks?: MarkupMark[]
|
||||
attrs?: Record<string, string | number>
|
||||
text?: string
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function emptyMarkupNode (): MarkupNode {
|
||||
return {
|
||||
type: MarkupNodeType.doc,
|
||||
content: [{ type: MarkupNodeType.paragraph }]
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface LinkMark extends MarkupMark {
|
||||
href: string
|
||||
title: string
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ReferenceMark extends MarkupMark {
|
||||
attrs: { id: string, label: string, objectclass: string }
|
||||
}
|
46
packages/text/src/markup/traverse.ts
Normal file
46
packages/text/src/markup/traverse.ts
Normal file
@ -0,0 +1,46 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import { MarkupMark, MarkupNode } from './model'
|
||||
|
||||
export function traverseNode (node: MarkupNode, f: (el: MarkupNode) => boolean | undefined): void {
|
||||
const result = f(node)
|
||||
if (result !== false) {
|
||||
node.content?.forEach((p) => {
|
||||
traverseNode(p, f)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function traverseNodeMarks (node: MarkupNode, f: (el: MarkupMark) => void): void {
|
||||
node.marks?.forEach((p) => {
|
||||
f(p)
|
||||
})
|
||||
}
|
||||
|
||||
export function traverseNodeContent (node: MarkupNode, f: (el: MarkupNode) => void): void {
|
||||
node.content?.forEach((p) => {
|
||||
f(p)
|
||||
})
|
||||
}
|
||||
|
||||
export function traverseAllMarks (node: MarkupNode, f: (el: MarkupNode, mark: MarkupMark) => void): void {
|
||||
traverseNode(node, (node) => {
|
||||
traverseNodeMarks(node, (mark) => {
|
||||
f(node, mark)
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
199
packages/text/src/markup/utils.ts
Normal file
199
packages/text/src/markup/utils.ts
Normal file
@ -0,0 +1,199 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { Editor, Extensions, getSchema } from '@tiptap/core'
|
||||
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 { nodeDoc, nodeParagraph, nodeText } from './dsl'
|
||||
|
||||
/** @public */
|
||||
export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode())
|
||||
|
||||
/** @public */
|
||||
export function getMarkup (editor: Editor): Markup {
|
||||
return jsonToMarkup(editor.getJSON() as MarkupNode)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function isEmptyMarkup (markup: Markup | undefined): boolean {
|
||||
if (markup === undefined || markup === null || markup === '') {
|
||||
return true
|
||||
}
|
||||
const node = markupToPmNode(markup)
|
||||
return node.textContent.trim() === ''
|
||||
}
|
||||
|
||||
/** @public */
|
||||
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()
|
||||
}
|
||||
|
||||
// Markup
|
||||
|
||||
/** @public */
|
||||
export function pmNodeToMarkup (node: ProseMirrorNode): Markup {
|
||||
return jsonToMarkup(pmNodeToJSON(node))
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function markupToPmNode (markup: Markup, schema?: Schema, extensions?: Extensions): ProseMirrorNode {
|
||||
const json = markupToJSON(markup)
|
||||
return jsonToPmNode(json, schema, extensions)
|
||||
}
|
||||
|
||||
// JSON
|
||||
|
||||
/** @public */
|
||||
export function jsonToMarkup (json: MarkupNode): Markup {
|
||||
return JSON.stringify(json)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function markupToJSON (markup: Markup): MarkupNode {
|
||||
if (markup == null || markup === '') {
|
||||
return emptyMarkupNode()
|
||||
}
|
||||
|
||||
try {
|
||||
// Ideally Markup should contain only serialized JSON
|
||||
// But there seem to be some cases when it contains HTML or plain text
|
||||
// So we need to handle those cases and produce valid MarkupNode
|
||||
if (markup.startsWith('{')) {
|
||||
return JSON.parse(markup) as MarkupNode
|
||||
} else if (markup.startsWith('<')) {
|
||||
return htmlToJSON(markup, defaultExtensions)
|
||||
} else {
|
||||
return nodeDoc(nodeParagraph(nodeText(markup)))
|
||||
}
|
||||
} catch (error) {
|
||||
return emptyMarkupNode()
|
||||
}
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function jsonToPmNode (json: MarkupNode, schema?: Schema, extensions?: Extensions): ProseMirrorNode {
|
||||
schema ??= getSchema(extensions ?? defaultExtensions)
|
||||
return ProseMirrorNode.fromJSON(schema, json)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function pmNodeToJSON (node: ProseMirrorNode): MarkupNode {
|
||||
return node.toJSON()
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function jsonToText (node: MarkupNode, schema?: Schema, extensions?: Extensions): string {
|
||||
const pmNode = jsonToPmNode(node, schema, extensions)
|
||||
return pmNode.textBetween(0, pmNode.content.size, '\n', '')
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function pmNodeToText (node: ProseMirrorNode): string {
|
||||
return jsonToText(node.toJSON())
|
||||
}
|
||||
|
||||
// HTML
|
||||
|
||||
/** @public */
|
||||
export function htmlToMarkup (html: string, extensions?: Extensions): Markup {
|
||||
const json = htmlToJSON(html, extensions)
|
||||
return jsonToMarkup(json)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function markupToHTML (markup: Markup, extensions?: Extensions): string {
|
||||
const json = markupToJSON(markup)
|
||||
return jsonToHTML(json, extensions)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function htmlToJSON (html: string, extensions?: Extensions): MarkupNode {
|
||||
extensions = extensions ?? defaultExtensions
|
||||
return generateJSON(html, extensions) as MarkupNode
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function jsonToHTML (json: MarkupNode, extensions?: Extensions): string {
|
||||
extensions = extensions ?? defaultExtensions
|
||||
return generateHTML(json, extensions)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function htmlToPmNode (html: string, schema?: Schema, extensions?: Extensions): ProseMirrorNode {
|
||||
schema ??= getSchema(extensions ?? defaultExtensions)
|
||||
const json = htmlToJSON(html, extensions)
|
||||
return ProseMirrorNode.fromJSON(schema, json)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function pmNodeToHTML (node: ProseMirrorNode, extensions?: Extensions): string {
|
||||
extensions ??= defaultExtensions
|
||||
return generateHTML(node.toJSON(), extensions)
|
||||
}
|
||||
|
||||
// UTILS
|
||||
|
||||
const ELLIPSIS_CHAR = '…'
|
||||
const WHITESPACE = ' '
|
||||
|
||||
/** @public */
|
||||
export function stripTags (markup: Markup, textLimit = 0, extensions: Extensions | undefined = undefined): string {
|
||||
const schema = getSchema(extensions ?? defaultExtensions)
|
||||
const parsed = markupToPmNode(markup, schema)
|
||||
|
||||
const textParts: string[] = []
|
||||
let charCount = 0
|
||||
let isHardStop = false
|
||||
|
||||
parsed.descendants((node, _pos, parent): boolean => {
|
||||
if (isHardStop) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (node.type.isText) {
|
||||
const text = node.text ?? ''
|
||||
if (textLimit > 0 && charCount + text.length > textLimit) {
|
||||
const toAddCount = textLimit - charCount
|
||||
const textPart = text.substring(0, toAddCount)
|
||||
textParts.push(textPart)
|
||||
textParts.push(ELLIPSIS_CHAR)
|
||||
isHardStop = true
|
||||
} else {
|
||||
textParts.push(text)
|
||||
charCount += text.length
|
||||
}
|
||||
return false
|
||||
} else if (node.type.isBlock) {
|
||||
if (textParts.length > 0 && textParts[textParts.length - 1] !== WHITESPACE) {
|
||||
textParts.push(WHITESPACE)
|
||||
charCount++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const result = textParts.join('')
|
||||
return result
|
||||
}
|
@ -14,21 +14,27 @@
|
||||
//
|
||||
|
||||
import { Extensions, getSchema } from '@tiptap/core'
|
||||
import { Node } from 'prosemirror-model'
|
||||
import { Node, Schema } from 'prosemirror-model'
|
||||
import { yDocToProsemirrorJSON } from 'y-prosemirror'
|
||||
import { Doc, applyUpdate } from 'yjs'
|
||||
import { defaultExtensions } from './extensions'
|
||||
|
||||
/**
|
||||
* Get ProseMirror node from Y.Doc content
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function yDocContentToNode (extensions: Extensions, content: ArrayBuffer, field?: string): Node {
|
||||
export function yDocContentToNode (
|
||||
content: ArrayBuffer,
|
||||
field?: string,
|
||||
schema?: Schema,
|
||||
extensions?: Extensions
|
||||
): Node {
|
||||
const ydoc = new Doc()
|
||||
const uint8arr = new Uint8Array(content)
|
||||
applyUpdate(ydoc, uint8arr)
|
||||
|
||||
return yDocToNode(extensions, ydoc, field)
|
||||
return yDocToNode(ydoc, field, schema, extensions)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -36,8 +42,8 @@ export function yDocContentToNode (extensions: Extensions, content: ArrayBuffer,
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function yDocToNode (extensions: Extensions, ydoc: Doc, field?: string): Node {
|
||||
const schema = getSchema(extensions)
|
||||
export function yDocToNode (ydoc: Doc, field?: string, schema?: Schema, extensions?: Extensions): Node {
|
||||
schema ??= getSchema(extensions ?? defaultExtensions)
|
||||
|
||||
try {
|
||||
const body = yDocToProsemirrorJSON(ydoc, field)
|
||||
@ -53,8 +59,8 @@ export function yDocToNode (extensions: Extensions, ydoc: Doc, field?: string):
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function yDocContentToNodes (extensions: Extensions, content: ArrayBuffer): Node[] {
|
||||
const schema = getSchema(extensions)
|
||||
export function yDocContentToNodes (content: ArrayBuffer, schema?: Schema, extensions?: Extensions): Node[] {
|
||||
schema ??= getSchema(extensions ?? defaultExtensions)
|
||||
|
||||
const nodes: Node[] = []
|
||||
|
@ -104,16 +104,6 @@
|
||||
ol ol ol ol ol ol { list-style: lower-roman; }
|
||||
ol ol ol ol ol ol ol { list-style: decimal; }
|
||||
|
||||
blockquote {
|
||||
margin-inline: 1px 0;
|
||||
padding-left: 1.5em;
|
||||
padding-right: 1.5em;
|
||||
font-style: italic;
|
||||
position: relative;
|
||||
|
||||
border-left: 3px solid var(--theme-text-primary-color);
|
||||
}
|
||||
|
||||
/* Placeholder (at the top) */
|
||||
p.is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
|
@ -300,6 +300,16 @@ table.proseTable {
|
||||
}
|
||||
}
|
||||
|
||||
.proseBlockQuote {
|
||||
margin-inline: 1px 0;
|
||||
padding-left: 1.5em;
|
||||
padding-right: 1.5em;
|
||||
font-style: italic;
|
||||
position: relative;
|
||||
|
||||
border-left: 3px solid var(--theme-text-primary-color);
|
||||
}
|
||||
|
||||
.proseCode {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
|
@ -23,6 +23,7 @@
|
||||
import { floorFractionDigits } from '../utils'
|
||||
import { themeStore } from '@hcengineering/theme'
|
||||
|
||||
export let id: string | undefined = undefined
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let maxWidth: string = '100%'
|
||||
export let value: string | number | undefined = undefined
|
||||
@ -106,6 +107,7 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
{id}
|
||||
class="antiEditBox"
|
||||
class:flex-grow={fullSize}
|
||||
class:w-full={focusable || fullSize}
|
||||
|
@ -25,7 +25,7 @@
|
||||
import { Action } from '@hcengineering/ui'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
import { HTMLViewer } from '@hcengineering/presentation'
|
||||
|
||||
import ActivityMessageTemplate from '../activity-message/ActivityMessageTemplate.svelte'
|
||||
import ActivityMessageHeader from '../activity-message/ActivityMessageHeader.svelte'
|
||||
@ -99,7 +99,7 @@
|
||||
<svelte:fragment slot="content">
|
||||
<div class="flex-row-center">
|
||||
<div class="customContent">
|
||||
<MessageViewer message={content} />
|
||||
<HTMLViewer value={content} />
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy, tick } from 'svelte'
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { Account, Class, Doc, generateId, IdMap, Ref, Space, toIdMap } from '@hcengineering/core'
|
||||
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'
|
||||
@ -27,7 +27,7 @@
|
||||
export let objectId: Ref<Doc>
|
||||
export let space: Ref<Space>
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let content: string = ''
|
||||
export let content: Markup = ''
|
||||
export let iconSend: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let labelSend: IntlString | undefined = undefined
|
||||
export let showSend = true
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { Account, Class, Doc, generateId, Ref, Space, toIdMap } from '@hcengineering/core'
|
||||
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'
|
||||
@ -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: string = ''
|
||||
export let content: Markup = ''
|
||||
export let placeholder: IntlString | undefined = undefined
|
||||
export let alwaysEdit = false
|
||||
export let showButtons = false
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { Calendar, RecurringRule, Visibility, generateEventId } from '@hcengineering/calendar'
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { Class, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
|
||||
import { Class, Doc, Markup, Ref, getCurrentAccount } from '@hcengineering/core'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||
import {
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
let reminders = [30 * 60 * 1000]
|
||||
|
||||
let description: string = ''
|
||||
let description: Markup = ''
|
||||
let visibility: Visibility = 'private'
|
||||
const me = getCurrentAccount()
|
||||
let space: Ref<Calendar> = `${me._id}_calendar` as Ref<Calendar>
|
||||
|
@ -21,6 +21,7 @@
|
||||
import chunter, { ChatMessage, ThreadMessage } from '@hcengineering/chunter'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import activity, { ActivityMessage } from '@hcengineering/activity'
|
||||
import { EmptyMarkup } from '@hcengineering/text-editor'
|
||||
|
||||
export let object: Doc
|
||||
export let chatMessage: ChatMessage | undefined = undefined
|
||||
@ -47,8 +48,8 @@
|
||||
const draftController = new DraftController<MessageDraft>(draftKey)
|
||||
const currentDraft = shouldSaveDraft ? $draftsStore[draftKey] : undefined
|
||||
|
||||
const emptyMessage = {
|
||||
message: '<p></p>',
|
||||
const emptyMessage: Pick<MessageDraft, 'message' | 'attachments'> = {
|
||||
message: EmptyMarkup,
|
||||
attachments: 0
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
import { ActivityMessage, ActivityMessageViewlet } from '@hcengineering/activity'
|
||||
import type { Person } from '@hcengineering/contact'
|
||||
import type { Account, AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||
import type { Account, AttachedDoc, Class, Doc, Markup, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||
import { NotificationType } from '@hcengineering/notification'
|
||||
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
|
||||
import { IntlString, plugin } from '@hcengineering/platform'
|
||||
@ -47,7 +47,7 @@ export interface DirectMessage extends ChunterSpace {}
|
||||
* @deprecated use ChatMessage instead
|
||||
*/
|
||||
export interface ChunterMessage extends AttachedDoc {
|
||||
content: string
|
||||
content: Markup
|
||||
attachments?: number
|
||||
createBy: Ref<Account>
|
||||
editedOn?: Timestamp
|
||||
@ -90,7 +90,7 @@ export interface ObjectChatPanel extends Class<Doc> {
|
||||
* @public
|
||||
*/
|
||||
export interface ChatMessage extends ActivityMessage {
|
||||
message: string
|
||||
message: Markup
|
||||
attachments?: number
|
||||
editedOn?: Timestamp
|
||||
}
|
||||
|
@ -61,7 +61,7 @@
|
||||
"@hcengineering/document": "^0.6.0",
|
||||
"@hcengineering/time": "^0.6.0",
|
||||
"@hcengineering/rank": "^0.6.0",
|
||||
"@tiptap/core": "^2.1.12",
|
||||
"@tiptap/core": "^2.2.4",
|
||||
"slugify": "^1.6.6",
|
||||
"fast-equals": "^2.0.3"
|
||||
}
|
||||
|
@ -30,7 +30,6 @@
|
||||
import document, { Teamspace } from '@hcengineering/document'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import presentation, { Card, getClient } from '@hcengineering/presentation'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||
import {
|
||||
Button,
|
||||
EditBox,
|
||||
@ -274,6 +273,7 @@
|
||||
</div>
|
||||
<div class="padding">
|
||||
<EditBox
|
||||
id="teamspace-title"
|
||||
bind:value={name}
|
||||
placeholder={documentRes.string.TeamspaceTitlePlaceholder}
|
||||
kind={'large-style'}
|
||||
@ -291,11 +291,10 @@
|
||||
<div class="antiGrid-row__header topAlign">
|
||||
<Label label={documentRes.string.Description} />
|
||||
</div>
|
||||
<div class="padding clear-mins">
|
||||
<StyledTextBox
|
||||
alwaysEdit
|
||||
showButtons={false}
|
||||
bind:content={description}
|
||||
<div class="padding">
|
||||
<EditBox
|
||||
id="teamspace-description"
|
||||
bind:value={description}
|
||||
placeholder={documentRes.string.TeamspaceDescriptionPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
|
@ -43,6 +43,7 @@
|
||||
"@hcengineering/gmail": "^0.6.15",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/presentation": "^0.6.2",
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"@hcengineering/text-editor": "^0.6.0",
|
||||
"@hcengineering/contact": "^0.6.20",
|
||||
"@hcengineering/setting": "^0.6.11",
|
||||
|
@ -17,13 +17,14 @@
|
||||
import attachmentP, { Attachment } from '@hcengineering/attachment'
|
||||
import { AttachmentPresenter } from '@hcengineering/attachment-resources'
|
||||
import contact, { Channel, Contact, getName } from '@hcengineering/contact'
|
||||
import { Data, generateId } from '@hcengineering/core'
|
||||
import { Data, Markup, generateId } from '@hcengineering/core'
|
||||
import { NewMessage, SharedMessage } from '@hcengineering/gmail'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Integration } from '@hcengineering/setting'
|
||||
import templates, { TemplateDataProvider } from '@hcengineering/templates'
|
||||
import { EmptyMarkup, htmlToMarkup, isEmptyMarkup } from '@hcengineering/text'
|
||||
import { StyledTextEditor } from '@hcengineering/text-editor'
|
||||
import { Button, EditBox, IconArrowLeft, IconAttachment, Label, Scroller } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
@ -43,9 +44,10 @@
|
||||
|
||||
let copy: string = ''
|
||||
|
||||
const obj: Data<NewMessage> = {
|
||||
let content: Markup = EmptyMarkup
|
||||
|
||||
const obj: Omit<Data<NewMessage>, 'content'> = {
|
||||
subject: currentMessage ? 'RE: ' + currentMessage.subject : '',
|
||||
content: '',
|
||||
to: channel.value,
|
||||
replyTo: currentMessage?.messageId,
|
||||
status: 'new'
|
||||
@ -53,7 +55,7 @@
|
||||
|
||||
let templateProvider: TemplateDataProvider | undefined
|
||||
|
||||
getResource(templates.function.GetTemplateDataProvider).then((p) => {
|
||||
void getResource(templates.function.GetTemplateDataProvider).then((p) => {
|
||||
templateProvider = p()
|
||||
})
|
||||
|
||||
@ -61,14 +63,15 @@
|
||||
templateProvider?.destroy()
|
||||
})
|
||||
|
||||
$: templateProvider && templateProvider.set(contact.class.Contact, object)
|
||||
$: templateProvider !== undefined && templateProvider.set(contact.class.Contact, object)
|
||||
|
||||
async function sendMsg () {
|
||||
async function sendMsg (): Promise<void> {
|
||||
await client.createDoc(
|
||||
plugin.class.NewMessage,
|
||||
plugin.space.Gmail,
|
||||
{
|
||||
...obj,
|
||||
content: htmlToMarkup(content),
|
||||
attachments: attachments.length,
|
||||
from: selectedIntegration.createdBy,
|
||||
copy: copy
|
||||
@ -86,7 +89,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
let inputFile: HTMLInputElement
|
||||
|
||||
function fileSelected () {
|
||||
function fileSelected (): void {
|
||||
progress = true
|
||||
const list = inputFile.files
|
||||
if (list === null || list.length === 0) return
|
||||
@ -98,7 +101,7 @@
|
||||
progress = false
|
||||
}
|
||||
|
||||
function fileDrop (e: DragEvent) {
|
||||
function fileDrop (e: DragEvent): void {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
progress = true
|
||||
@ -207,7 +210,7 @@
|
||||
<Button
|
||||
label={plugin.string.Send}
|
||||
kind={'primary'}
|
||||
disabled={progress || obj.content === '' || obj.content === '<p></p>'}
|
||||
disabled={progress || isEmptyMarkup(content)}
|
||||
on:click={sendMsg}
|
||||
/>
|
||||
</div>
|
||||
@ -239,7 +242,7 @@
|
||||
<EditBox label={plugin.string.Copy} bind:value={copy} placeholder={plugin.string.CopyPlaceholder} />
|
||||
</div>
|
||||
<div class="input clear-mins">
|
||||
<StyledTextEditor full bind:content={obj.content} maxHeight={'max'} on:template={onTemplate} />
|
||||
<StyledTextEditor full bind:content maxHeight={'max'} on:template={onTemplate} />
|
||||
</div>
|
||||
</Scroller>
|
||||
<div class="antiVSpacer x2" />
|
||||
|
@ -17,13 +17,13 @@
|
||||
import attachmentP, { Attachment } from '@hcengineering/attachment'
|
||||
import { AttachmentPresenter } from '@hcengineering/attachment-resources'
|
||||
import contact, { Channel, Contact, getName as getContactName } from '@hcengineering/contact'
|
||||
import { generateId, getCurrentAccount, Ref, toIdMap } from '@hcengineering/core'
|
||||
import { generateId, getCurrentAccount, Markup, Ref, toIdMap } from '@hcengineering/core'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import setting, { Integration } from '@hcengineering/setting'
|
||||
import templates, { TemplateDataProvider } from '@hcengineering/templates'
|
||||
import { StyledTextEditor } from '@hcengineering/text-editor'
|
||||
import { StyledTextEditor, isEmptyMarkup } from '@hcengineering/text-editor'
|
||||
import {
|
||||
Button,
|
||||
EditBox,
|
||||
@ -38,6 +38,7 @@
|
||||
import plugin from '../plugin'
|
||||
import Connect from './Connect.svelte'
|
||||
import IntegrationSelector from './IntegrationSelector.svelte'
|
||||
import { markupToHTML } from '@hcengineering/text'
|
||||
|
||||
export let value: Contact[] | Contact
|
||||
const contacts = Array.isArray(value) ? value : [value]
|
||||
@ -69,18 +70,19 @@
|
||||
const attachmentParentId = generateId()
|
||||
|
||||
let subject: string = ''
|
||||
let content: string = ''
|
||||
let content: Markup = ''
|
||||
let copy: string = ''
|
||||
let saved = false
|
||||
|
||||
async function sendMsg () {
|
||||
async function sendMsg (): Promise<void> {
|
||||
const templateProvider = (await getResource(templates.function.GetTemplateDataProvider))()
|
||||
if (templateProvider === undefined || selectedIntegration === undefined) return
|
||||
for (const channel of channels) {
|
||||
const target = contacts.find((p) => p._id === channel.attachedTo)
|
||||
if (target === undefined) continue
|
||||
templateProvider.set(contact.class.Contact, target)
|
||||
const message = await templateProvider.fillTemplate(content)
|
||||
const htmlContent = markupToHTML(content)
|
||||
const message = await templateProvider.fillTemplate(htmlContent)
|
||||
const id = await client.createDoc(plugin.class.NewMessage, plugin.space.Gmail, {
|
||||
subject,
|
||||
content: message,
|
||||
@ -296,7 +298,7 @@
|
||||
label={plugin.string.Send}
|
||||
size={'small'}
|
||||
kind={'primary'}
|
||||
disabled={channels.length === 0 || content === '' || content === '<p></p>'}
|
||||
disabled={channels.length === 0 || isEmptyMarkup(content)}
|
||||
on:click={sendMsg}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -16,7 +16,7 @@
|
||||
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
||||
import calendar from '@hcengineering/calendar'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import core, { DocumentQuery, generateId, Ref } from '@hcengineering/core'
|
||||
import core, { DocumentQuery, generateId, Markup, Ref } from '@hcengineering/core'
|
||||
import { Request, RequestType, Staff, toTzDate } from '@hcengineering/hr'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { Card, createQuery, getClient } from '@hcengineering/presentation'
|
||||
@ -39,7 +39,7 @@
|
||||
export let docQuery: DocumentQuery<Employee> | undefined
|
||||
export let employeeRequests: Map<Ref<Staff>, Request[]>
|
||||
|
||||
let description: string = ''
|
||||
let description: Markup = ''
|
||||
let employee: Ref<Employee> = staff._id
|
||||
|
||||
const objectId: Ref<Request> = generateId()
|
||||
|
@ -15,7 +15,7 @@
|
||||
//
|
||||
|
||||
import type { Contact } from '@hcengineering/contact'
|
||||
import type { Attribute, Class, Doc, Ref, Status, Timestamp } from '@hcengineering/core'
|
||||
import type { Attribute, Class, Doc, Markup, Ref, Status, Timestamp } from '@hcengineering/core'
|
||||
import { Mixin } from '@hcengineering/core'
|
||||
import type { Asset, IntlString, Plugin } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
@ -25,7 +25,7 @@ import type { Project, ProjectType, ProjectTypeDescriptor, Task, TaskType } from
|
||||
* @public
|
||||
*/
|
||||
export interface Funnel extends Project {
|
||||
fullDescription?: string
|
||||
fullDescription?: Markup
|
||||
attachments?: number
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import { Doc, Markup } from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { CommonInboxNotification } from '@hcengineering/notification'
|
||||
@ -23,11 +23,11 @@
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let content = ''
|
||||
let content: Markup = ''
|
||||
|
||||
$: void updateContent(value.message, value.messageHtml)
|
||||
|
||||
async function updateContent (message?: IntlString, messageHtml?: string): Promise<void> {
|
||||
async function updateContent (message?: IntlString, messageHtml?: Markup): Promise<void> {
|
||||
if (messageHtml !== undefined) {
|
||||
content = messageHtml
|
||||
} else if (message !== undefined) {
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
IdMap,
|
||||
Markup,
|
||||
Mixin,
|
||||
Ref,
|
||||
Space,
|
||||
@ -244,7 +245,7 @@ export interface CommonInboxNotification extends InboxNotification {
|
||||
headerObjectId?: Ref<Doc>
|
||||
headerObjectClass?: Ref<Class<Doc>>
|
||||
message?: IntlString
|
||||
messageHtml?: string
|
||||
messageHtml?: Markup
|
||||
props?: Record<string, any>
|
||||
icon?: Asset
|
||||
iconProps?: Record<string, any>
|
||||
|
@ -5,6 +5,7 @@
|
||||
Card,
|
||||
createQuery,
|
||||
getClient,
|
||||
HTMLViewer,
|
||||
IndexedDocumentCompare,
|
||||
MessageViewer,
|
||||
SpaceSelect
|
||||
@ -188,7 +189,7 @@
|
||||
{vacancy.description}
|
||||
{/if}
|
||||
{#if vacancyState?.fullSummary}
|
||||
<MessageViewer message={vacancyState?.fullSummary.split('\n').join('<br/>')} />
|
||||
<HTMLViewer value={vacancyState?.fullSummary.split('\n').join('<br/>')} />
|
||||
{/if}
|
||||
</div>
|
||||
</Scroller>
|
||||
|
@ -16,7 +16,17 @@
|
||||
import calendar from '@hcengineering/calendar'
|
||||
import type { Contact, PersonAccount, Organization, Person } from '@hcengineering/contact'
|
||||
import contact from '@hcengineering/contact'
|
||||
import { Account, Class, Client, DateRangeMode, Doc, generateId, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import {
|
||||
Account,
|
||||
Class,
|
||||
Client,
|
||||
DateRangeMode,
|
||||
Doc,
|
||||
generateId,
|
||||
getCurrentAccount,
|
||||
Markup,
|
||||
Ref
|
||||
} from '@hcengineering/core'
|
||||
import { getResource, OK, Resource, Severity, Status } from '@hcengineering/platform'
|
||||
import { Card, getClient } from '@hcengineering/presentation'
|
||||
import { UserBox, UserBoxList } from '@hcengineering/contact-resources'
|
||||
@ -45,7 +55,7 @@
|
||||
let status: Status = OK
|
||||
|
||||
let title: string = ''
|
||||
let description: string = ''
|
||||
let description: Markup = ''
|
||||
let startDate: number = initDate.getTime()
|
||||
let dueDate: number = initDate.getTime() + 30 * 60 * 1000
|
||||
let location: string = ''
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
import { Event } from '@hcengineering/calendar'
|
||||
import type { Channel, Organization, Person } from '@hcengineering/contact'
|
||||
import type { AttachedData, AttachedDoc, Ref, Space, Status, Timestamp } from '@hcengineering/core'
|
||||
import type { AttachedData, AttachedDoc, Markup, Ref, Space, Status, Timestamp } from '@hcengineering/core'
|
||||
import { TagReference } from '@hcengineering/tags'
|
||||
import type { Project, Task } from '@hcengineering/task'
|
||||
|
||||
@ -102,6 +102,6 @@ export interface Opinion extends AttachedDoc {
|
||||
attachedTo: Ref<Review>
|
||||
comments?: number
|
||||
attachments?: number
|
||||
description: string
|
||||
description: Markup
|
||||
value: string
|
||||
}
|
||||
|
@ -16,10 +16,10 @@
|
||||
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
|
||||
import chunter, { ChatMessage } from '@hcengineering/chunter'
|
||||
import { PersonAccount } from '@hcengineering/contact'
|
||||
import { AttachedData, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import { AttachedData, getCurrentAccount, Markup, Ref } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Request, RequestStatus } from '@hcengineering/request'
|
||||
import type { RefAction } from '@hcengineering/text-editor'
|
||||
import { type RefAction, isEmptyMarkup } from '@hcengineering/text-editor'
|
||||
import { Button } from '@hcengineering/ui'
|
||||
|
||||
import request from '../plugin'
|
||||
@ -53,7 +53,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
let message: string = ''
|
||||
let message: Markup = ''
|
||||
let attachments: number | undefined = 0
|
||||
|
||||
async function onUpdate (event: CustomEvent<AttachedData<ChatMessage>>) {
|
||||
@ -90,7 +90,7 @@
|
||||
}
|
||||
|
||||
function commentIsEmpty (message: string, attachments: number | undefined): boolean {
|
||||
return (message === '<p></p>' || message.trim().length === 0) && !((attachments ?? 0) > 0)
|
||||
return isEmptyMarkup(message) && !((attachments ?? 0) > 0)
|
||||
}
|
||||
|
||||
let refInput: AttachmentRefInput
|
||||
|
@ -50,6 +50,7 @@
|
||||
"@hcengineering/setting": "^0.6.11",
|
||||
"@hcengineering/telegram": "^0.6.14",
|
||||
"@hcengineering/templates": "^0.6.7",
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"@hcengineering/text-editor": "^0.6.0",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/view-resources": "^0.6.0",
|
||||
|
@ -25,6 +25,7 @@
|
||||
import setting, { Integration } from '@hcengineering/setting'
|
||||
import type { NewTelegramMessage, SharedTelegramMessage, TelegramMessage } from '@hcengineering/telegram'
|
||||
import templates, { TemplateDataProvider } from '@hcengineering/templates'
|
||||
import { markupToHTML } from '@hcengineering/text'
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
@ -121,7 +122,7 @@
|
||||
channel._class,
|
||||
'newMessages',
|
||||
{
|
||||
content: message,
|
||||
content: markupToHTML(message),
|
||||
status: 'new',
|
||||
attachments
|
||||
},
|
||||
|
@ -18,7 +18,7 @@
|
||||
import { AttachmentList } from '@hcengineering/attachment-resources'
|
||||
import { formatName } from '@hcengineering/contact'
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
import { HTMLViewer } from '@hcengineering/presentation'
|
||||
import type { SharedTelegramMessage } from '@hcengineering/telegram'
|
||||
import { CheckBox, getPlatformColorForText, themeStore } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@ -34,6 +34,7 @@
|
||||
|
||||
<div class="message-row-bg" class:selectable class:selected-row={selected} data-type={message.incoming ? 'in' : 'out'}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="message-row"
|
||||
class:selectable
|
||||
@ -56,7 +57,7 @@
|
||||
<AttachmentList {attachments} />
|
||||
{/if}
|
||||
<div class="flex">
|
||||
<div class="caption-color mr-4"><MessageViewer message={message.content} /></div>
|
||||
<div class="caption-color mr-4"><HTMLViewer value={message.content} /></div>
|
||||
<div class="time">
|
||||
{new Date(message.sendOn).toLocaleString('default', { hour: 'numeric', minute: 'numeric' })}
|
||||
</div>
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createQuery, MessageViewer } from '@hcengineering/presentation'
|
||||
import { createQuery, HTMLViewer } from '@hcengineering/presentation'
|
||||
import { TelegramMessage } from '@hcengineering/telegram'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
|
||||
{#if doc}
|
||||
<div class="content lines-limit-2">
|
||||
<MessageViewer message={doc.content} />
|
||||
<HTMLViewer value={doc.content} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import { createQuery, getClient, HTMLViewer } from '@hcengineering/presentation'
|
||||
import { TelegramMessage } from '@hcengineering/telegram'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { buildRemovedDoc, checkIsObjectRemoved } from '@hcengineering/view-resources'
|
||||
@ -29,7 +29,7 @@
|
||||
|
||||
$: value === undefined && _id && loadObject(_id)
|
||||
|
||||
async function loadObject (_id: Ref<TelegramMessage>) {
|
||||
async function loadObject (_id: Ref<TelegramMessage>): Promise<void> {
|
||||
const isRemoved = await checkIsObjectRemoved(client, _id, telegram.class.Message)
|
||||
|
||||
if (isRemoved) {
|
||||
@ -44,7 +44,7 @@
|
||||
|
||||
{#if value}
|
||||
<div class="content lines-limit-2 overflow-label">
|
||||
<MessageViewer message={value.content} {preview} />
|
||||
<HTMLViewer value={value.content} {preview} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -13,13 +13,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
import { HTMLViewer } from '@hcengineering/presentation'
|
||||
import { TelegramMessage } from '@hcengineering/telegram'
|
||||
export let value: TelegramMessage
|
||||
</script>
|
||||
|
||||
<div class="content lines-limit-2">
|
||||
<MessageViewer message={value.content} />
|
||||
<HTMLViewer value={value.content} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||
import type { Class, Doc, Markup, Ref, Space } from '@hcengineering/core'
|
||||
import type { IntlString, Plugin, Resource } from '@hcengineering/platform'
|
||||
import { Asset, plugin } from '@hcengineering/platform'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
@ -29,7 +29,7 @@ export interface TemplateCategory extends Space {}
|
||||
export interface MessageTemplate extends Doc {
|
||||
space: Ref<TemplateCategory>
|
||||
title: string
|
||||
message: string
|
||||
message: Markup
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -46,6 +46,7 @@
|
||||
import tags, { TagElement, TagReference } from '@hcengineering/tags'
|
||||
import { TaskType, makeRank } from '@hcengineering/task'
|
||||
import { TaskKindSelector } from '@hcengineering/task-resources'
|
||||
import { EmptyMarkup } from '@hcengineering/text-editor'
|
||||
import {
|
||||
Component as ComponentType,
|
||||
Issue,
|
||||
@ -227,7 +228,7 @@
|
||||
assignee: assignee ?? currentProject?.defaultAssignee,
|
||||
status: status ?? currentProject?.defaultIssueStatus,
|
||||
parentIssue: parentIssue?._id,
|
||||
description: '<p></p>',
|
||||
description: EmptyMarkup,
|
||||
component: component ?? $activeComponent ?? null,
|
||||
milestone: milestone ?? $activeMilestone ?? null,
|
||||
priority: priority ?? IssuePriority.NoPriority,
|
||||
|
@ -31,7 +31,6 @@
|
||||
import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation'
|
||||
import task, { ProjectType, TaskType } from '@hcengineering/task'
|
||||
import { taskTypeStore, typeStore } from '@hcengineering/task-resources'
|
||||
import { StyledTextBox } from '@hcengineering/text-editor'
|
||||
import { IssueStatus, Project, TimeReportDayType } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
@ -339,6 +338,7 @@
|
||||
</div>
|
||||
<div class="padding">
|
||||
<EditBox
|
||||
id="project-title"
|
||||
bind:value={name}
|
||||
placeholder={tracker.string.ProjectTitlePlaceholder}
|
||||
kind={'large-style'}
|
||||
@ -360,6 +360,7 @@
|
||||
</div>
|
||||
<div bind:this={changeIdentityRef} class="padding flex-row-center relative">
|
||||
<EditBox
|
||||
id="project-identifier"
|
||||
bind:value={identifier}
|
||||
disabled={!isNew}
|
||||
placeholder={tracker.string.ProjectIdentifierPlaceholder}
|
||||
@ -379,10 +380,9 @@
|
||||
<Label label={tracker.string.Description} />
|
||||
</div>
|
||||
<div class="padding clear-mins">
|
||||
<StyledTextBox
|
||||
alwaysEdit
|
||||
showButtons={false}
|
||||
bind:content={description}
|
||||
<EditBox
|
||||
id="project-description"
|
||||
bind:value={description}
|
||||
placeholder={tracker.string.IssueDescriptionPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
|
@ -48,6 +48,7 @@
|
||||
"@hcengineering/view": "^0.6.9",
|
||||
"@hcengineering/ui": "^0.6.11",
|
||||
"@hcengineering/task": "^0.6.13",
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"@hcengineering/preference": "^0.6.9",
|
||||
"@hcengineering/notification": "^0.6.16",
|
||||
"@hcengineering/presentation": "^0.6.2",
|
||||
|
@ -19,10 +19,8 @@
|
||||
import { getAttribute, getClient, KeyedAttribute, updateAttribute } from '@hcengineering/presentation'
|
||||
import { FullDescriptionBox } from '@hcengineering/text-editor'
|
||||
|
||||
// export let objectId: Ref<Doc>
|
||||
// export let _class: Ref<Class<Doc>>
|
||||
// TODO Rename this component to MarkupEditor
|
||||
export let object: Doc
|
||||
// export let space: Ref<Space>
|
||||
export let key: KeyedAttribute
|
||||
|
||||
$: description = getAttribute(getClient(), object, key)
|
||||
|
@ -14,12 +14,12 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
import { HTMLViewer } from '@hcengineering/presentation'
|
||||
import { ShowMore } from '@hcengineering/ui'
|
||||
|
||||
export let value: string
|
||||
</script>
|
||||
|
||||
<ShowMore>
|
||||
<MessageViewer message={value} />
|
||||
<HTMLViewer {value} />
|
||||
</ShowMore>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
// Copyright © 2023 Anticrm Platform Contributors.
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
// Copyright © 2023, 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
|
||||
@ -14,35 +14,78 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Markup } from '@hcengineering/core'
|
||||
import { EmptyMarkup, MarkupNode, MarkupNodeType, markupToJSON } from '@hcengineering/text'
|
||||
import { MarkupDiffViewer } from '@hcengineering/text-editor'
|
||||
import { ShowMore } from '@hcengineering/ui'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
export let value: Markup | undefined
|
||||
export let prevValue: Markup | undefined = undefined
|
||||
|
||||
export let value: string | undefined
|
||||
export let prevValue: string | undefined = undefined
|
||||
export let showOnlyDiff: boolean = false
|
||||
|
||||
function removeSimilarLines (str1: string | undefined, str2: string | undefined) {
|
||||
str1 = str1 ?? ''
|
||||
str2 = str2 ?? ''
|
||||
const lines1 = str1.split('</p>')
|
||||
const lines2 = str2.split('</p>')
|
||||
let result1 = ''
|
||||
let result2 = ''
|
||||
for (let i = 0; i < Math.max(lines1.length, lines2.length); i++) {
|
||||
if (lines1[i] !== lines2[i]) {
|
||||
if (lines1[i]) result1 += lines1[i] + '</p>'
|
||||
if (lines2[i]) result2 += lines2[i] + '</p>'
|
||||
$: content = markupToJSON(value ?? EmptyMarkup)
|
||||
$: comparedVersion = markupToJSON(prevValue ?? EmptyMarkup)
|
||||
|
||||
function cleanup (node1: MarkupNode, node2: MarkupNode): MarkupNode[] {
|
||||
if (node1.type !== MarkupNodeType.doc || node2.type !== MarkupNodeType.doc) {
|
||||
return [node1, node2]
|
||||
}
|
||||
|
||||
const content1 = node1.content ?? []
|
||||
const content2 = node2.content ?? []
|
||||
|
||||
const newContent1: MarkupNode[] = []
|
||||
const newContent2: MarkupNode[] = []
|
||||
for (let i = 0; i < Math.max(content1.length, content2.length); i++) {
|
||||
if (!same(content1[i], content2[i])) {
|
||||
if (content1[i] !== undefined) {
|
||||
newContent1.push(content1[i])
|
||||
}
|
||||
if (content2[i] !== undefined) {
|
||||
newContent2.push(content2[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
value = result1
|
||||
prevValue = result2
|
||||
|
||||
return [
|
||||
{ ...node1, content: newContent1 },
|
||||
{ ...node2, content: newContent2 }
|
||||
]
|
||||
}
|
||||
|
||||
$: showOnlyDiff && removeSimilarLines(value, prevValue)
|
||||
function same (node1: MarkupNode | undefined, node2: MarkupNode | undefined): boolean {
|
||||
if (node1 === undefined && node2 === undefined) return true
|
||||
if (node1 === undefined || node2 === undefined) return false
|
||||
|
||||
if (
|
||||
node1.type !== node2.type ||
|
||||
node1.text !== node2.text ||
|
||||
!deepEqual(node1.marks ?? [], node2.marks ?? []) ||
|
||||
!deepEqual(node1.attrs ?? {}, node2.attrs ?? {})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const content1 = node1.content ?? []
|
||||
const content2 = node2.content ?? []
|
||||
if (content1.length !== content2.length) return false
|
||||
|
||||
for (let i = 0; i < content1.length; i++) {
|
||||
if (!same(content1[i], content2[i])) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
$: if (showOnlyDiff) {
|
||||
;[content, comparedVersion] = cleanup(content, comparedVersion)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ShowMore>
|
||||
{#key [value, prevValue]}
|
||||
<MarkupDiffViewer content={value ?? ''} comparedVersion={prevValue ?? ''} />
|
||||
<MarkupDiffViewer {content} {comparedVersion} />
|
||||
{/key}
|
||||
</ShowMore>
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { MessageViewer } from '@hcengineering/presentation'
|
||||
import { HTMLViewer } from '@hcengineering/presentation'
|
||||
import { getPlatformColor, Label as LabelComponent, themeStore } from '@hcengineering/ui'
|
||||
import view from '../../plugin'
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
<a class="fs-title mb-1" {href}>#{data.number} {data.title}</a>
|
||||
{#if data.body}
|
||||
<div>
|
||||
<MessageViewer message={data.body} />
|
||||
<HTMLViewer value={data.body} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-between">
|
||||
|
@ -14,12 +14,13 @@
|
||||
//
|
||||
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { EmptyMarkup, MarkupNodeType, jsonToMarkup } from '@hcengineering/text'
|
||||
|
||||
import { getReferencesData } from '../references'
|
||||
|
||||
describe('extractBacklinks', () => {
|
||||
it('should return no references for empty document', () => {
|
||||
const content = '<p></p>'
|
||||
const content = EmptyMarkup
|
||||
const references = getReferencesData(
|
||||
'srcDocId' as Ref<Doc>,
|
||||
'srcDocClass' as Ref<Class<Doc>>,
|
||||
@ -32,8 +33,28 @@ describe('extractBacklinks', () => {
|
||||
})
|
||||
|
||||
it('should parse single backlink', () => {
|
||||
const content =
|
||||
'<p>hello <span class="reference" data-type="reference" data-id="id" data-objectclass="contact:class:Person" data-label="Appleseed John">@Appleseed John</span> </p>'
|
||||
const content = jsonToMarkup({
|
||||
type: MarkupNodeType.doc,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.paragraph,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.reference,
|
||||
attrs: {
|
||||
id: 'id',
|
||||
objectclass: 'contact:class:Person',
|
||||
label: 'Appleseed John'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: ' hello'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const references = getReferencesData(
|
||||
'srcDocId' as Ref<Doc>,
|
||||
@ -52,7 +73,7 @@ describe('extractBacklinks', () => {
|
||||
srcDocId: 'srcDocId',
|
||||
srcDocClass: 'srcDocClass',
|
||||
message:
|
||||
'hello <span data-type="reference" data-id="id" data-objectclass="contact:class:Person" data-label="Appleseed John" class="reference">@Appleseed John</span>',
|
||||
'{"type":"paragraph","content":[{"type":"reference","attrs":{"id":"id","objectclass":"contact:class:Person","label":"Appleseed John","class":null}},{"type":"text","text":" hello"}]}',
|
||||
attachedDocId: 'attachedDocId',
|
||||
attachedDocClass: 'attachedDocClass'
|
||||
}
|
||||
@ -60,8 +81,36 @@ describe('extractBacklinks', () => {
|
||||
})
|
||||
|
||||
it('should parse single backlink for multiple references', () => {
|
||||
const content =
|
||||
'<p><span class="reference" data-type="reference" data-id="id" data-label="Appleseed John" data-objectclass="contact:class:Person">@Appleseed John</span> <span data-type="reference" class="reference" data-id="id" data-label="Appleseed John" data-objectclass="contact:class:Person">@Appleseed John</span> </p>'
|
||||
const content = jsonToMarkup({
|
||||
type: MarkupNodeType.doc,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.paragraph,
|
||||
content: [
|
||||
{
|
||||
type: MarkupNodeType.reference,
|
||||
attrs: {
|
||||
id: 'id',
|
||||
objectclass: 'contact:class:Person',
|
||||
label: 'Appleseed John'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: MarkupNodeType.text,
|
||||
text: ' '
|
||||
},
|
||||
{
|
||||
type: MarkupNodeType.reference,
|
||||
attrs: {
|
||||
id: 'id',
|
||||
objectclass: 'contact:class:Person',
|
||||
label: 'Appleseed John'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const references = getReferencesData(
|
||||
'srcDocId' as Ref<Doc>,
|
||||
|
@ -36,7 +36,7 @@ import core, {
|
||||
Type
|
||||
} from '@hcengineering/core'
|
||||
import notification, { MentionInboxNotification } from '@hcengineering/notification'
|
||||
import { ServerKit, extractReferences, getHTML, parseHTML, yDocContentToNodes } from '@hcengineering/text'
|
||||
import { extractReferences, markupToPmNode, pmNodeToMarkup, yDocContentToNodes } from '@hcengineering/text'
|
||||
import { StorageAdapter, TriggerControl } from '@hcengineering/server-core'
|
||||
import activity, { ActivityMessage, ActivityReference } from '@hcengineering/activity'
|
||||
import contact, { Person, PersonAccount } from '@hcengineering/contact'
|
||||
@ -47,18 +47,16 @@ import {
|
||||
shouldNotifyCommon
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
|
||||
const extensions = [ServerKit]
|
||||
|
||||
export function isDocMentioned (doc: Ref<Doc>, content: string | Buffer): boolean {
|
||||
const references = []
|
||||
|
||||
if (content instanceof Buffer) {
|
||||
const nodes = yDocContentToNodes(extensions, content)
|
||||
const nodes = yDocContentToNodes(content)
|
||||
for (const node of nodes) {
|
||||
references.push(...extractReferences(node))
|
||||
}
|
||||
} else {
|
||||
const doc = parseHTML(content, extensions)
|
||||
const doc = markupToPmNode(content)
|
||||
references.push(...extractReferences(doc))
|
||||
}
|
||||
|
||||
@ -338,12 +336,12 @@ export function getReferencesData (
|
||||
const references = []
|
||||
|
||||
if (content instanceof Buffer) {
|
||||
const nodes = yDocContentToNodes(extensions, content)
|
||||
const nodes = yDocContentToNodes(content)
|
||||
for (const node of nodes) {
|
||||
references.push(...extractReferences(node))
|
||||
}
|
||||
} else {
|
||||
const doc = parseHTML(content, extensions)
|
||||
const doc = markupToPmNode(content)
|
||||
references.push(...extractReferences(doc))
|
||||
}
|
||||
|
||||
@ -355,7 +353,7 @@ export function getReferencesData (
|
||||
collection: 'references',
|
||||
srcDocId,
|
||||
srcDocClass,
|
||||
message: ref.parentNode !== null ? getHTML(ref.parentNode, extensions) : '',
|
||||
message: ref.parentNode !== null ? pmNodeToMarkup(ref.parentNode) : '',
|
||||
attachedDocId,
|
||||
attachedDocClass
|
||||
})
|
||||
@ -461,6 +459,7 @@ async function ActivityReferenceCreate (tx: TxCUD<Doc>, control: TriggerControl)
|
||||
const ctx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
|
||||
|
||||
if (ctx._class !== core.class.TxCreateDoc) return []
|
||||
if (control.hierarchy.isDerived(ctx.objectClass, notification.class.InboxNotification)) return []
|
||||
if (control.hierarchy.isDerived(ctx.objectClass, activity.class.ActivityReference)) return []
|
||||
|
||||
control.storageFx(async (adapter) => {
|
||||
@ -568,8 +567,9 @@ async function ActivityReferenceRemove (tx: Tx, control: TriggerControl): Promis
|
||||
export async function ReferenceTrigger (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
|
||||
const result: Tx[] = []
|
||||
|
||||
const etx = TxProcessor.extractTx(tx) as TxCreateDoc<Doc>
|
||||
const etx = TxProcessor.extractTx(tx) as TxCUD<Doc>
|
||||
if (control.hierarchy.isDerived(etx.objectClass, activity.class.ActivityReference)) return []
|
||||
if (control.hierarchy.isDerived(etx.objectClass, notification.class.InboxNotification)) return []
|
||||
|
||||
if (etx._class === core.class.TxCreateDoc) {
|
||||
result.push(...(await ActivityReferenceCreate(tx, control)))
|
||||
|
@ -41,6 +41,7 @@
|
||||
"@hcengineering/task": "^0.6.13",
|
||||
"@hcengineering/tracker": "^0.6.13",
|
||||
"@hcengineering/server-time": "^0.6.0",
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"@hcengineering/time": "^0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ import {
|
||||
isShouldNotifyTx
|
||||
} from '@hcengineering/server-notification-resources'
|
||||
import task, { makeRank } from '@hcengineering/task'
|
||||
import { jsonToMarkup, nodeDoc, nodeParagraph, nodeText } from '@hcengineering/text'
|
||||
import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker'
|
||||
import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time'
|
||||
import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time'
|
||||
@ -196,7 +197,7 @@ export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Pr
|
||||
headerIcon: time.icon.Planned,
|
||||
headerObjectId: object._id,
|
||||
headerObjectClass: object._class,
|
||||
messageHtml: todo.title
|
||||
messageHtml: jsonToMarkup(nodeDoc(nodeParagraph(nodeText(todo.title))))
|
||||
}
|
||||
|
||||
res.push(
|
||||
|
@ -63,8 +63,8 @@
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"@hocuspocus/server": "^2.9.0",
|
||||
"@hocuspocus/transformer": "^2.9.0",
|
||||
"@tiptap/core": "^2.1.12",
|
||||
"@tiptap/html": "^2.1.12",
|
||||
"@tiptap/core": "^2.2.4",
|
||||
"@tiptap/html": "^2.2.4",
|
||||
"mongodb": "^6.3.0",
|
||||
"yjs": "^13.5.52",
|
||||
"y-prosemirror": "^1.2.1",
|
||||
|
@ -33,7 +33,7 @@ import { StorageExtension } from './extensions/storage'
|
||||
import { Controller, getClientFactory } from './platform'
|
||||
import { RpcErrorResponse, RpcRequest, RpcResponse, methods } from './rpc'
|
||||
import { PlatformStorageAdapter } from './storage/platform'
|
||||
import { HtmlTransformer } from './transformers/html'
|
||||
import { MarkupTransformer } from './transformers/markup'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -83,7 +83,7 @@ export async function start (
|
||||
|
||||
const controller = new Controller()
|
||||
|
||||
const transformer = new HtmlTransformer(extensions)
|
||||
const transformer = new MarkupTransformer(extensions)
|
||||
|
||||
const hocuspocus = new Hocuspocus({
|
||||
address: '0.0.0.0',
|
||||
|
@ -220,7 +220,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
|
||||
})
|
||||
|
||||
const content = doc !== null && objectAttr in doc ? ((doc as any)[objectAttr] as string) : ''
|
||||
if (content.startsWith('<') && content.endsWith('>')) {
|
||||
if (content.startsWith('{') && content.endsWith('}')) {
|
||||
return await ctx.with('transform', {}, () => {
|
||||
return transformer.toYdoc(content, objectAttr)
|
||||
})
|
||||
|
41
server/collaborator/src/transformers/markup.ts
Normal file
41
server/collaborator/src/transformers/markup.ts
Normal file
@ -0,0 +1,41 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import { jsonToMarkup, markupToJSON } from '@hcengineering/text'
|
||||
import { TiptapTransformer, Transformer } from '@hocuspocus/transformer'
|
||||
import { Extensions } from '@tiptap/core'
|
||||
import { Doc } from 'yjs'
|
||||
|
||||
export class MarkupTransformer implements Transformer {
|
||||
transformer: Transformer
|
||||
|
||||
constructor (private readonly extensions: Extensions) {
|
||||
this.transformer = TiptapTransformer.extensions(extensions)
|
||||
}
|
||||
|
||||
fromYdoc (document: Doc, fieldName?: string | string[] | undefined): any {
|
||||
const json = this.transformer.fromYdoc(document, fieldName)
|
||||
return jsonToMarkup(json)
|
||||
}
|
||||
|
||||
toYdoc (document: any, fieldName: string): Doc {
|
||||
if (typeof document === 'string' && document !== '') {
|
||||
const json = markupToJSON(document)
|
||||
return this.transformer.toYdoc(json, fieldName)
|
||||
}
|
||||
|
||||
return new Doc()
|
||||
}
|
||||
}
|
@ -29,7 +29,6 @@
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"@types/html-to-text": "^8.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
@ -38,9 +37,9 @@
|
||||
"dependencies": {
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/text": "^0.6.1",
|
||||
"@hcengineering/query": "^0.6.8",
|
||||
"fast-equals": "^2.0.3",
|
||||
"html-to-text": "^9.0.3",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import core, {
|
||||
type ServerStorage,
|
||||
type WorkspaceId
|
||||
} from '@hcengineering/core'
|
||||
import { jsonToText, markupToJSON } from '@hcengineering/text'
|
||||
import { type DbAdapter } from '../adapter'
|
||||
import { updateDocWithPresenter } from '../mapper'
|
||||
import { type FullTextAdapter, type IndexedDoc } from '../types'
|
||||
@ -273,8 +274,15 @@ function updateDoc2Elastic (
|
||||
(attribute.type as ArrOf<any>).of._class === core.class.RefTo)
|
||||
))
|
||||
) {
|
||||
if (!(doc.fulltextSummary ?? '').includes(vv)) {
|
||||
doc.fulltextSummary = (doc.fulltextSummary ?? '') + vv + '\n'
|
||||
let vvv = vv
|
||||
if (
|
||||
attribute.type._class === core.class.TypeMarkup ||
|
||||
attribute.type._class === core.class.TypeCollaborativeMarkup
|
||||
) {
|
||||
vvv = jsonToText(markupToJSON(vv))
|
||||
}
|
||||
if (!(doc.fulltextSummary ?? '').includes(vvv)) {
|
||||
doc.fulltextSummary = (doc.fulltextSummary ?? '') + vvv + '\n'
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ import core, {
|
||||
type ServerStorage
|
||||
} from '@hcengineering/core'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { convert } from 'html-to-text'
|
||||
import { jsonToText, markupToJSON } from '@hcengineering/text'
|
||||
import { type DbAdapter } from '../adapter'
|
||||
import { type IndexedDoc } from '../types'
|
||||
import {
|
||||
@ -285,10 +285,7 @@ export async function extractIndexedValues (
|
||||
}
|
||||
|
||||
if (keyAttr.type._class === core.class.TypeMarkup || keyAttr.type._class === core.class.TypeCollaborativeMarkup) {
|
||||
sourceContent = convert(sourceContent, {
|
||||
preserveNewlines: true,
|
||||
selectors: [{ selector: 'img', format: 'skip' }]
|
||||
})
|
||||
sourceContent = jsonToText(markupToJSON(sourceContent))
|
||||
}
|
||||
|
||||
if (!opt.fieldFilter.every((it) => it(keyAttr, sourceContent))) {
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { MeasureContext, WorkspaceId } from '@hcengineering/core'
|
||||
import { ContentTextAdapter } from '@hcengineering/server-core'
|
||||
import { ServerKit, getText, yDocContentToNodes } from '@hcengineering/text'
|
||||
import { pmNodeToText, yDocContentToNodes } from '@hcengineering/text'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
const extensions = [ServerKit]
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -38,8 +36,8 @@ export async function createYDocAdapter (
|
||||
}
|
||||
|
||||
if (chunks.length > 0) {
|
||||
const nodes = yDocContentToNodes(extensions, Buffer.concat(chunks))
|
||||
return nodes.map(getText).join('\n')
|
||||
const nodes = yDocContentToNodes(Buffer.concat(chunks))
|
||||
return nodes.map(pmNodeToText).join('\n')
|
||||
}
|
||||
|
||||
return ''
|
||||
|
@ -27,24 +27,22 @@ export class DocumentsPage extends CommonPage {
|
||||
this.popupCreateDocument = new DocumentCreatePopup(page)
|
||||
this.popupMoveDocument = new DocumentMovePopup(page)
|
||||
|
||||
const newForm = page.locator('form[id="document:string:NewTeamspace"]')
|
||||
const editForm = page.locator('form[id="document:string:EditTeamspace"]')
|
||||
|
||||
this.buttonCreateDocument = page.locator('div[data-float="navigator"] button[id="new-document"]')
|
||||
this.divTeamspacesParent = page.locator('div#tree-teamspaces').locator('xpath=..')
|
||||
this.buttonCreateTeamspace = page.locator('div#tree-teamspaces > button')
|
||||
this.inputModalNewTeamspaceTitle = page.locator(
|
||||
'form[id="document:string:NewTeamspace"] input[placeholder="New teamspace"]'
|
||||
)
|
||||
this.inputModalNewTeamspaceDescription = page.locator('form[id="document:string:NewTeamspace"] div.tiptap')
|
||||
this.inputModalNewTeamspacePrivate = page.locator(
|
||||
'form[id="document:string:NewTeamspace"] div.antiGrid label.toggle'
|
||||
)
|
||||
this.buttonModalNewTeamspaceCreate = page.locator('form[id="document:string:NewTeamspace"] button[type="submit"]')
|
||||
this.buttonModalEditTeamspaceTitle = page.locator('form[id="document:string:EditTeamspace"] input[type="text"]')
|
||||
this.buttonModalEditTeamspaceDescription = page.locator('form[id="document:string:EditTeamspace"] div.tiptap')
|
||||
this.buttonModalEditTeamspacePrivate = page.locator(
|
||||
'form[id="document:string:EditTeamspace"] div.antiGrid label.toggle'
|
||||
)
|
||||
this.buttonModalEditTeamspaceSave = page.locator('form[id="document:string:EditTeamspace"] button[type="submit"]')
|
||||
this.buttonModalEditTeamspaceClose = page.locator('form[id="document:string:EditTeamspace"] button#card-close')
|
||||
this.inputModalNewTeamspaceTitle = newForm.locator('div[id="teamspace-title"] input')
|
||||
this.inputModalNewTeamspaceDescription = newForm.locator('div[id="teamspace-description"] input')
|
||||
this.inputModalNewTeamspacePrivate = newForm.locator('div.antiGrid label.toggle')
|
||||
this.buttonModalNewTeamspaceCreate = newForm.locator('button[type="submit"]')
|
||||
|
||||
this.buttonModalEditTeamspaceTitle = editForm.locator('div[id="teamspace-title"] input')
|
||||
this.buttonModalEditTeamspaceDescription = editForm.locator('div[id="teamspace-description"] input')
|
||||
this.buttonModalEditTeamspacePrivate = editForm.locator('div.antiGrid label.toggle')
|
||||
this.buttonModalEditTeamspaceSave = editForm.locator('button[type="submit"]')
|
||||
this.buttonModalEditTeamspaceClose = editForm.locator('button#card-close')
|
||||
}
|
||||
|
||||
async createNewTeamspace (data: NewTeamspace): Promise<void> {
|
||||
@ -117,7 +115,7 @@ export class DocumentsPage extends CommonPage {
|
||||
async checkTeamspace (data: NewTeamspace): Promise<void> {
|
||||
await expect(this.buttonModalEditTeamspaceTitle).toHaveValue(data.title)
|
||||
if (data.description != null) {
|
||||
await expect(this.buttonModalEditTeamspaceDescription).toHaveText(data.description)
|
||||
await expect(this.buttonModalEditTeamspaceDescription).toHaveValue(data.description)
|
||||
}
|
||||
await this.buttonModalEditTeamspaceClose.click()
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user