UBERF-4795 Make editor extensions kit configurable (#4329)

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-01-10 17:51:05 +07:00 committed by GitHub
parent e6ae8703b3
commit b84aa01534
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 388 additions and 221 deletions

1
.vscode/launch.json vendored
View File

@ -90,6 +90,7 @@
"SECRET": "secret", "SECRET": "secret",
"METRICS_CONSOLE": "true", "METRICS_CONSOLE": "true",
"TRANSACTOR_URL": "ws://localhost:3333", "TRANSACTOR_URL": "ws://localhost:3333",
"UPLOAD_URL": "/files",
"MONGO_URL": "mongodb://localhost:27017", "MONGO_URL": "mongodb://localhost:27017",
"MINIO_ACCESS_KEY": "minioadmin", "MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin", "MINIO_SECRET_KEY": "minioadmin",

View File

@ -707,6 +707,9 @@ dependencies:
'@tiptap/extension-highlight': '@tiptap/extension-highlight':
specifier: ^2.1.12 specifier: ^2.1.12
version: 2.1.12(@tiptap/core@2.1.12) version: 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-history':
specifier: ^2.1.12
version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-link': '@tiptap/extension-link':
specifier: ^2.1.12 specifier: ^2.1.12
version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
@ -23650,7 +23653,7 @@ packages:
dev: false dev: false
file:projects/text-editor.tgz(@types/node@16.11.68)(bufferutil@4.0.7)(esbuild@0.16.17)(postcss-load-config@4.0.1)(postcss@8.4.31)(prosemirror-model@1.19.3)(ts-node@10.9.1): file:projects/text-editor.tgz(@types/node@16.11.68)(bufferutil@4.0.7)(esbuild@0.16.17)(postcss-load-config@4.0.1)(postcss@8.4.31)(prosemirror-model@1.19.3)(ts-node@10.9.1):
resolution: {integrity: sha512-OTmONKS1vIIvQHrDVFHNsaVZopEiXGbimLMlkHpMj1sznAdusJITPsPbj9Q3YLvIc8VXNSlQeRzN2QlkwW2IYg==, tarball: file:projects/text-editor.tgz} resolution: {integrity: sha512-QgUDUW3v5zIVHETcfC/UsoZ8EPeauNcSyGO+I/J6rXLG2ndtNKIWRdl7Herj480/Q6JjPjWmm5RTIRAnz8lZXQ==, tarball: file:projects/text-editor.tgz}
id: file:projects/text-editor.tgz id: file:projects/text-editor.tgz
name: '@rush-temp/text-editor' name: '@rush-temp/text-editor'
version: 0.0.0 version: 0.0.0
@ -23665,6 +23668,7 @@ packages:
'@tiptap/extension-gapcursor': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) '@tiptap/extension-gapcursor': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-heading': 2.1.12(@tiptap/core@2.1.12) '@tiptap/extension-heading': 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-highlight': 2.1.12(@tiptap/core@2.1.12) '@tiptap/extension-highlight': 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-history': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-link': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) '@tiptap/extension-link': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-list-keymap': 2.1.12(@tiptap/core@2.1.12) '@tiptap/extension-list-keymap': 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-mention': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(@tiptap/suggestion@2.1.12) '@tiptap/extension-mention': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(@tiptap/suggestion@2.1.12)
@ -23735,7 +23739,7 @@ packages:
dev: false dev: false
file:projects/text.tgz(@types/node@16.11.68)(esbuild@0.16.17)(svelte@4.2.5)(ts-node@10.9.1): file:projects/text.tgz(@types/node@16.11.68)(esbuild@0.16.17)(svelte@4.2.5)(ts-node@10.9.1):
resolution: {integrity: sha512-mAnt4uQfrjjGcqAXJI8y+sOq5joLxJAU4l8shfLkgdv0FJY+JcR7/lnT0cg79pAEySJ3WWb3RnXlyH+TtGejJQ==, tarball: file:projects/text.tgz} resolution: {integrity: sha512-yBBmdVZ6t7OiE7WauaYvqDsgVsOUzS8W4sKslQ6EXa/gvDvnV3F/cjAMIvbH6B4w1bFOWoqgC25sqxO8HUaEOQ==, tarball: file:projects/text.tgz}
id: file:projects/text.tgz id: file:projects/text.tgz
name: '@rush-temp/text' name: '@rush-temp/text'
version: 0.0.0 version: 0.0.0
@ -23744,6 +23748,7 @@ packages:
'@tiptap/extension-gapcursor': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) '@tiptap/extension-gapcursor': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-heading': 2.1.12(@tiptap/core@2.1.12) '@tiptap/extension-heading': 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-highlight': 2.1.12(@tiptap/core@2.1.12) '@tiptap/extension-highlight': 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-history': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-link': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) '@tiptap/extension-link': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-mention': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(@tiptap/suggestion@2.1.12) '@tiptap/extension-mention': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(@tiptap/suggestion@2.1.12)
'@tiptap/extension-table': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) '@tiptap/extension-table': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)

