From 6747131e23fd42e929ae9fd1d2f6db47c38d24ea Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Mon, 27 Apr 2026 15:55:53 +0700 Subject: [PATCH 01/24] Fix link action (#10793) * Fix link action Signed-off-by: Artem Savchenko * Clean up Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko --- .../src/components/extension/inlineComment.ts | 2 ++ .../src/components/extension/note.ts | 2 ++ .../src/components/extension/shortcuts/linkKeymap.ts | 3 +++ .../src/components/extension/toolbar/toolbar.ts | 9 +++++++++ plugins/text-editor-resources/src/utils.ts | 2 ++ 5 files changed, 18 insertions(+) diff --git a/plugins/text-editor-resources/src/components/extension/inlineComment.ts b/plugins/text-editor-resources/src/components/extension/inlineComment.ts index ca312793a5..6bd502ee43 100644 --- a/plugins/text-editor-resources/src/components/extension/inlineComment.ts +++ b/plugins/text-editor-resources/src/components/extension/inlineComment.ts @@ -34,6 +34,7 @@ import tippy, { type Instance } from 'tippy.js' import 'tippy.js/animations/shift-toward.css' import { type Doc as YDoc, type Map as YMap } from 'yjs' import { SvelteRenderer } from '../node-view' +import { hideTextEditorToolbar } from './toolbar/toolbar' export interface InlineCommentExtensionOptions { boundary?: HTMLElement @@ -946,6 +947,7 @@ function getReferenceRect (view: EditorView, from: number, to: number): DOMRect } export async function createInlineComment (editor: Editor, event: MouseEvent): Promise { + hideTextEditorToolbar(editor) editor.view.dispatch(setMeta(editor.state.tr, { newCommentRequested: true })) } diff --git a/plugins/text-editor-resources/src/components/extension/note.ts b/plugins/text-editor-resources/src/components/extension/note.ts index 28d1a1874d..65ec85e16f 100644 --- a/plugins/text-editor-resources/src/components/extension/note.ts +++ b/plugins/text-editor-resources/src/components/extension/note.ts @@ -7,6 +7,7 @@ import { NoteBaseExtension, type NoteKind, name as noteName } from '@hcengineeri import ConfigureNotePopup from '../note/ConfigureNotePopup.svelte' import DisplayNotePopup from '../note/DisplayNotePopup.svelte' +import { hideTextEditorToolbar } from './toolbar/toolbar' export interface NoteOptions { readonly: boolean @@ -195,6 +196,7 @@ export function displayNote (text: string, event: MouseEvent): void { } export async function configureNote (editor: Editor, event: MouseEvent): Promise { + hideTextEditorToolbar(editor) const attributes = editor.getAttributes(noteName) const text = attributes.title const kind = attributes.kind diff --git a/plugins/text-editor-resources/src/components/extension/shortcuts/linkKeymap.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/linkKeymap.ts index 30e2aacf88..2dda211bb9 100644 --- a/plugins/text-editor-resources/src/components/extension/shortcuts/linkKeymap.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/linkKeymap.ts @@ -15,6 +15,8 @@ import { showPopup } from '@hcengineering/ui' import { Extension } from '@tiptap/core' + +import { hideTextEditorToolbar } from '../toolbar/toolbar' import { type MarkType } from '@tiptap/pm/model' import { Plugin, PluginKey } from '@tiptap/pm/state' import LinkPopup from '../../LinkPopup.svelte' @@ -30,6 +32,7 @@ export const LinkKeymapExtension = Extension.create({ const link = this.editor.getAttributes('link').href + hideTextEditorToolbar(this.editor) showPopup(LinkPopup, { link }, undefined, undefined, (newLink) => { if (newLink === '') { this.editor.chain().focus().extendMarkRange('link').unsetLink().run() diff --git a/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts b/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts index 30e63c3b37..136ac9adbd 100644 --- a/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts +++ b/plugins/text-editor-resources/src/components/extension/toolbar/toolbar.ts @@ -475,6 +475,15 @@ export function getToolbarCursor (editorState: EditorState): ToolbarCursor return state?.cursor ?? null } +/** Dismiss the floating text toolbar (e.g. before opening a popup so it does not cover it). */ +export function hideTextEditorToolbar (editor: Editor): void { + const pluginState = getToolbarControlPluginState(editor.state) + if (pluginState?.cursor == null) { + return + } + editor.view.dispatch(updateCursor(editor.state.tr, null, pluginState, 'hide-for-popup')) +} + function resolveCursor (props: ResolveCursorProps): ToolbarCursor | null { const providers = props.state.providers diff --git a/plugins/text-editor-resources/src/utils.ts b/plugins/text-editor-resources/src/utils.ts index e01ea34e1d..8e0373d4a4 100644 --- a/plugins/text-editor-resources/src/utils.ts +++ b/plugins/text-editor-resources/src/utils.ts @@ -17,6 +17,7 @@ import { SelectPopup, showPopup, type PopupAlignment } from '@hcengineering/ui' import { type Editor, type Attribute } from '@tiptap/core' import { type ActionContext } from '@hcengineering/text-editor' +import { hideTextEditorToolbar } from './components/extension/toolbar/toolbar' import { mInsertTable } from './components/extensions' import LinkPopup from './components/LinkPopup.svelte' @@ -76,6 +77,7 @@ export async function isHeadingVisible (editor: Editor, ctx: ActionContext): Pro } export async function formatLink (editor: Editor): Promise { + hideTextEditorToolbar(editor) const link = editor.getAttributes('link').href // give editor some time to handle blur event From 1f239235c0f0c9d25a41f984fae5d143f21c498c Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Mon, 27 Apr 2026 20:29:29 +0500 Subject: [PATCH 02/24] card/process updating import (#10805) Signed-off-by: Denis Bykhov --- plugins/card-resources/src/exporter.ts | 11 ++++++++++- plugins/process-resources/src/exporter.ts | 24 +++++++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/plugins/card-resources/src/exporter.ts b/plugins/card-resources/src/exporter.ts index d0f5ae3336..e40d13b655 100644 --- a/plugins/card-resources/src/exporter.ts +++ b/plugins/card-resources/src/exporter.ts @@ -16,6 +16,7 @@ import core, { import { getResource } from '@hcengineering/platform' import { getClient } from '@hcengineering/presentation' import view from '@hcengineering/view' +import { deepEqual } from 'fast-equals' import card from './plugin' export async function importModule (json: string): Promise { @@ -27,7 +28,15 @@ export async function importModule (json: string): Promise { const h = client.getHierarchy() const apply = client.apply('Import') for (const elem of data) { - if (m.findObject(elem._id) !== undefined) continue + const existing = m.findObject(elem._id) + if (existing !== undefined) { + const newData = stripData(elem) + const oldData = stripData(existing) + if (!deepEqual(newData, oldData)) { + await apply.updateDoc(elem._class, existing.space, elem._id, newData) + } + continue + } if (h.isDerived(elem._class, core.class.AttachedDoc)) { const attachedDoc = elem as AttachedDoc await apply.addCollection( diff --git a/plugins/process-resources/src/exporter.ts b/plugins/process-resources/src/exporter.ts index 533f6817c8..42d1bdfea3 100644 --- a/plugins/process-resources/src/exporter.ts +++ b/plugins/process-resources/src/exporter.ts @@ -4,7 +4,6 @@ import core, { type Doc, type Ref, type Class, - generateId, type EnumOf, type RefTo, type ArrOf, @@ -13,6 +12,7 @@ import core, { type Type } from '@hcengineering/core' import { getClient } from '@hcengineering/presentation' +import { deepEqual } from 'fast-equals' import { type Process, type Transition } from '@hcengineering/process' import processPlugin from './plugin' @@ -131,15 +131,27 @@ function processParams ( export async function importProcess (masterTag: Ref, json: string): Promise { try { - const data = JSON.parse(json) as Doc[] - if (data[0] === undefined) return + const rawData = JSON.parse(json) as Doc[] + const data = Array.from(new Map(rawData.map((d) => [d._id, d])).values()) + if (data.length === 0) return const client = getClient() const m = client.getModel() const apply = client.apply('Import process') - ;(data[0] as any).masterTag = masterTag - ;(data[0] as any)._id = generateId() for (const elem of data) { - if (m.findObject(elem._id) !== undefined) continue + if (elem._class === processPlugin.class.Process) { + ;(elem as any).masterTag = masterTag + } + } + for (const elem of data) { + const existing = m.findObject(elem._id) + if (existing !== undefined) { + const newData = stripData(elem) + const oldData = stripData(existing) + if (!deepEqual(newData, oldData)) { + await apply.updateDoc(elem._class, existing.space, elem._id, newData) + } + continue + } await apply.createDoc(elem._class, core.space.Model, stripData(elem), elem._id) } await apply.commit(true) From 37f5f77f1ac61f5a343196a7cb6f08800a55abcf Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Tue, 28 Apr 2026 19:21:10 +0500 Subject: [PATCH 03/24] feat: add StringFromEnum and EnumFromString process functions (#10806) Signed-off-by: Denis Bykhov --- models/process/src/functions.ts | 26 +++++++++++++++++++ models/server-process/src/index.ts | 8 ++++++ plugins/process-assets/lang/cs.json | 4 ++- plugins/process-assets/lang/de.json | 4 ++- plugins/process-assets/lang/en.json | 4 ++- plugins/process-assets/lang/es.json | 4 ++- plugins/process-assets/lang/fr.json | 4 ++- plugins/process-assets/lang/it.json | 4 ++- plugins/process-assets/lang/ja.json | 4 ++- plugins/process-assets/lang/pt-br.json | 4 ++- plugins/process-assets/lang/pt.json | 4 ++- plugins/process-assets/lang/ru.json | 4 ++- plugins/process-assets/lang/tr.json | 4 ++- plugins/process-assets/lang/zh.json | 4 ++- plugins/process-resources/src/plugin.ts | 4 ++- plugins/process/src/index.ts | 2 ++ server-plugins/process-resources/src/index.ts | 8 ++++-- .../process-resources/src/transform.ts | 9 +++++++ server-plugins/process/src/index.ts | 4 ++- 19 files changed, 93 insertions(+), 16 deletions(-) diff --git a/models/process/src/functions.ts b/models/process/src/functions.ts index f01223ee7b..ba7c6f932f 100644 --- a/models/process/src/functions.ts +++ b/models/process/src/functions.ts @@ -756,4 +756,30 @@ export function defineFunctions (builder: Builder): void { }, process.function.DayFromDate ) + + builder.createDoc( + process.class.ProcessFunction, + core.space.Model, + { + of: core.class.EnumOf, + to: core.class.TypeString, + category: 'attribute', + label: process.string.TextFromSelect, + type: 'convert' + }, + process.function.StringFromEnum + ) + + builder.createDoc( + process.class.ProcessFunction, + core.space.Model, + { + of: core.class.TypeString, + to: core.class.EnumOf, + category: 'attribute', + label: process.string.SelectFromText, + type: 'convert' + }, + process.function.EnumFromString + ) } diff --git a/models/server-process/src/index.ts b/models/server-process/src/index.ts index ad4b12c79d..dafae6227b 100644 --- a/models/server-process/src/index.ts +++ b/models/server-process/src/index.ts @@ -191,6 +191,14 @@ export function createModel (builder: Builder): void { func: serverProcess.transform.MarkupFromString }) + builder.mixin(process.function.StringFromEnum, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, { + func: serverProcess.transform.StringFromEnum + }) + + builder.mixin(process.function.EnumFromString, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, { + func: serverProcess.transform.EnumFromString + }) + builder.mixin(process.function.NumberFromDate, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, { func: serverProcess.transform.NumberFromDate }) diff --git a/plugins/process-assets/lang/cs.json b/plugins/process-assets/lang/cs.json index a8baf9cd3e..42bda02948 100644 --- a/plugins/process-assets/lang/cs.json +++ b/plugins/process-assets/lang/cs.json @@ -168,7 +168,9 @@ "Min": "Min", "Max": "Max", "MarkupFromText": "MarkUp z textu", - "TextFromMarkup": "Text z MarkUp" + "TextFromMarkup": "Text z MarkUp", + "TextFromSelect": "Text z výběru", + "SelectFromText": "Výběr z textu" }, "error": { "MethodNotFound": "Metoda nenalezena: {methodId}", diff --git a/plugins/process-assets/lang/de.json b/plugins/process-assets/lang/de.json index 0206cb16a9..087e57e1e4 100644 --- a/plugins/process-assets/lang/de.json +++ b/plugins/process-assets/lang/de.json @@ -168,7 +168,9 @@ "Min": "Min", "Max": "Max", "TextFromMarkup": "Text aus Markup", - "MarkupFromText": "Markup aus Text" + "MarkupFromText": "Markup aus Text", + "TextFromSelect": "Text aus Auswahl", + "SelectFromText": "Auswahl aus Text" }, "error": { "MethodNotFound": "Methode nicht gefunden: {methodId}", diff --git a/plugins/process-assets/lang/en.json b/plugins/process-assets/lang/en.json index f23853530f..9be999f900 100644 --- a/plugins/process-assets/lang/en.json +++ b/plugins/process-assets/lang/en.json @@ -173,7 +173,9 @@ "DateDifference": "Date difference", "EmptyValue": "Empty value", "TextFromMarkup": "Text from markup", - "MarkupFromText": "Markup from text" + "MarkupFromText": "Markup from text", + "TextFromSelect": "Text from select", + "SelectFromText": "Select from text" }, "error": { "MethodNotFound": "Method not found: {methodId}", diff --git a/plugins/process-assets/lang/es.json b/plugins/process-assets/lang/es.json index df88749535..3f9d12bf64 100644 --- a/plugins/process-assets/lang/es.json +++ b/plugins/process-assets/lang/es.json @@ -173,7 +173,9 @@ "DateDifference": "Diferencia de fechas", "EmptyValue": "Valor vacío", "TextFromMarkup": "Texto desde markup", - "MarkupFromText": "Markup desde texto" + "MarkupFromText": "Markup desde texto", + "TextFromSelect": "Texto desde selección", + "SelectFromText": "Selección desde texto" }, "error": { "MethodNotFound": "Método no encontrado: {methodId}", diff --git a/plugins/process-assets/lang/fr.json b/plugins/process-assets/lang/fr.json index ad9b563040..47a7a0806d 100644 --- a/plugins/process-assets/lang/fr.json +++ b/plugins/process-assets/lang/fr.json @@ -173,7 +173,9 @@ "DateDifference": "Différence de dates", "EmptyValue": "Valeur vide", "TextFromMarkup": "Texte depuis markup", - "MarkupFromText": "Markup depuis texte" + "MarkupFromText": "Markup depuis texte", + "TextFromSelect": "Texte depuis sélection", + "SelectFromText": "Sélection depuis texte" }, "error": { "MethodNotFound": "Méthode introuvable : {methodId}", diff --git a/plugins/process-assets/lang/it.json b/plugins/process-assets/lang/it.json index 6dfe7e902e..0abfa19c8c 100644 --- a/plugins/process-assets/lang/it.json +++ b/plugins/process-assets/lang/it.json @@ -173,7 +173,9 @@ "DateDifference": "Differenza di date", "EmptyValue": "Valore vuoto", "TextFromMarkup": "Testo da markup", - "MarkupFromText": "Markup da testo" + "MarkupFromText": "Markup da testo", + "TextFromSelect": "Testo da selezione", + "SelectFromText": "Selezione da testo" }, "error": { "MethodNotFound": "Metodo non trovato: {methodId}", diff --git a/plugins/process-assets/lang/ja.json b/plugins/process-assets/lang/ja.json index 987f3a6c50..29aea93578 100644 --- a/plugins/process-assets/lang/ja.json +++ b/plugins/process-assets/lang/ja.json @@ -172,7 +172,9 @@ "DateDifference": "日付の差", "EmptyValue": "空の値", "TextFromMarkup": "テキストからマークアップ", - "MarkupFromText": "マークアップからテキスト" + "MarkupFromText": "マークアップからテキスト", + "TextFromSelect": "テキストから選択", + "SelectFromText": "選択からテキスト" }, "error": { "MethodNotFound": "メソッドが見つかりません: {methodId}", diff --git a/plugins/process-assets/lang/pt-br.json b/plugins/process-assets/lang/pt-br.json index 354bc9bf97..28540b8b8a 100644 --- a/plugins/process-assets/lang/pt-br.json +++ b/plugins/process-assets/lang/pt-br.json @@ -161,7 +161,9 @@ "DateDifference": "Diferença de datas", "EmptyValue": "Valor vazio", "TextFromMarkup": "Texto de marcação", - "MarkupFromText": "Marcação de texto" + "MarkupFromText": "Marcação de texto", + "TextFromSelect": "Texto de seleção", + "SelectFromText": "Seleção de texto" }, "error": { "MethodNotFound": "Método não encontrado: {methodId}", diff --git a/plugins/process-assets/lang/pt.json b/plugins/process-assets/lang/pt.json index 222160cd62..546076873f 100644 --- a/plugins/process-assets/lang/pt.json +++ b/plugins/process-assets/lang/pt.json @@ -173,7 +173,9 @@ "DateDifference": "Diferença de datas", "EmptyValue": "Valor vazio", "TextFromMarkup": "Texto de marcação", - "MarkupFromText": "Marcação de texto" + "MarkupFromText": "Marcação de texto", + "TextFromSelect": "Texto de seleção", + "SelectFromText": "Seleção de texto" }, "error": { "MethodNotFound": "Método não encontrado: {methodId}", diff --git a/plugins/process-assets/lang/ru.json b/plugins/process-assets/lang/ru.json index 5d340cff1b..33caf6ab35 100644 --- a/plugins/process-assets/lang/ru.json +++ b/plugins/process-assets/lang/ru.json @@ -173,7 +173,9 @@ "DateDifference": "Разница дат", "EmptyValue": "Пустое значение", "MarkupFromText": "Текст из разметки", - "TextFromMarkup": "Разметка из текста" + "TextFromMarkup": "Разметка из текста", + "TextFromSelect": "Текст из выбора", + "SelectFromText": "Выбор из текста" }, "error": { "MethodNotFound": "Метод не найден: {methodId}", diff --git a/plugins/process-assets/lang/tr.json b/plugins/process-assets/lang/tr.json index 997ed40e54..fed651ff77 100644 --- a/plugins/process-assets/lang/tr.json +++ b/plugins/process-assets/lang/tr.json @@ -168,7 +168,9 @@ "DateDifference": "Tarih farkı", "EmptyValue": "Boş değer", "TextFromMarkup": "Metinden işaretleme", - "MarkupFromText": "İşaretlemeden metin" + "MarkupFromText": "İşaretlemeden metin", + "TextFromSelect": "Seçimden metin", + "SelectFromText": "Metinden seçim" }, "error": { "MethodNotFound": "Metod bulunamadı: {methodId}", diff --git a/plugins/process-assets/lang/zh.json b/plugins/process-assets/lang/zh.json index 63ae4e3adb..94928bf0e9 100644 --- a/plugins/process-assets/lang/zh.json +++ b/plugins/process-assets/lang/zh.json @@ -173,7 +173,9 @@ "DateDifference": "日期差异", "EmptyValue": "空值", "TextFromMarkup": "标记到文本", - "MarkupFromText": "文本到标记" + "MarkupFromText": "文本到标记", + "TextFromSelect": "选择到文本", + "SelectFromText": "文本到选择" }, "error": { "MethodNotFound": "找不到方法:{methodId}", diff --git a/plugins/process-resources/src/plugin.ts b/plugins/process-resources/src/plugin.ts index 6c6568e0c5..652df27f95 100644 --- a/plugins/process-resources/src/plugin.ts +++ b/plugins/process-resources/src/plugin.ts @@ -279,7 +279,9 @@ export default mergeIds(processId, process, { YearFromDate: '' as IntlString, MonthFromDate: '' as IntlString, DayFromDate: '' as IntlString, - DateDifference: '' as IntlString + DateDifference: '' as IntlString, + TextFromSelect: '' as IntlString, + SelectFromText: '' as IntlString }, permission: { RunProcess: '' as Ref, diff --git a/plugins/process/src/index.ts b/plugins/process/src/index.ts index 70468f93c8..dab168fdab 100644 --- a/plugins/process/src/index.ts +++ b/plugins/process/src/index.ts @@ -390,6 +390,8 @@ export default plugin(processId, { YearFromDate: '' as Ref, MonthFromDate: '' as Ref, DayFromDate: '' as Ref, + StringFromEnum: '' as Ref, + EnumFromString: '' as Ref, DateDifference: '' as Ref, ExportProcess: '' as Resource, CheckProcessSectionVisibility: '' as Resource<(doc: Card) => Promise> diff --git a/server-plugins/process-resources/src/index.ts b/server-plugins/process-resources/src/index.ts index cfd3de8c21..238d71812d 100644 --- a/server-plugins/process-resources/src/index.ts +++ b/server-plugins/process-resources/src/index.ts @@ -134,7 +134,9 @@ import { Subtract, Trim, UpperCase, - YearFromDate + YearFromDate, + StringFromEnum, + EnumFromString } from './transform' async function putEventToQueue (value: Omit, control: TriggerControl): Promise { @@ -797,7 +799,9 @@ export default async () => ({ Min, Max, StringFromMarkup, - MarkupFromString + MarkupFromString, + StringFromEnum, + EnumFromString }, rollbacks: { ToDoCloseRollback, diff --git a/server-plugins/process-resources/src/transform.ts b/server-plugins/process-resources/src/transform.ts index 72cd866829..31806a2af8 100644 --- a/server-plugins/process-resources/src/transform.ts +++ b/server-plugins/process-resources/src/transform.ts @@ -582,4 +582,13 @@ export function MarkupFromString (value: string): string { return value } +export function StringFromEnum (value: string): string { + if (value == null) return '' + return String(value) +} + +export function EnumFromString (value: string): string { + return value +} + // #endregion diff --git a/server-plugins/process/src/index.ts b/server-plugins/process/src/index.ts index 18cc298676..83318be440 100644 --- a/server-plugins/process/src/index.ts +++ b/server-plugins/process/src/index.ts @@ -129,7 +129,9 @@ export default plugin(serverProcessId, { YearFromDate: '' as Resource, MonthFromDate: '' as Resource, DayFromDate: '' as Resource, - DateDifference: '' as Resource + DateDifference: '' as Resource, + StringFromEnum: '' as Resource, + EnumFromString: '' as Resource }, trigger: { OnTransition: '' as Resource, From 77ac9e09a38a302a81f7fdbf4a88779ac5280dbf Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Tue, 28 Apr 2026 20:25:32 +0500 Subject: [PATCH 04/24] Add left menu for styled text (#10807) Signed-off-by: Denis Bykhov --- .../src/components/StyledTextBox.svelte | 100 ++++++++++++++---- .../src/components/extension/leftMenu.ts | 18 +++- .../src/kits/editor-kit.ts | 2 +- 3 files changed, 94 insertions(+), 26 deletions(-) diff --git a/plugins/text-editor-resources/src/components/StyledTextBox.svelte b/plugins/text-editor-resources/src/components/StyledTextBox.svelte index 26c28ffd97..7bb74ef1ca 100644 --- a/plugins/text-editor-resources/src/components/StyledTextBox.svelte +++ b/plugins/text-editor-resources/src/components/StyledTextBox.svelte @@ -24,6 +24,7 @@ import { EditorKitOptions } from '../kits/editor-kit' import { addTableHandler } from '../utils' + import view from '@hcengineering/view' import { type FileAttachFunction } from './extension/types' import { inlineCommandsConfig } from './extensions' @@ -280,28 +281,46 @@ {isScrollable} {extraActions} {boundary} - kitOptions={mergeKitOptions( - { - emoji: true, - hooks: { - focus: { - onCanBlur: (value) => (canBlur = value), - onFocus: handleFocus - } - }, - shortcuts: { - imageUpload: { - attachFile, - getFileUrl - } - }, - inlineCommands: inlineCommandsConfig( - handleCommandSelected, - attachFile == null ? ['drawing-board', 'todo-list', 'image'] : ['drawing-board', 'todo-list'] - ) + kitOptions={{ + emoji: true, + textColorStyling: true, + hooks: { + focus: { + onCanBlur: (value) => (canBlur = value), + onFocus: handleFocus + } }, - kitOptions - )} + shortcuts: { + imageUpload: { + attachFile, + getFileUrl + } + }, + inlineCommands: inlineCommandsConfig( + handleCommandSelected, + attachFile == null ? ['drawing-board', 'todo-list', 'image'] : ['drawing-board', 'todo-list'] + ), + ...kitOptions, + leftMenu: { + width: 20, + height: 20, + marginX: 8, + className: 'tiptap-left-menu', + icon: view.icon.Add, + iconProps: { + className: 'svg-tiny', + fill: 'currentColor' + }, + items: [ + { id: 'image', label: textEditor.string.Image, icon: view.icon.Image }, + { id: 'table', label: textEditor.string.Table, icon: view.icon.Table2 }, + { id: 'code-block', label: textEditor.string.CodeBlock, icon: view.icon.CodeBlock }, + { id: 'separator-line', label: textEditor.string.SeparatorLine, icon: view.icon.SeparatorLine }, + { id: 'mermaid', label: textEditor.string.MermaidDiargram, icon: view.icon.Model } + ], + handleSelect: handleCommandSelected + } + }} bind:content={rawValue} bind:this={editor} on:value={(evt) => { @@ -355,6 +374,33 @@ diff --git a/plugins/text-editor-resources/src/components/extension/leftMenu.ts b/plugins/text-editor-resources/src/components/extension/leftMenu.ts index a48617b953..13e1dc54c3 100644 --- a/plugins/text-editor-resources/src/components/extension/leftMenu.ts +++ b/plugins/text-editor-resources/src/components/extension/leftMenu.ts @@ -31,6 +31,9 @@ export interface LeftMenuOptions { } function nodeDOMAtCoords (coords: { x: number, y: number }): Element | undefined { + if (!Number.isFinite(coords.x) || !Number.isFinite(coords.y)) { + return undefined + } return document .elementsFromPoint(coords.x, coords.y) .find((elem: Element) => elem.parentElement?.matches?.('.ProseMirror') === true) @@ -94,14 +97,16 @@ function LeftMenu (options: LeftMenuOptions): Plugin { const svgNs = 'http://www.w3.org/2000/svg' const icon = document.createElementNS(svgNs, 'svg') - const { className: iconClassName, ...restIconProps } = options.iconProps - icon.classList.add(iconClassName) + const { className: iconClassName, ...restIconProps } = options.iconProps ?? {} + if (iconClassName !== undefined) { + icon.classList.add(iconClassName) + } Object.entries(restIconProps).forEach(([key, value]) => { icon.setAttribute(key, value as string) }) const use = document.createElementNS(svgNs, 'use') - const href = getMetadata(options.icon) + const href = options.icon !== undefined ? getMetadata(options.icon) : undefined if (href !== undefined) { use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', href) @@ -161,7 +166,12 @@ function LeftMenu (options: LeftMenuOptions): Plugin { y: event.clientY }) - if (!(node instanceof HTMLElement) || node.nodeName === 'HR') { + if ( + !(node instanceof HTMLElement) || + node.nodeName === 'HR' || + options.items === undefined || + options.items.length === 0 + ) { hideLeftMenu() return } diff --git a/plugins/text-editor-resources/src/kits/editor-kit.ts b/plugins/text-editor-resources/src/kits/editor-kit.ts index 7429987d4f..82669655cb 100644 --- a/plugins/text-editor-resources/src/kits/editor-kit.ts +++ b/plugins/text-editor-resources/src/kits/editor-kit.ts @@ -129,7 +129,7 @@ const StaticEditorKit = extensionKit( context: getActionContext(context) }), toc: e(ToCExtension, false), - leftMenu: e(LeftMenuExtension, false), + leftMenu: e(LeftMenuExtension), inlineCommands: e(InlineCommandsExtension, false), placeholder: e(I18nPlaceholderExtension, false), editorContext: e(EditorContextExtension, { From 62f06940dfe134af7b5d68d7ab6a2fb1393bbb5c Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Tue, 28 Apr 2026 20:26:21 +0500 Subject: [PATCH 05/24] enable nested association attribute configuration (#10808) Signed-off-by: Denis Bykhov --- .../settings/view/ViewSetting.svelte | 137 ++++++++++++------ .../src/components/RelationshipTable.svelte | 32 +++- .../src/components/ViewletSetting.svelte | 126 ++++++++++------ plugins/view-resources/src/utils.ts | 110 +++++++++++++- 4 files changed, 317 insertions(+), 88 deletions(-) diff --git a/plugins/card-resources/src/components/settings/view/ViewSetting.svelte b/plugins/card-resources/src/components/settings/view/ViewSetting.svelte index a257589de9..7e9cc174d2 100644 --- a/plugins/card-resources/src/components/settings/view/ViewSetting.svelte +++ b/plugins/card-resources/src/components/settings/view/ViewSetting.svelte @@ -15,10 +15,20 @@ @@ -44,7 +46,7 @@ height={value ? 'auto' : undefined} on:click={(ev) => { if (!shown && !readonly) { - showPopup(MarkupEditorPopup, { value }, eventToHTMLElement(ev), (res) => { + showPopup(MarkupEditorPopup, { value, kitOptions }, eventToHTMLElement(ev), (res) => { if (res != null) { value = res onChange(value) @@ -69,6 +71,7 @@ content={value} {placeholder} alwaysEdit + {kitOptions} mode={2} on:value={(e) => { onChange(e.detail) diff --git a/plugins/view-resources/src/components/MarkupEditorPopup.svelte b/plugins/view-resources/src/components/MarkupEditorPopup.svelte index fa966e2acd..185a7a1987 100644 --- a/plugins/view-resources/src/components/MarkupEditorPopup.svelte +++ b/plugins/view-resources/src/components/MarkupEditorPopup.svelte @@ -16,10 +16,12 @@ + +{#if attribute} +
+
+{/if} +
+ { + if (e.detail !== undefined) { + onChange(e.detail) + } + }} + /> +
diff --git a/plugins/process-resources/src/components/contextEditors/RequestUserInput.svelte b/plugins/process-resources/src/components/contextEditors/RequestUserInput.svelte index 50140ea3bc..417e9988f4 100644 --- a/plugins/process-resources/src/components/contextEditors/RequestUserInput.svelte +++ b/plugins/process-resources/src/components/contextEditors/RequestUserInput.svelte @@ -21,6 +21,7 @@ import plugin from '../../plugin' import TransitionPresenter from '../settings/TransitionPresenter.svelte' import RequestUserInputAttribute from './RequestUserInputAttribute.svelte' + import ClassUserInput from './ClassUserInput.svelte' export let processId: Ref export let space: Ref @@ -66,16 +67,27 @@ {/if}
{#each inputs as input} - { - values[input.id] = e.detail - values = values - }} - /> + {#if input.key === '_class'} + { + values[input.id] = e.detail + values = values + }} + /> + {:else} + { + values[input.id] = e.detail + values = values + }} + /> + {/if} {/each}
diff --git a/plugins/process-resources/src/components/presenters/CreateCardPresenter.svelte b/plugins/process-resources/src/components/presenters/CreateCardPresenter.svelte index 22d65f9996..b0c1738ee4 100644 --- a/plugins/process-resources/src/components/presenters/CreateCardPresenter.svelte +++ b/plugins/process-resources/src/components/presenters/CreateCardPresenter.svelte @@ -33,9 +33,8 @@ $: contextValue = params.title !== undefined ? parseContext(params.title) : undefined $: context = getContext(client, process, core.class.TypeString, 'attribute') - $: _class = params?._class - ? (client.getHierarchy().getClass(params._class as Ref) as MasterTag) - : undefined + $: target = params?.targetClass ?? params?._class + $: _class = target !== undefined ? (client.getHierarchy().getClass(target as Ref) as MasterTag) : undefined $: icon = _class?.icon diff --git a/plugins/process-resources/src/components/settings/CreateCardEditor.svelte b/plugins/process-resources/src/components/settings/CreateCardEditor.svelte index c617b4101c..207299d069 100644 --- a/plugins/process-resources/src/components/settings/CreateCardEditor.svelte +++ b/plugins/process-resources/src/components/settings/CreateCardEditor.svelte @@ -18,10 +18,12 @@ import core, { AnyAttribute, Class, Ref } from '@hcengineering/core' import { translateCB } from '@hcengineering/platform' import presentation, { getClient } from '@hcengineering/presentation' - import { Process, Step } from '@hcengineering/process' - import { Button, eventToHTMLElement, Label, SelectPopup, showPopup, tooltip } from '@hcengineering/ui' + import { createContext, Process, Step } from '@hcengineering/process' + import { Button, eventToHTMLElement, Label, SelectPopup, showPopup, Toggle, tooltip } from '@hcengineering/ui' import { createEventDispatcher } from 'svelte' import ParamsEditor from './ParamsEditor.svelte' + import plugin from '../../plugin' + import { generateContextId } from '../../utils' export let process: Process export let step: Step @@ -29,14 +31,16 @@ const dispatch = createEventDispatcher() step.params.title = step.params.title ?? '' + step.params.targetClass = step.params.targetClass ?? step.params._class ?? process.masterTag step.params._class = step.params._class ?? process.masterTag let params = step.params - let _class: Ref> = (params._class as Ref>) ?? process.masterTag + let targetClass: Ref> = + (params.targetClass as Ref>) ?? (params._class as Ref>) ?? process.masterTag const client = getClient() - const hierarchy = client.getHierarchy() + const h = client.getHierarchy() translateCB(cardPlugin.string.Card, {}, undefined, (res) => { setName(res) @@ -59,8 +63,8 @@ } function getKeys (_class: Ref>): AnyAttribute[] { - const ignoreKeys = ['_class', 'parent', 'attachments', 'todos'] - const attributes = hierarchy.getAllAttributes(_class, core.class.Doc) + const ignoreKeys = ['_class', 'targetClass', 'parent', 'attachments', 'todos'] + const attributes = h.getAllAttributes(_class, core.class.Doc) const res: AnyAttribute[] = [] for (const [key, attr] of attributes) { if (attr.hidden === true) continue @@ -71,10 +75,10 @@ } let keys = Object.keys(params).filter((key) => { - return key !== '_class' + return key !== '_class' && key !== 'targetClass' }) - $: allAttrs = getKeys(_class) + $: allAttrs = getKeys(targetClass) $: possibleAttrs = allAttrs.filter((attr) => !keys.includes(attr.name)) function addKey (key: string): void { @@ -112,25 +116,48 @@ function typeChange (_id: Ref>): void { if (_id === undefined) return - allAttrs = getKeys(_id) + const attrKeys = ['_class', 'targetClass', ...getKeys(_id).map((p) => p.name)] const oldParams = { ...params } params = {} - for (const attr of allAttrs) { - const key = attr.name + for (const key of attrKeys) { if (oldParams[key] !== undefined) { ;(params as any)[key] = oldParams[key] } } - keys = Object.keys(params) - params._class = _class + keys = Object.keys(params).filter((key) => { + return key !== '_class' && key !== 'targetClass' + }) + if (params.targetClass !== _id) { + params._class = _id + params.targetClass = _id + } step.params = params if (step.context != null) { - step.context._class = _class + step.context._class = _id } + step = step dispatch('change', step) } - $: typeChange(_class) + $: typeChange(targetClass) + + $: subclasses = h.getDescendants(targetClass).filter((c) => !h.isMixin(c) && c !== targetClass) + + let askSubclass = params._class !== params.targetClass + $: askSubclass = params._class !== params.targetClass + + function changeAskSubclass (e: CustomEvent): void { + const context = createContext({ + type: 'userRequest', + id: generateContextId(), + _class: targetClass, + key: '_class' + }) + if (e.detail !== undefined) { + step.params._class = e.detail ? context : targetClass + dispatch('change', step) + } + }
@@ -143,18 +170,31 @@
-{#key _class} - +{#key targetClass} + {/key} {#if possibleAttrs.length > 0}
diff --git a/plugins/process-resources/src/plugin.ts b/plugins/process-resources/src/plugin.ts index 652df27f95..55d58db6b6 100644 --- a/plugins/process-resources/src/plugin.ts +++ b/plugins/process-resources/src/plugin.ts @@ -281,7 +281,8 @@ export default mergeIds(processId, process, { DayFromDate: '' as IntlString, DateDifference: '' as IntlString, TextFromSelect: '' as IntlString, - SelectFromText: '' as IntlString + SelectFromText: '' as IntlString, + AskSubclass: '' as IntlString }, permission: { RunProcess: '' as Ref, From f561318a6a1926f8040b1a50fb317c7b11879972 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Wed, 6 May 2026 15:45:22 +0500 Subject: [PATCH 17/24] integrate MarkupProperties directly into tag attribute components (#10824) Signed-off-by: Denis Bykhov --- models/card/src/index.ts | 12 ----- .../src/components/MarkupProperties.svelte | 2 +- .../src/components/MasterTagAttributes.svelte | 2 + .../src/components/TagAttributes.svelte | 2 + .../sections/MarkupPropertiesSection.svelte | 51 ------------------- plugins/card-resources/src/index.ts | 4 +- plugins/card-resources/src/plugin.ts | 1 - plugins/card/src/index.ts | 1 - 8 files changed, 6 insertions(+), 69 deletions(-) delete mode 100644 plugins/card-resources/src/components/sections/MarkupPropertiesSection.svelte diff --git a/models/card/src/index.ts b/models/card/src/index.ts index e7c21586c4..14c76397eb 100644 --- a/models/card/src/index.ts +++ b/models/card/src/index.ts @@ -1153,18 +1153,6 @@ function defineTabs (builder: Builder): void { card.section.Properties ) - builder.createDoc( - card.class.CardSection, - core.space.Model, - { - label: card.string.RichtextProperties, - component: card.sectionComponent.MarkupPropertiesSection, - order: 150, - navigation: [] - }, - card.section.MarkupProperties - ) - builder.createDoc( card.class.CardSection, core.space.Model, diff --git a/plugins/card-resources/src/components/MarkupProperties.svelte b/plugins/card-resources/src/components/MarkupProperties.svelte index e2c3ddd382..3dce494d83 100644 --- a/plugins/card-resources/src/components/MarkupProperties.svelte +++ b/plugins/card-resources/src/components/MarkupProperties.svelte @@ -63,7 +63,7 @@ {#each keys as key} -
+
+