From c3079951a86454a683779feb338a8d7fd084c41f Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Mon, 30 Mar 2026 17:13:23 +0700 Subject: [PATCH 1/4] Do not handle markdown table as KaTeX (#10699) * Do not handle markdown table as LATEX Signed-off-by: Artem Savchenko * Tighten the math paste criteria Signed-off-by: Artem Savchenko * Potential fix for code scanning alert no. 296: 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 --------- 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> --- .../components/extension/mathematics.test.ts | 43 +++++++++++++++++++ .../src/components/extension/mathematics.ts | 21 +++++++-- .../extension/shortcuts/smartPaste.ts | 4 +- .../extension/shortcuts/tableMetadata.ts | 24 +++++++++++ .../extension/shortcuts/tablePaste.ts | 5 ++- 5 files changed, 90 insertions(+), 7 deletions(-) create mode 100644 plugins/text-editor-resources/src/components/extension/mathematics.test.ts create mode 100644 plugins/text-editor-resources/src/components/extension/shortcuts/tableMetadata.ts diff --git a/plugins/text-editor-resources/src/components/extension/mathematics.test.ts b/plugins/text-editor-resources/src/components/extension/mathematics.test.ts new file mode 100644 index 0000000000..336ffe97ad --- /dev/null +++ b/plugins/text-editor-resources/src/components/extension/mathematics.test.ts @@ -0,0 +1,43 @@ +// +// Copyright © 2026 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 { isStandaloneMathExpression } from './mathematics' + +describe('isStandaloneMathExpression', () => { + it('matches standalone inline math', () => { + expect(isStandaloneMathExpression('$x + y$')).toBe(true) + expect(isStandaloneMathExpression(' $E=mc^2$ ')).toBe(true) + }) + + it('matches standalone block math', () => { + expect(isStandaloneMathExpression('$$x^2 + y^2 = z^2$$')).toBe(true) + expect(isStandaloneMathExpression(' $$\\frac{a}{b}$$\n')).toBe(true) + }) + + it('does not match plain text containing dollar signs', () => { + expect(isStandaloneMathExpression('price is $5 only')).toBe(false) + expect(isStandaloneMathExpression('Total: $5 and $7')).toBe(false) + }) + + it('does not match markdown table content', () => { + const table = `| A | B | +| --- | --- | +| value $1 | text |` + expect(isStandaloneMathExpression(table)).toBe(false) + }) + + it('does not match empty input', () => { + expect(isStandaloneMathExpression('')).toBe(false) + }) +}) diff --git a/plugins/text-editor-resources/src/components/extension/mathematics.ts b/plugins/text-editor-resources/src/components/extension/mathematics.ts index 25f00adc22..57bf63b15a 100644 --- a/plugins/text-editor-resources/src/components/extension/mathematics.ts +++ b/plugins/text-editor-resources/src/components/extension/mathematics.ts @@ -19,6 +19,7 @@ import katex from 'katex' import { type Node } from '@tiptap/pm/model' import { type EditorState, Plugin, PluginKey, type Transaction, TextSelection } from '@tiptap/pm/state' import { Decoration, DecorationSet } from '@tiptap/pm/view' +import { hasTableMetadataMarker } from './shortcuts/tableMetadata' declare module '@tiptap/core' { interface Commands { @@ -216,8 +217,15 @@ function createMathPlugin ( }) } +// Handle paste as math only when the full payload is a standalone inline/block expression. +// This avoids intercepting rich text/table markdown that may contain incidental '$' chars. +const LATEX_RE = /^\s*(?:\$\$[\s\S]+\$\$|\$(?:\\.|[^\n$\\])+\$)\s*$/ + +export function isStandaloneMathExpression (text: string): boolean { + return text !== '' && LATEX_RE.test(text) +} + function MathPastePlugin (): Plugin { - const LATEX_RE = /\$\$?[^$]+\$\$?/ return new Plugin({ props: { handleDOMEvents: { @@ -225,8 +233,15 @@ function MathPastePlugin (): Plugin { const clipboardData = event.clipboardData if (clipboardData === null) return false - const text = clipboardData.getData('text/plain') - if (text === '' || !LATEX_RE.test(text)) return false + // Let table metadata paste handlers process this payload. + const plainText = clipboardData.getData('text/plain') + const markdownText = clipboardData.getData('text/markdown') + if (hasTableMetadataMarker(plainText) || hasTableMetadataMarker(markdownText)) { + return false + } + + const text = plainText + if (!isStandaloneMathExpression(text)) return false event.preventDefault() view.dispatch(view.state.tr.insertText(text)) 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 0cd4924a7c..0ac28afe83 100644 --- a/plugins/text-editor-resources/src/components/extension/shortcuts/smartPaste.ts +++ b/plugins/text-editor-resources/src/components/extension/shortcuts/smartPaste.ts @@ -19,6 +19,7 @@ import { Extension } from '@tiptap/core' import { Node, type Schema } from '@tiptap/pm/model' import { Plugin } from '@tiptap/pm/state' import { CodeBlockHighlighExtension } from '../codeSnippets/codeblock' +import { hasTableMetadataMarker } from './tableMetadata' export const SmartPasteExtension = Extension.create({ name: 'transformPastedContent', @@ -53,8 +54,7 @@ function PasteTextAsMarkdownPlugin (): Plugin { const markdownSource = hasMarkdown ? pastedMarkdown : pastedText // Table copies include metadata comment; treat as markdown even when clipboard has rich types. - const hasTableMetadata = - pastedText.includes(' - const commentRegex = //s + const commentRegex = new RegExp(``, 's') const match = text.match(commentRegex) if (match?.[1] !== undefined) { try { @@ -79,7 +80,7 @@ function TableMetadataPastePlugin (): Plugin { let metadata: TableMetadata | null = null // 1. Try custom MIME type (fastest, most reliable for internal paste) - const metadataType = 'application/x-huly-table-metadata' + const metadataType = TABLE_METADATA_MIME_TYPE if (clipboardData.types.includes(metadataType)) { try { const metadataJsonStr = clipboardData.getData(metadataType) From d09f05acb476e76572407bdf215f23dadce9e20b Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Mon, 30 Mar 2026 22:43:47 +0500 Subject: [PATCH 2/4] Fix view settings (#10706) Signed-off-by: Denis Bykhov --- .../src/components/settings/view/ViewSetting.svelte | 10 +--------- .../src/components/ViewletSetting.svelte | 3 +-- plugins/view-resources/src/utils.ts | 13 ++++++++++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/plugins/card-resources/src/components/settings/view/ViewSetting.svelte b/plugins/card-resources/src/components/settings/view/ViewSetting.svelte index fa86478857..e3c901b568 100644 --- a/plugins/card-resources/src/components/settings/view/ViewSetting.svelte +++ b/plugins/card-resources/src/components/settings/view/ViewSetting.svelte @@ -158,11 +158,10 @@ if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) return const { attrClass, category } = getAttributePresenterClass(hierarchy, attribute.type) const value = getValue(attribute.name, attribute.type, attrClass) - const proxiedValue = attribute.attributeOf + '.' + attribute.name for (const res of result) { const key = getKey(res.value) if (key === undefined) continue - if (key === attribute.name || key === value || key === proxiedValue) return + if (key === attribute.name || key === value) return if (key === '' && isAttribute(res) && res.label === attribute.label) return } const mixin = @@ -235,19 +234,12 @@ function getConfig (viewlet: Viewlet, preference: ViewletPreference | undefined): Config[] { const result = getBaseConfig(viewlet) - if (viewlet.configOptions?.strict !== true) { const allAttributes = hierarchy.getAllAttributes(viewlet.attachTo) for (const [, attribute] of allAttributes) { processAttribute(attribute, result) } - hierarchy.getDescendants(viewlet.attachTo).forEach((it) => { - hierarchy.getOwnAttributes(it).forEach((attr) => { - processAttribute(attr, result, true) - }) - }) - const desc = hierarchy.getDescendants(viewlet.attachTo) for (const d of desc) { if (!hierarchy.isMixin(d)) continue diff --git a/plugins/view-resources/src/components/ViewletSetting.svelte b/plugins/view-resources/src/components/ViewletSetting.svelte index fabf7aac63..96ddac1560 100644 --- a/plugins/view-resources/src/components/ViewletSetting.svelte +++ b/plugins/view-resources/src/components/ViewletSetting.svelte @@ -196,11 +196,10 @@ if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) return const { attrClass, category } = getAttributePresenterClass(hierarchy, attribute.type) const value = getValue(attribute.name, attribute.type, attrClass) - const proxiedValue = attribute.attributeOf + '.' + attribute.name for (const res of result) { const key = getKey(res.value) if (key === undefined) continue - if (key === attribute.name || key === value || key === proxiedValue) return + if (key === attribute.name || key === value) return if (key === '' && isAttribute(res) && res.label === attribute.label) return } const mixin = diff --git a/plugins/view-resources/src/utils.ts b/plugins/view-resources/src/utils.ts index 830578dcae..e82afd58fa 100644 --- a/plugins/view-resources/src/utils.ts +++ b/plugins/view-resources/src/utils.ts @@ -1172,11 +1172,13 @@ export function canResolveAttribute ( } } if (key.length === 0) return true - try { + const parts = key.split('.') + if (parts.length === 1) { return hierarchy.findAttribute(_class, key) !== undefined - } catch { - return false + } else if (hierarchy.isDerived(parts[0] as Ref>, _class)) { + return hierarchy.findAttribute(parts[0] as Ref>, parts[1]) !== undefined } + return false } export function getKeyLabel ( @@ -1206,6 +1208,11 @@ export function getKeyLabel ( const clazz = client.getHierarchy().getClass(_class) return clazz.label } else { + const parts = key.split('.') + if (parts.length === 2 && client.getHierarchy().isDerived(parts[0] as Ref>, _class)) { + const attribute = client.getHierarchy().getAttribute(parts[0] as Ref>, parts[1]) + return attribute.label + } const attribute = client.getHierarchy().getAttribute(_class, key) return attribute.label } From 2bb87bf441135893a95e2556cb7c6d820f351ddf Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Tue, 31 Mar 2026 00:44:00 +0700 Subject: [PATCH 3/4] Do not select space not accessible to the user (#10707) Signed-off-by: Artem Savchenko --- .../src/components/SpaceSelect.svelte | 12 +++++++--- .../src/components/SpaceSelector.svelte | 2 ++ .../src/components/CreateCardPopup.svelte | 10 ++++++--- .../src/components/CreateIssue.svelte | 22 +++++++++++++++---- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/presentation/src/components/SpaceSelect.svelte b/packages/presentation/src/components/SpaceSelect.svelte index e96add4ab0..074b1fcbb7 100644 --- a/packages/presentation/src/components/SpaceSelect.svelte +++ b/packages/presentation/src/components/SpaceSelect.svelte @@ -61,6 +61,7 @@ export let component: AnyComponent | AnySvelteComponent | undefined = undefined export let componentProps: any | undefined = undefined export let autoSelect = true + export let clearInvalidValue = false export let readonly = false export let ignoreFill = false export let iconWithEmoji: AnySvelteComponent | Asset | ComponentType | undefined = view.ids.IconWithEmoji @@ -87,12 +88,17 @@ value = selected._id ?? undefined } } + + // If a value is provided but can't be resolved, optionally clear the bound value. + if (selected === undefined && clearInvalidValue && _value !== undefined && value === _value) { + value = undefined + } dispatch('object', selected) }) $: void updateSelected(value, spaceQuery) - const showSpacesPopup = (ev: MouseEvent) => { + const showSpacesPopup = (ev: MouseEvent): void => { if (readonly) { return } @@ -136,8 +142,8 @@ {shape} disabled={readonly} {focusIndex} - icon={selected?.icon === iconWithEmoji && iconWithEmoji ? IconWithEmoji : (selected?.icon ?? defaultIcon)} - iconProps={selected?.icon === iconWithEmoji && iconWithEmoji + icon={selected?.icon === iconWithEmoji && iconWithEmoji != null ? IconWithEmoji : (selected?.icon ?? defaultIcon)} + iconProps={selected?.icon === iconWithEmoji && iconWithEmoji != null ? { icon: selected?.color } : ignoreFill ? undefined diff --git a/packages/presentation/src/components/SpaceSelector.svelte b/packages/presentation/src/components/SpaceSelector.svelte index 8af5f62a97..1458591582 100644 --- a/packages/presentation/src/components/SpaceSelector.svelte +++ b/packages/presentation/src/components/SpaceSelector.svelte @@ -35,6 +35,7 @@ export let component: AnyComponent | AnySvelteComponent | undefined = undefined export let componentProps: any | undefined = undefined export let autoSelect = true + export let clearInvalidValue = false export let iconWithEmoji: AnySvelteComponent | Asset | ComponentType | undefined = undefined export let defaultIcon: AnySvelteComponent | Asset | ComponentType | undefined = undefined export let readonly: boolean = false @@ -66,6 +67,7 @@ {component} {componentProps} {autoSelect} + {clearInvalidValue} {readonly} {iconWithEmoji} {defaultIcon} diff --git a/plugins/card-resources/src/components/CreateCardPopup.svelte b/plugins/card-resources/src/components/CreateCardPopup.svelte index 25442bc165..920cd4587c 100644 --- a/plugins/card-resources/src/components/CreateCardPopup.svelte +++ b/plugins/card-resources/src/components/CreateCardPopup.svelte @@ -15,7 +15,7 @@ import card, { Card, CardSpace, MasterTag } from '@hcengineering/card' import presentation, { getClient, getCommunicationClient, SpaceSelector } from '@hcengineering/presentation' import { createEventDispatcher } from 'svelte' - import core, { Data, generateId, Ref, Markup, notEmpty } from '@hcengineering/core' + import core, { Data, generateId, Ref, Markup, notEmpty, getCurrentAccount } from '@hcengineering/core' import { getResource, translate, getEmbeddedLabel } from '@hcengineering/platform' import { Label, Modal, ModernEditbox, languageStore, showPopup, Component } from '@hcengineering/ui' import { AttachmentStyledBox } from '@hcengineering/attachment-resources' @@ -142,7 +142,7 @@ } } - $: allowed = _space && canCreateObject(type, _space, $permissionsStore) + $: allowed = _space != null && canCreateObject(type, _space, $permissionsStore)