From cc0bf8d03075eed4a756e2273ced049c9247ce9d Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 27 Mar 2026 11:04:02 +0700 Subject: [PATCH 1/7] Fix hyperlink in contoll doc comment (#10690) Signed-off-by: Artem Savchenko --- .../document/popups/AddCommentPopup.svelte | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/plugins/controlled-documents-resources/src/components/document/popups/AddCommentPopup.svelte b/plugins/controlled-documents-resources/src/components/document/popups/AddCommentPopup.svelte index f8321ce8a6..f46b22e0ce 100644 --- a/plugins/controlled-documents-resources/src/components/document/popups/AddCommentPopup.svelte +++ b/plugins/controlled-documents-resources/src/components/document/popups/AddCommentPopup.svelte @@ -22,10 +22,20 @@ let popup: HTMLDivElement | undefined + function isClickInsidePopup (target: Node): boolean { + if (popup !== undefined && popup.contains(target)) return true + if (!(target instanceof Element)) return false + + if (target.closest('.tippy-box') !== null) return true + if (target.closest('[data-block-editor-blur="true"]') !== null) return true + + return false + } + function handleClick (event: MouseEvent): void { if (event.target instanceof Node) { const top = $popups.length > 0 && $popups[$popups.length - 1].id === popupId - if (top && popup !== undefined && !popup.contains(event.target)) { + if (top && !isClickInsidePopup(event.target)) { event.preventDefault() event.stopPropagation() dispatch('close', undefined) From 371c4b1465dde5bcf64fd2a62a27db3be7ef6722 Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Fri, 27 Mar 2026 09:04:52 +0500 Subject: [PATCH 2/7] Fix todo reassign (#10693) * Correctly user reassignment target by handling mixins. Signed-off-by: Denis Bykhov * Fix formatting Signed-off-by: Denis Bykhov --------- Signed-off-by: Denis Bykhov --- server-plugins/process-resources/src/index.ts | 112 ++++++++++-------- 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/server-plugins/process-resources/src/index.ts b/server-plugins/process-resources/src/index.ts index 8de4079f78..8e9b7eb028 100644 --- a/server-plugins/process-resources/src/index.ts +++ b/server-plugins/process-resources/src/index.ts @@ -30,25 +30,53 @@ import core, { TxUpdateDoc } from '@hcengineering/core' import process, { + ApproveRequest, ContextId, Execution, + ExecutionStatus, + isUpdateTx, Method, parseContext, Process, ProcessContext, + ProcessCustomEvent, ProcessToDo, SelectedExecutionContext, State, Step, Transition, - isUpdateTx, - ProcessCustomEvent, - ApproveRequest, - ExecutionStatus, Trigger } from '@hcengineering/process' import { QueueTopic, TriggerControl } from '@hcengineering/server-core' import { ProcessMessage } from '@hcengineering/server-process' +import { + AddRelation, + AddTag, + ApproveRequestApproved, + ApproveRequestRejected, + CancelSubProcess, + CancelToDo, + CheckSubProcessesDone, + CheckSubProcessMatch, + CheckTime, + CheckToDoCancelled, + CheckToDoDone, + CreateCard, + CreateToDo, + EventCheck, + FieldChangedCheck, + LockCard, + LockField, + LockSection, + MatchCardCheck, + RequestApproval, + RunSubProcess, + UnlockCard, + UnlockField, + UnlockSection, + UpdateCard +} from './functions' +import { FieldChangedRollback, ToDoCancellRollback, ToDoCloseRollback } from './rollback' import { Absolute, Add, @@ -58,7 +86,16 @@ import { CurrentDate, CurrentUser, Cut, + DateDifference, + DateFromNumber, + DateFromString, + DayFromDate, Divide, + EmptyArray, + ExecutionInitiator, + ExecutionStarted, + Filter, + FirstMatchValue, FirstValue, FirstWorkingDayAfter, Floor, @@ -66,10 +103,12 @@ import { LastValue, LowerCase, Modulo, + MonthFromDate, Multiply, + NumberFromDate, + NumberFromString, Offset, Power, - Sqrt, Prepend, Random, Remove, @@ -80,54 +119,15 @@ import { RoleContext, Round, Split, + Sqrt, + StringFromBoolean, + StringFromDate, + StringFromNumber, Subtract, Trim, UpperCase, - EmptyArray, - ExecutionInitiator, - ExecutionStarted, - FirstMatchValue, - Filter, - StringFromNumber, - StringFromDate, - StringFromBoolean, - NumberFromDate, - DateFromNumber, - NumberFromString, - DateFromString, - YearFromDate, - MonthFromDate, - DayFromDate, - DateDifference + YearFromDate } from './transform' -import { - RunSubProcess, - CancelSubProcess, - CreateToDo, - UpdateCard, - CreateCard, - AddRelation, - AddTag, - CheckToDoDone, - CheckToDoCancelled, - MatchCardCheck, - CheckSubProcessesDone, - CheckSubProcessMatch, - CheckTime, - FieldChangedCheck, - EventCheck, - RequestApproval, - ApproveRequestApproved, - ApproveRequestRejected, - CancelToDo, - LockCard, - LockSection, - UnlockCard, - UnlockSection, - LockField, - UnlockField -} from './functions' -import { FieldChangedRollback, ToDoCancellRollback, ToDoCloseRollback } from './rollback' async function putEventToQueue (value: Omit, control: TriggerControl): Promise { if (control.queue === undefined) return @@ -374,6 +374,7 @@ async function reassignToDos (card: Card, ops: DocumentUpdate, control: Tr doneOn: null, field: { $ne: null } } as any) + const cache = new Map, Execution>() const handledGroups = new Set() for (const todo of todos as any[]) { if (todo.field === undefined || !TxProcessor.hasUpdate(ops, todo.field)) continue @@ -383,7 +384,18 @@ async function reassignToDos (card: Card, ops: DocumentUpdate, control: Tr if (handledGroups.has(request.group)) continue handledGroups.add(request.group) - const newUsers = (card[todo.field as keyof Card] as any[]) ?? [] + const execution = + cache.get(todo.execution) ?? + (await control.findAll(control.ctx, process.class.Execution, { _id: todo.execution }, { limit: 1 }))[0] + if (execution === undefined) continue + cache.set(todo.execution, execution) + const _process = control.modelDb.findObject(execution.process) + if (_process === undefined) continue + const h = control.hierarchy + + const target = h.isMixin(_process.masterTag) ? h.asIf(card, _process.masterTag) : card + if (target === undefined) continue + const newUsers = (target[todo.field as keyof Card] as any[]) ?? [] if (newUsers.length === 0) { continue } From 2b6d06521742f4d09439034fd8c5b8e37619d267 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Fri, 27 Mar 2026 11:05:23 +0700 Subject: [PATCH 3/7] Fix markdown links in table (#10688) * Fix markdown links in table Signed-off-by: Artem Savchenko * Potential fix for code scanning alert no. 294: Inefficient regular expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Artyom Savchenko * Add tests Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko Signed-off-by: Artyom Savchenko Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../src/__tests__/markdown.escape.test.ts | 28 +-------------- .../src/markdown/escape.ts | 20 ----------- .../src/markdown/tableBuilder.ts | 6 ++-- .../extension/shortcuts/smartPaste.ts | 14 ++++++-- .../extension/shortcuts/tablePaste.test.ts | 35 +++++++++++++++++++ .../extension/shortcuts/tablePaste.ts | 22 ++++++++++++ 6 files changed, 73 insertions(+), 52 deletions(-) create mode 100644 plugins/text-editor-resources/src/components/extension/shortcuts/tablePaste.test.ts diff --git a/plugins/converter-resources/src/__tests__/markdown.escape.test.ts b/plugins/converter-resources/src/__tests__/markdown.escape.test.ts index 29926dcf9d..737b943de4 100644 --- a/plugins/converter-resources/src/__tests__/markdown.escape.test.ts +++ b/plugins/converter-resources/src/__tests__/markdown.escape.test.ts @@ -13,7 +13,7 @@ // limitations under the License. // -import { escapeMarkdownLinkText, escapeMarkdownLinkUrl, escapeTableCell } from '../markdown/escape' +import { escapeMarkdownLinkText, escapeMarkdownLinkUrl } from '../markdown/escape' describe('markdown/escape', () => { describe('escapeMarkdownLinkText', () => { @@ -56,30 +56,4 @@ describe('markdown/escape', () => { expect(escapeMarkdownLinkUrl('https://example.com')).toBe('https://example.com') }) }) - - describe('escapeTableCellPreservingLink', () => { - it('escapes plain text pipe for table safety', () => { - expect(escapeTableCell('a|b')).toBe('a\\|b') - }) - - it('preserves markdown link and escapes pipes inside text and URL', () => { - const input = '[a|b](http://example.com/x|y)' - expect(escapeTableCell(input)).toBe('[a\\|b](http://example.com/x\\|y)') - }) - - it('escapes pipes even when value contains escaped characters', () => { - const input = '[a|b](http://example.com/x|y\\z)' - expect(escapeTableCell(input)).toBe('[a\\|b](http://example.com/x\\|y\\\\z)') - }) - - it('treats strings that do not end with `)` as plain text', () => { - const input = '[a|b](http://example.com/x|y' - expect(escapeTableCell(input)).toBe('\\[a\\|b\\](http://example.com/x\\|y') - }) - - it('returns empty string for null/undefined', () => { - expect(escapeTableCell(null)).toBe('') - expect(escapeTableCell(undefined)).toBe('') - }) - }) }) diff --git a/plugins/converter-resources/src/markdown/escape.ts b/plugins/converter-resources/src/markdown/escape.ts index 3175bf13eb..0a4b0360c4 100644 --- a/plugins/converter-resources/src/markdown/escape.ts +++ b/plugins/converter-resources/src/markdown/escape.ts @@ -37,23 +37,3 @@ export function escapeMarkdownLinkUrl (url: string): string { .replace(/\|/g, '\\|') ) } - -/** - * Escape a markdown table cell while preserving `[text](url)` links. - */ -export function escapeTableCell (value: unknown): string { - const s = value == null ? '' : String(value) - - const sep = s.indexOf('](') - const looksLikeMarkdownLink = s.startsWith('[') && sep !== -1 && s.endsWith(')') - if (!looksLikeMarkdownLink) { - return escapeMarkdownLinkText(s) - } - - const rawText = s.slice(1, sep) - const rawUrl = s.slice(sep + 2, -1) - - const escapedText = escapeMarkdownLinkText(rawText) - const escapedUrl = escapeMarkdownLinkUrl(rawUrl) - return `[${escapedText}](${escapedUrl})` -} diff --git a/plugins/converter-resources/src/markdown/tableBuilder.ts b/plugins/converter-resources/src/markdown/tableBuilder.ts index 16109337c9..8d2bcdec58 100644 --- a/plugins/converter-resources/src/markdown/tableBuilder.ts +++ b/plugins/converter-resources/src/markdown/tableBuilder.ts @@ -28,7 +28,7 @@ import type { CopyAsMarkdownTableProps, CopyRelationshipTableAsMarkdownProps } f import { formatValue } from '../formatter' import { generateHeaders, loadViewletConfig, buildTableModel } from '../model' import { rebuildRelationshipTableViewModel, isRelationshipTable } from '../data' -import { escapeTableCell } from './escape' +import { escapeMarkdownLinkText } from './escape' import { createMarkdownLink } from './link' async function preloadRefLookups ( @@ -253,7 +253,7 @@ export async function buildMarkdownTableFromDocs ( const linkValue = await createMarkdownLink(hierarchy, card, value) row.push(linkValue) } else { - row.push(escapeTableCell(value)) + row.push(escapeMarkdownLinkText(value == null ? '' : String(value))) } } rows.push(row) @@ -375,7 +375,7 @@ export async function buildRelationshipTableMarkdown ( if (isDocumentTitle) { value = await createMarkdownLink(hierarchy, docToUse, value) } else { - value = escapeTableCell(value) + value = escapeMarkdownLinkText(value == null ? '' : String(value)) } row[attrIndex] = value diff --git a/plugins/text-editor-resources/src/components/extension/shortcuts/smartPaste.ts b/plugins/text-editor-resources/src/components/extension/shortcuts/smartPaste.ts index b568e76ea6..0cd4924a7c 100644 --- a/plugins/text-editor-resources/src/components/extension/shortcuts/smartPaste.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/smartPaste.ts @@ -36,6 +36,7 @@ function PasteTextAsMarkdownPlugin (): Plugin { if (clipboardData === null) return false const pastedText = clipboardData.getData('text/plain') + const pastedMarkdown = clipboardData.getData('text/markdown') // check if we are in code block const { $from } = view.state.selection @@ -47,12 +48,21 @@ function PasteTextAsMarkdownPlugin (): Plugin { } } + // If the clipboard explicitly provides markdown, prefer it even if other types (e.g. html) exist. + const hasMarkdown = pastedMarkdown.trim().length > 0 + const markdownSource = hasMarkdown ? pastedMarkdown : pastedText + + // Table copies include metadata comment; treat as markdown even when clipboard has rich types. + const hasTableMetadata = + pastedText.includes('