View File

@ -103,6 +103,7 @@ services:
- COLLABORATOR_PORT=3078 - COLLABORATOR_PORT=3078
- SECRET=secret - SECRET=secret
- TRANSACTOR_URL=ws://localhost:3333 - TRANSACTOR_URL=ws://localhost:3333
- UPLOAD_URL=/files
- MONGO_URL=mongodb://mongodb:27017 - MONGO_URL=mongodb://mongodb:27017
- MINIO_ENDPOINT=minio - MINIO_ENDPOINT=minio
- MINIO_ACCESS_KEY=minioadmin - MINIO_ACCESS_KEY=minioadmin

View File

@ -60,6 +60,7 @@
"@tiptap/extension-code-block": "^2.1.12", "@tiptap/extension-code-block": "^2.1.12",
"@tiptap/extension-gapcursor": "^2.1.12", "@tiptap/extension-gapcursor": "^2.1.12",
"@tiptap/extension-heading": "^2.1.12", "@tiptap/extension-heading": "^2.1.12",
"@tiptap/extension-history": "^2.1.12",
"@tiptap/extension-table": "^2.1.12", "@tiptap/extension-table": "^2.1.12",
"@tiptap/extension-table-cell": "^2.1.12", "@tiptap/extension-table-cell": "^2.1.12",
"@tiptap/extension-table-header": "^2.1.12", "@tiptap/extension-table-header": "^2.1.12",

View File

@ -25,7 +25,7 @@
import { calculateDecorations, createYdocDocument } from './diff/decorations' import { calculateDecorations, createYdocDocument } from './diff/decorations'
import { defaultEditorAttributes } from './editor/editorProps' import { defaultEditorAttributes } from './editor/editorProps'
import { defaultExtensions } from './extensions' import { EditorKit } from '../kits/editor-kit'
export let ydoc: Ydoc export let ydoc: Ydoc
export let field: string | undefined = undefined export let field: string | undefined = undefined
@ -79,7 +79,7 @@
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, { class: 'flex-grow' }) }, editorProps: { attributes: mergeAttributes(defaultEditorAttributes, { class: 'flex-grow' }) },
element, element,
editable: false, editable: false,
extensions: [...defaultExtensions, DecorationExtension, Collaboration.configure({ document: ydoc, field })] extensions: [EditorKit, DecorationExtension, Collaboration.configure({ document: ydoc, field })]
}) })
}) })

View File

@ -100,4 +100,5 @@
on:focus on:focus
on:blur on:blur
on:update on:update
on:open-document
/> />

View File

@ -27,6 +27,7 @@
import { Completion } from '../Completion' import { Completion } from '../Completion'
import { textEditorCommandHandler } from '../commands' import { textEditorCommandHandler } from '../commands'
import { EditorKit } from '../kits/editor-kit'
import textEditorPlugin from '../plugin' import textEditorPlugin from '../plugin'
import { DocumentId, TiptapCollabProvider } from '../provider' import { DocumentId, TiptapCollabProvider } from '../provider'
import { import {
@ -47,7 +48,7 @@
import { FileAttachFunction, ImageExtension } from './extension/imageExt' import { FileAttachFunction, ImageExtension } from './extension/imageExt'
import { InlinePopupExtension } from './extension/inlinePopup' import { InlinePopupExtension } from './extension/inlinePopup'
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar' import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
import { completionConfig, defaultExtensions } from './extensions' import { completionConfig } from './extensions'
export let documentId: DocumentId export let documentId: DocumentId
export let field: string | undefined = undefined export let field: string | undefined = undefined
@ -207,7 +208,8 @@
optionalExtensions.push( optionalExtensions.push(
ImageExtension.configure({ ImageExtension.configure({
inline: true, inline: true,
attachFile attachFile,
uploadUrl: getMetadata(presentation.metadata.UploadURL)
}) })
) )
} }
@ -220,7 +222,7 @@
element, element,
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) }, editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) },
extensions: [ extensions: [
...defaultExtensions, EditorKit.configure({ history: false }),
...optionalExtensions, ...optionalExtensions,
Placeholder.configure({ placeholder: placeHolderStr }), Placeholder.configure({ placeholder: placeHolderStr }),
InlineStyleToolbarExtension.configure({ InlineStyleToolbarExtension.configure({
@ -259,7 +261,7 @@
dispatch('open-document', { event, _id, _class }) dispatch('open-document', { event, _id, _class })
} }
}), }),
EmojiExtension.configure(), EmojiExtension,
...extensions ...extensions
], ],
parseOptions: { parseOptions: {

View File

@ -20,10 +20,13 @@
import { DecorationSet } from '@tiptap/pm/view' import { DecorationSet } from '@tiptap/pm/view'
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte'
import { Markup } from '@hcengineering/core' import { Markup } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { calculateDecorations, createMarkupDocument } from './diff/decorations' import { calculateDecorations, createMarkupDocument } from './diff/decorations'
import { defaultEditorAttributes } from './editor/editorProps' import { defaultEditorAttributes } from './editor/editorProps'
import { defaultExtensions } from './extensions' import { ImageExtension } from './extension/imageExt'
import { EditorKit } from '../kits/editor-kit'
export let content: Markup export let content: Markup
export let comparedVersion: Markup | undefined = undefined export let comparedVersion: Markup | undefined = undefined
@ -78,7 +81,13 @@
element, element,
content, content,
editable: false, editable: false,
extensions: [...defaultExtensions, DecorationExtension], extensions: [
EditorKit,
ImageExtension.configure({
uploadUrl: getMetadata(presentation.metadata.UploadURL)
}),
DecorationExtension
],
onTransaction: () => { onTransaction: () => {
// force re-render so `editor.isActive` works as expected // force re-render so `editor.isActive` works as expected
editor = editor editor = editor

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { IntlString } from '@hcengineering/platform' import { IntlString, getMetadata } from '@hcengineering/platform'
import presentation, { MessageViewer } from '@hcengineering/presentation' import presentation, { MessageViewer } from '@hcengineering/presentation'
import { import {
ActionIcon, ActionIcon,
@ -175,7 +175,8 @@
attachFile, attachFile,
reportNode: (id, node) => { reportNode: (id, node) => {
attachments.set(id, node) attachments.set(id, node)
} },
uploadUrl: getMetadata(presentation.metadata.UploadURL)
}) })
const completionPlugin = Completion.configure({ const completionPlugin = Completion.configure({

View File

@ -31,7 +31,7 @@
import { InlinePopupExtension } from './extension/inlinePopup' import { InlinePopupExtension } from './extension/inlinePopup'
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar' import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
import { SubmitExtension } from './extension/submit' import { SubmitExtension } from './extension/submit'
import { defaultExtensions } from './extensions' import { EditorKit } from '../kits/editor-kit'
export let content: string = '' export let content: string = ''
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
@ -124,7 +124,7 @@
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes) }, editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes) },
content, content,
extensions: [ extensions: [
...defaultExtensions, EditorKit,
...(supportSubmit ? [Handle] : []), // order important ...(supportSubmit ? [Handle] : []), // order important
Placeholder.configure({ placeholder: placeHolderStr }), Placeholder.configure({ placeholder: placeHolderStr }),
...extensions, ...extensions,

View File

@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import { getMetadata } from '@hcengineering/platform' import { PDFViewer } from '@hcengineering/presentation'
import presentation, { PDFViewer, getFileUrl } from '@hcengineering/presentation'
import { ImageNode, type ImageOptions as ImageNodeOptions } from '@hcengineering/text' import { ImageNode, type ImageOptions as ImageNodeOptions } from '@hcengineering/text'
import { type IconSize, getIconSize2x, showPopup } from '@hcengineering/ui' import { type IconSize, getIconSize2x, showPopup } from '@hcengineering/ui'
import { mergeAttributes, nodeInputRule } from '@tiptap/core' import { mergeAttributes, nodeInputRule } from '@tiptap/core'
@ -37,6 +36,7 @@ export type ImageAlignment = 'center' | 'left' | 'right'
export interface ImageOptions extends ImageNodeOptions { export interface ImageOptions extends ImageNodeOptions {
attachFile?: FileAttachFunction attachFile?: FileAttachFunction
reportNode?: (id: string, node: ProseMirrorNode) => void reportNode?: (id: string, node: ProseMirrorNode) => void
uploadUrl: string
} }
export interface ImageAlignmentOptions { export interface ImageAlignmentOptions {
@ -80,6 +80,11 @@ function getType (type: string): 'image' | 'other' {
return 'other' return 'other'
} }
// This is a simplified version of getFileUrl from presentation plugin, which we cannot use
function getFileUrl (fileId: string, size: IconSize = 'full', uploadUrl: string): string {
return `${uploadUrl}?file=${fileId}&size=${size as string}`
}
/** /**
* @public * @public
*/ */
@ -87,7 +92,8 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
addOptions () { addOptions () {
return { return {
inline: true, inline: true,
HTMLAttributes: {} HTMLAttributes: {},
uploadUrl: ''
} }
}, },
@ -117,9 +123,11 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
HTMLAttributes HTMLAttributes
) )
const uploadUrl = this.options.uploadUrl ?? ''
const id = imgAttributes['file-id'] const id = imgAttributes['file-id']
if (id != null) { if (id != null) {
imgAttributes.src = getFileUrl(id, 'full') imgAttributes.src = getFileUrl(id, 'full', uploadUrl)
let width: IconSize | undefined let width: IconSize | undefined
switch (imgAttributes.width) { switch (imgAttributes.width) {
case '32px': case '32px':
@ -137,8 +145,9 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
break break
} }
if (width !== undefined) { if (width !== undefined) {
imgAttributes.src = getFileUrl(id, width) imgAttributes.src = getFileUrl(id, width, uploadUrl)
imgAttributes.srcset = getFileUrl(id, width) + ' 1x,' + getFileUrl(id, getIconSize2x(width)) + ' 2x' imgAttributes.srcset =
getFileUrl(id, width, uploadUrl) + ' 1x,' + getFileUrl(id, getIconSize2x(width), uploadUrl) + ' 2x'
} }
imgAttributes.class = 'text-editor-image' imgAttributes.class = 'text-editor-image'
imgAttributes.contentEditable = false imgAttributes.contentEditable = false
@ -207,8 +216,7 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
for (const uri of uris) { for (const uri of uris) {
if (uri !== '') { if (uri !== '') {
const url = new URL(uri) const url = new URL(uri)
const uploadUrl = getMetadata(presentation.metadata.UploadURL) if (opt.uploadUrl === undefined || !url.href.includes(opt.uploadUrl)) {
if (uploadUrl === undefined || !url.href.includes(uploadUrl)) {
continue continue
} }

View File

@ -1,91 +1,23 @@
import TableHeader from '@tiptap/extension-table-header' //
// 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 TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import { type Level } from '@tiptap/extension-heading'
import Highlight from '@tiptap/extension-highlight'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import Gapcursor from '@tiptap/extension-gapcursor'
import ListKeymap from '@tiptap/extension-list-keymap'
import { type AnyExtension } from '@tiptap/core'
import Link from '@tiptap/extension-link'
import Typography from '@tiptap/extension-typography'
import { type CompletionOptions } from '../Completion' import { type CompletionOptions } from '../Completion'
import MentionList from './MentionList.svelte' import MentionList from './MentionList.svelte'
import { NodeUuidExtension } from './extension/nodeUuid'
import { SvelteRenderer } from './node-view' import { SvelteRenderer } from './node-view'
import { CodemarkExtension } from './extension/codemark'
import type { SuggestionKeyDownProps, SuggestionProps } from './extension/suggestion' import type { SuggestionKeyDownProps, SuggestionProps } from './extension/suggestion'
import { Table, TableCell, TableRow } from './extension/table'
export const tableExtensions = [
Table.configure({
resizable: false,
HTMLAttributes: {
class: 'proseTable'
}
}),
TableRow.configure({}),
TableHeader.configure({}),
TableCell.configure({})
]
export const taskListExtensions = [
TaskList,
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: 'flex flex-grow gap-1 checkbox_style'
}
})
]
export const supportedHeadingLevels: Level[] = [1, 2, 3]
export const defaultExtensions: AnyExtension[] = [
StarterKit.configure({
code: {
HTMLAttributes: {
class: 'proseCode'
}
},
codeBlock: {
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'proseCodeBlock'
}
},
heading: {
levels: supportedHeadingLevels,
HTMLAttributes: {
class: 'proseHeading'
}
}
}),
CodemarkExtension,
Highlight.configure({
multicolor: false
}),
Underline.configure({}),
Typography.configure({}),
Gapcursor,
Link.configure({
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
}),
ListKeymap.configure({}),
NodeUuidExtension,
...tableExtensions
// ...taskListExtensions // Disable since tasks are not working properly now.
]
export const mInsertTable = [ export const mInsertTable = [
{ {
label: '2x2', label: '2x2',

View File

@ -0,0 +1,63 @@
//
// 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 { Extension } from '@tiptap/core'
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 StarterKit from '@tiptap/starter-kit'
export interface DefaultKitOptions {
heading?: {
levels?: Level[]
}
history?: false
}
export const DefaultKit = Extension.create<DefaultKitOptions>({
name: 'defaultKit',
addExtensions () {
return [
StarterKit.configure({
code: {
HTMLAttributes: {
class: 'proseCode'
}
},
codeBlock: {
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'proseCodeBlock'
}
},
heading: this.options.heading,
history: this.options.history
}),
Highlight.configure({
multicolor: false
}),
Typography.configure({}),
Link.configure({
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
})
]
}
})

View File

@ -0,0 +1,77 @@
//
// 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 { Extension } from '@tiptap/core'
import { type Level } from '@tiptap/extension-heading'
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'
import { CodemarkExtension } from '../components/extension/codemark'
import { NodeUuidExtension } from '../components/extension/nodeUuid'
import { Table, TableCell, TableRow } from '../components/extension/table'
const headingLevels: Level[] = [1, 2, 3]
export const tableExtensions = [
Table.configure({
resizable: false,
HTMLAttributes: {
class: 'proseTable'
}
}),
TableRow.configure({}),
TableHeader.configure({}),
TableCell.configure({})
]
export const taskListExtensions = [
TaskList,
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: 'flex flex-grow gap-1 checkbox_style'
}
})
]
export interface EditorKitOptions extends DefaultKitOptions {
history?: false
}
export const EditorKit = Extension.create<EditorKitOptions>({
name: 'defaultKit',
addExtensions () {
return [
DefaultKit.configure({
...this.options,
heading: {
levels: headingLevels
}
}),
CodemarkExtension,
Underline,
ListKeymap,
NodeUuidExtension,
...tableExtensions
// ...taskListExtensions // Disable since tasks are not working properly now.
]
}
})

View File

@ -37,6 +37,7 @@
"@tiptap/extension-gapcursor": "^2.1.12", "@tiptap/extension-gapcursor": "^2.1.12",
"@tiptap/extension-heading": "^2.1.12", "@tiptap/extension-heading": "^2.1.12",
"@tiptap/extension-highlight": "^2.1.12", "@tiptap/extension-highlight": "^2.1.12",
"@tiptap/extension-history": "^2.1.12",
"@tiptap/extension-link": "^2.1.12", "@tiptap/extension-link": "^2.1.12",
"@tiptap/extension-mention": "^2.1.12", "@tiptap/extension-mention": "^2.1.12",
"@tiptap/extension-table": "^2.1.12", "@tiptap/extension-table": "^2.1.12",

View File

@ -1,106 +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 { AnyExtension } from '@tiptap/core'
import { Level } from '@tiptap/extension-heading'
import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link'
import Table from '@tiptap/extension-table'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import Typography from '@tiptap/extension-typography'
import StarterKit from '@tiptap/starter-kit'
import { ImageNode, ReferenceNode, TodoItemNode, TodoListNode } from './nodes'
/**
* @public
*/
export const headingLevels: Level[] = [1, 2, 3, 4, 5, 6]
/**
* @public
*/
export const tableExtensions = [
Table.configure({
resizable: false,
HTMLAttributes: {
class: 'proseTable'
}
}),
TableRow.configure({}),
TableHeader.configure({}),
TableCell.configure({})
]
/**
* @public
*/
export const taskListExtensions = [
TaskList,
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: 'flex flex-grow gap-1 checkbox_style'
}
})
]
/**
* @public
*/
export const defaultExtensions: AnyExtension[] = [
StarterKit.configure({
code: {
HTMLAttributes: {
class: 'proseCode'
}
},
codeBlock: {
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'proseCodeBlock'
}
},
heading: {
levels: headingLevels
}
}),
Highlight.configure({
multicolor: false
}),
Typography.configure({}),
Link.configure({
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
}),
...tableExtensions,
...taskListExtensions
]
/**
* @public
*/
export const serverExtensions: AnyExtension[] = [
...defaultExtensions,
ImageNode,
ReferenceNode,
TodoItemNode,
TodoListNode
]

View File

@ -17,7 +17,10 @@ import { Extensions, getSchema } from '@tiptap/core'
import { generateJSON, generateHTML } from '@tiptap/html' import { generateJSON, generateHTML } from '@tiptap/html'
import { Node as ProseMirrorNode } from '@tiptap/pm/model' import { Node as ProseMirrorNode } from '@tiptap/pm/model'
import { defaultExtensions } from './extensions' import { ServerKit } from './kits/server-kit'
const defaultExtensions = [ServerKit]
/** /**
* @public * @public
*/ */

View File

@ -13,8 +13,10 @@
// limitations under the License. // limitations under the License.
// //
export * from './extensions'
export * from './html' export * from './html'
export * from './node' export * from './node'
export * from './nodes' export * from './nodes'
export * from './text' export * from './text'
export * from './kits/default-kit'
export * from './kits/server-kit'

View File

@ -0,0 +1,63 @@
//
// 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 { Extension } from '@tiptap/core'
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 StarterKit from '@tiptap/starter-kit'
export interface DefaultKitOptions {
heading?: {
levels?: Level[]
}
history?: false
}
export const DefaultKit = Extension.create<DefaultKitOptions>({
name: 'defaultKit',
addExtensions () {
return [
StarterKit.configure({
code: {
HTMLAttributes: {
class: 'proseCode'
}
},
codeBlock: {
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'proseCodeBlock'
}
},
heading: this.options.heading,
history: this.options.history
}),
Highlight.configure({
multicolor: false
}),
Typography.configure({}),
Link.configure({
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
})
]
}
})

View File

@ -0,0 +1,78 @@
//
// 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 { Extension } from '@tiptap/core'
import Table from '@tiptap/extension-table'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import { ImageNode, ImageOptions } from '../nodes/image'
import { ReferenceNode } from '../nodes/reference'
import { TodoItemNode, TodoListNode } from '../nodes/todo'
import { DefaultKit, DefaultKitOptions } from './default-kit'
import { Level } from '@tiptap/extension-heading'
const headingLevels: Level[] = [1, 2, 3, 4, 5, 6]
const tableExtensions = [
Table.configure({
resizable: false,
HTMLAttributes: {
class: 'proseTable'
}
}),
TableRow.configure({}),
TableHeader.configure({}),
TableCell.configure({})
]
const taskListExtensions = [
TaskList,
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: 'flex flex-grow gap-1 checkbox_style'
}
})
]
export interface ServerKitOptions extends DefaultKitOptions {
image: Partial<ImageOptions>
}
export const ServerKit = Extension.create<ServerKitOptions>({
name: 'serverKit',
addExtensions () {
return [
DefaultKit.configure({
...this.options,
heading: {
levels: headingLevels
}
}),
...tableExtensions,
...taskListExtensions,
ImageNode.configure(this.options.image),
TodoItemNode,
TodoListNode,
ReferenceNode
]
}
})

View File

@ -21,6 +21,12 @@ import { getDataAttribute } from './utils'
export interface ImageOptions { export interface ImageOptions {
inline: boolean inline: boolean
HTMLAttributes: Record<string, any> HTMLAttributes: Record<string, any>
uploadUrl?: string
}
// This is a simplified version of getFileUrl from presentation plugin, which we cannot use
function getFileUrl (uploadUrl: string, fileId: string, size: string = 'full'): string {
return `${uploadUrl}?file=${fileId}&size=${size}`
} }
/** /**
@ -32,7 +38,8 @@ export const ImageNode = Node.create<ImageOptions>({
addOptions () { addOptions () {
return { return {
inline: true, inline: true,
HTMLAttributes: {} HTMLAttributes: {},
uploadUrl: ''
} }
}, },
@ -98,6 +105,12 @@ export const ImageNode = Node.create<ImageOptions>({
HTMLAttributes HTMLAttributes
) )
const fileId = imgAttributes['file-id']
if (fileId != null) {
const uploadUrl = this.options.uploadUrl ?? ''
imgAttributes.src = getFileUrl(uploadUrl, fileId)
}
return ['div', divAttributes, ['img', imgAttributes]] return ['div', divAttributes, ['img', imgAttributes]]
} }
}) })

View File

@ -15,9 +15,9 @@
import chunter, { Backlink } from '@hcengineering/chunter' import chunter, { Backlink } from '@hcengineering/chunter'
import { Class, Data, Doc, Ref, Tx, TxFactory } from '@hcengineering/core' import { Class, Data, Doc, Ref, Tx, TxFactory } from '@hcengineering/core'
import { extractReferences, getHTML, parseHTML, serverExtensions } from '@hcengineering/text' import { ServerKit, extractReferences, getHTML, parseHTML } from '@hcengineering/text'
const extensions = serverExtensions const extensions = [ServerKit]
export function getBacklinks ( export function getBacklinks (
backlinkId: Ref<Doc>, backlinkId: Ref<Doc>,

View File

@ -26,6 +26,7 @@ export interface Config {
TransactorUrl: string TransactorUrl: string
MongoUrl: string MongoUrl: string
UploadUrl: string
MinioEndpoint: string MinioEndpoint: string
MinioAccessKey: string MinioAccessKey: string
@ -39,6 +40,7 @@ const envMap: { [key in keyof Config]: string } = {
Port: 'COLLABORATOR_PORT', Port: 'COLLABORATOR_PORT',
TransactorUrl: 'TRANSACTOR_URL', TransactorUrl: 'TRANSACTOR_URL',
MongoUrl: 'MONGO_URL', MongoUrl: 'MONGO_URL',
UploadUrl: 'UPLOAD_URL',
MinioEndpoint: 'MINIO_ENDPOINT', MinioEndpoint: 'MINIO_ENDPOINT',
MinioAccessKey: 'MINIO_ACCESS_KEY', MinioAccessKey: 'MINIO_ACCESS_KEY',
MinioSecretKey: 'MINIO_SECRET_KEY' MinioSecretKey: 'MINIO_SECRET_KEY'
@ -63,6 +65,7 @@ const config: Config = (() => {
Port: parseInt(process.env[envMap.Port] ?? '3078'), Port: parseInt(process.env[envMap.Port] ?? '3078'),
TransactorUrl: process.env[envMap.TransactorUrl], TransactorUrl: process.env[envMap.TransactorUrl],
MongoUrl: process.env[envMap.MongoUrl], MongoUrl: process.env[envMap.MongoUrl],
UploadUrl: process.env[envMap.UploadUrl] ?? '/files',
MinioEndpoint: process.env[envMap.MinioEndpoint], MinioEndpoint: process.env[envMap.MinioEndpoint],
MinioAccessKey: process.env[envMap.MinioAccessKey], MinioAccessKey: process.env[envMap.MinioAccessKey],
MinioSecretKey: process.env[envMap.MinioSecretKey] MinioSecretKey: process.env[envMap.MinioSecretKey]

View File

@ -15,7 +15,7 @@
import { MeasureContext } from '@hcengineering/core' import { MeasureContext } from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio' import { MinioService } from '@hcengineering/minio'
import { serverExtensions } from '@hcengineering/text' import { ServerKit } from '@hcengineering/text'
import { Hocuspocus, onAuthenticatePayload } from '@hocuspocus/server' import { Hocuspocus, onAuthenticatePayload } from '@hocuspocus/server'
import bp from 'body-parser' import bp from 'body-parser'
import compression from 'compression' import compression from 'compression'
@ -72,6 +72,14 @@ export async function start (
}) })
) )
const extensions = [
ServerKit.configure({
image: {
uploadUrl: config.UploadUrl
}
})
]
const extensionsCtx = ctx.newChild('extensions', {}) const extensionsCtx = ctx.newChild('extensions', {})
const storageCtx = ctx.newChild('storage', {}) const storageCtx = ctx.newChild('storage', {})
@ -111,7 +119,7 @@ export async function start (
extensions: [ extensions: [
new ActionsExtension({ new ActionsExtension({
ctx: extensionsCtx.newChild('actions', {}), ctx: extensionsCtx.newChild('actions', {}),
transformer: new HtmlTransformer(serverExtensions) transformer: new HtmlTransformer(extensions)
}), }),
new StorageExtension({ new StorageExtension({
ctx: extensionsCtx.newChild('storage', {}), ctx: extensionsCtx.newChild('storage', {}),
@ -121,12 +129,12 @@ export async function start (
mongodb: new MongodbStorageAdapter( mongodb: new MongodbStorageAdapter(
storageCtx.newChild('mongodb', {}), storageCtx.newChild('mongodb', {}),
mongo, mongo,
new HtmlTransformer(serverExtensions) new HtmlTransformer(extensions)
), ),
platform: new PlatformStorageAdapter( platform: new PlatformStorageAdapter(
storageCtx.newChild('platform', {}), storageCtx.newChild('platform', {}),
config.TransactorUrl, config.TransactorUrl,
new HtmlTransformer(serverExtensions) new HtmlTransformer(extensions)
) )
}, },
'minio' 'minio'

View File

@ -1,9 +1,9 @@
import { MeasureContext, WorkspaceId } from '@hcengineering/core' import { MeasureContext, WorkspaceId } from '@hcengineering/core'
import { ContentTextAdapter } from '@hcengineering/server-core' import { ContentTextAdapter } from '@hcengineering/server-core'
import { getText, serverExtensions, yDocContentToNodes } from '@hcengineering/text' import { ServerKit, getText, yDocContentToNodes } from '@hcengineering/text'
import { Readable } from 'stream' import { Readable } from 'stream'
const extensions = serverExtensions const extensions = [ServerKit]
/** /**
* @public * @public

View File

@ -111,6 +111,7 @@ services:
- COLLABORATOR_PORT=3078 - COLLABORATOR_PORT=3078
- SECRET=secret - SECRET=secret
- TRANSACTOR_URL=ws://localhost:3334 - TRANSACTOR_URL=ws://localhost:3334
- UPLOAD_URL=/files
- MONGO_URL=mongodb://mongodb:27018 - MONGO_URL=mongodb://mongodb:27018
- MINIO_ENDPOINT=minio - MINIO_ENDPOINT=minio
- MINIO_ACCESS_KEY=minioadmin - MINIO_ACCESS_KEY=minioadmin