From 4fbaa52abed6ab3df4cb40819180db088566f120 Mon Sep 17 00:00:00 2001 From: gjulivan Date: Tue, 21 Jul 2026 12:11:53 +0200 Subject: [PATCH 1/3] chore: updated version of rich text with tiptap --- .../rich-text-web/CHANGELOG.md | 16 + .../rich-text-web/e2e/RichText.spec.js | 68 ++ .../add-cell-column-width/.openspec.yaml | 2 + .../changes/add-cell-column-width/design.md | 186 ++++ .../changes/add-cell-column-width/proposal.md | 41 + .../specs/table-column-width-control/spec.md | 102 +++ .../changes/add-cell-column-width/tasks.md | 48 + .../README.md | 84 ++ .../design.md | 374 ++++++++ .../proposal.md | 58 ++ .../tasks.md | 107 +++ .../.openspec.yaml | 2 + .../design.md | 176 ++++ .../proposal.md | 48 + .../specs/list-style-auto-cycle/spec.md | 96 ++ .../tasks.md | 17 + .../.openspec.yaml | 2 + .../2026-07-12-fix-list-tab-indent/design.md | 109 +++ .../proposal.md | 38 + .../specs/list-tab-indent/spec.md | 77 ++ .../2026-07-12-fix-list-tab-indent/tasks.md | 22 + .../.openspec.yaml | 2 + .../design.md | 250 +++++ .../proposal.md | 60 ++ .../specs/manual-list-style/spec.md | 129 +++ .../tasks.md | 48 + .../.openspec.yaml | 2 + .../design.md | 465 ++++++++++ .../proposal.md | 160 ++++ .../specs/ordered-list-interaction/spec.md | 139 +++ .../specs/split-button-component/spec.md | 185 ++++ .../specs/toolbar-configuration/spec.md | 91 ++ .../tasks.md | 97 ++ .../.openspec.yaml | 2 + .../2026-07-13-add-link-bubble-menu/design.md | 89 ++ .../proposal.md | 32 + .../specs/rich-text-link-bubble-menu/spec.md | 58 ++ .../2026-07-13-add-link-bubble-menu/tasks.md | 30 + .../.openspec.yaml | 2 + .../design.md | 93 ++ .../proposal.md | 33 + .../specs/rich-text-toolbar-button/spec.md | 67 ++ .../tasks.md | 31 + .../proposal.md | 144 +++ .../specs/list-margin-indent/spec.md | 114 +++ .../2026-07-13-list-margin-keyboard/tasks.md | 54 ++ .../code-view-highlighting/.openspec.yaml | 2 + .../changes/code-view-highlighting/README.md | 3 + .../rich-text-web/openspec/config.yaml | 20 + .../openspec/specs/list-margin-indent/spec.md | 120 +++ .../specs/list-style-auto-cycle/spec.md | 102 +++ .../openspec/specs/list-tab-indent/spec.md | 83 ++ .../openspec/specs/manual-list-style/spec.md | 135 +++ .../specs/ordered-list-interaction/spec.md | 143 +++ .../specs/rich-text-link-bubble-menu/spec.md | 64 ++ .../specs/rich-text-toolbar-button/spec.md | 73 ++ .../specs/split-button-component/spec.md | 189 ++++ .../specs/toolbar-configuration/spec.md | 95 ++ .../rich-text-web/package.json | 44 +- .../src/RichText.editorConfig.ts | 2 +- .../rich-text-web/src/RichText.tsx | 46 +- .../rich-text-web/src/RichText.xml | 15 +- .../src/__tests__/Indent.spec.ts | 296 ++++++ .../src/__tests__/LinkBubbleMenu.spec.ts | 90 ++ .../src/__tests__/RichText.spec.tsx | 23 +- .../src/__tests__/customList.spec.ts | 59 ++ .../rich-text-web/src/__tests__/fonts.spec.ts | 148 +++ .../src/__tests__/helpers.spec.ts | 214 +++++ .../rich-text-web/src/assets/.gitkeep | 0 .../rich-text-web/src/assets/Icons.tsx | 101 --- .../src/assets/RichTextIconFont.woff2 | Bin 7052 -> 5848 bytes .../components/CustomToolbars/Fullscreen.tsx | 23 - .../CustomToolbars/ToolbarWrapper.tsx | 84 -- .../components/CustomToolbars/UndoRedo.tsx | 19 - .../components/CustomToolbars/constants.ts | 249 ----- .../CustomToolbars/customToolbars.d.ts | 21 - .../src/components/CustomToolbars/presets.ts | 44 - .../CustomToolbars/useEmbedModal.ts | 252 ------ .../src/components/DynamicTableStyles.tsx | 78 ++ .../src/components/DynamicTextColorStyles.tsx | 83 ++ .../rich-text-web/src/components/Editor.tsx | 469 ++++++---- .../src/components/EditorContext.tsx | 106 +++ .../src/components/EditorWrapper.tsx | 300 +++--- .../src/components/EmbedResize.tsx | 106 +++ .../src/components/HighlightedCodeEditor.tsx | 50 + .../src/components/ImageResize.tsx | 95 ++ .../src/components/LinkBubbleMenu.scss | 27 + .../src/components/LinkBubbleMenu.tsx | 71 ++ .../src/components/ModalDialog/Dialog.scss | 169 ---- .../src/components/ModalDialog/Dialog.tsx | 116 --- .../components/ModalDialog/DialogContent.tsx | 121 --- .../components/ModalDialog/ImageDialog.tsx | 275 ------ .../src/components/ModalDialog/LinkDialog.tsx | 79 -- .../components/ModalDialog/VideoDialog.tsx | 166 ---- .../components/ModalDialog/ViewCodeDialog.tsx | 62 -- .../src/components/StatusBar.tsx | 75 ++ .../src/components/StickySentinel.tsx | 49 - .../rich-text-web/src/components/Toolbar.tsx | 106 --- .../src/components/YouTubeResize.tsx | 100 ++ .../src/components/toolbars/Toolbar.scss | 349 +++++++ .../src/components/toolbars/Toolbar.tsx | 194 ++++ .../src/components/toolbars/ToolbarConfig.ts | 802 ++++++++++++++++ .../toolbars/components/CodeView.tsx | 31 + .../toolbars/components/ColorPicker.scss | 9 + .../toolbars/components/ColorPicker.tsx | 70 ++ .../components/ConfigurationDropdown.scss | 119 +++ .../components/ConfigurationDropdown.tsx | 137 +++ .../toolbars/components/ConfirmDialog.tsx | 42 + .../toolbars/components/Dialog.scss | 462 ++++++++++ .../components/toolbars/components/Dialog.tsx | 46 + .../toolbars/components/ImageDialog.tsx | 312 +++++++ .../toolbars/components/LinkDialog.tsx | 162 ++++ .../toolbars/components/TableGrid.tsx | 30 + .../components/TableGridSelector.scss | 49 + .../toolbars/components/TableGridSelector.tsx | 64 ++ .../toolbars/components/ToolbarButton.tsx | 109 +++ .../components/ToolbarDefaultButton.tsx | 41 + .../toolbars/components/ToolbarDropdown.tsx | 99 ++ .../components/ToolbarSplitButton.tsx | 191 ++++ .../toolbars/components/VideoDialog.tsx | 296 ++++++ .../__tests__/ToolbarDefaultButton.spec.tsx | 111 +++ .../toolbars/helpers/colorPickerHelpers.ts | 70 ++ .../toolbars/helpers/configurationHelpers.ts | 260 ++++++ .../toolbars/helpers/fontHelpers.ts | 76 ++ .../toolbars/helpers/listHelpers.ts | 14 + .../toolbars/helpers/toolbarTypes.ts | 121 +++ .../components/toolbars/hooks/useDropdown.ts | 96 ++ .../src/components/toolbars/index.ts | 19 + .../src/extensions/FontFamilyClass.ts | 164 ++++ .../rich-text-web/src/extensions/FontSize.ts | 88 ++ .../src/extensions/Fullscreen.ts | 81 ++ .../src/extensions/GenericEmbed.ts | 136 +++ .../src/extensions/ImageResize.ts | 81 ++ .../rich-text-web/src/extensions/Indent.ts | 310 +++++++ .../src/extensions/KeyboardNavigation.ts | 111 +++ .../src/extensions/OrderedListStyled.ts | 79 ++ .../src/extensions/TableBackgroundColor.ts | 507 +++++++++++ .../extensions/TableCellBackgroundColor.ts | 216 +++++ .../src/extensions/TextAlignClass.ts | 185 ++++ .../src/extensions/TextColorClass.ts | 84 ++ .../src/extensions/TextDirection.ts | 76 ++ .../src/extensions/TextHighlightClass.ts | 109 +++ .../src/extensions/YouTubeResize.ts | 9 + .../rich-text-web/src/package.xml | 2 +- .../src/store/EditorProvider.tsx | 15 - .../rich-text-web/src/store/store.ts | 25 - .../src/store/useActionEvents.ts | 59 -- .../rich-text-web/src/ui/RichText.scss | 678 +++++++++++--- .../src/ui/RichTextFormatStyle.scss | 75 ++ .../rich-text-web/src/ui/RichTextIcons.scss | 186 ++-- .../rich-text-web/src/ui/TableStyle.scss | 56 ++ .../formats/fonts.scss => ui/variables.scss} | 75 +- .../rich-text-web/src/utils/MxQuill.ts | 236 ----- .../src/utils/customPluginRegisters.ts | 49 - .../src/utils/embedCodeParser.ts | 129 +++ .../rich-text-web/src/utils/formats.d.ts | 7 + .../rich-text-web/src/utils/formats/block.ts | 35 - .../rich-text-web/src/utils/formats/button.ts | 55 -- .../src/utils/formats/customList.scss | 7 - .../src/utils/formats/customList.ts | 40 - .../rich-text-web/src/utils/formats/fonts.ts | 63 -- .../src/utils/formats/fontsize.ts | 25 - .../src/utils/formats/formula.ts | 31 - .../src/utils/formats/header.scss | 12 - .../rich-text-web/src/utils/formats/image.ts | 38 - .../rich-text-web/src/utils/formats/indent.ts | 52 -- .../rich-text-web/src/utils/formats/link.ts | 61 -- .../assets/css/quill-table-better.scss | 684 -------------- .../quill-table-better/assets/icon/check.png | Bin 209 -> 0 bytes .../quill-table-better/config/index.ts | 448 --------- .../quill-table-better/formats/header.ts | 82 -- .../quill-table-better/formats/list.ts | 161 ---- .../quill-table-better/formats/table.ts | 785 ---------------- .../quill-table-better/language/de_DE.ts | 60 -- .../quill-table-better/language/en_US.ts | 60 -- .../quill-table-better/language/fr_FR.ts | 60 -- .../quill-table-better/language/index.ts | 63 -- .../quill-table-better/language/pl_PL.ts | 60 -- .../quill-table-better/language/ru_RU.ts | 60 -- .../quill-table-better/language/tr_TR.ts | 60 -- .../quill-table-better/language/zh_CN.ts | 60 -- .../quill-table-better/modules/clipboard.ts | 58 -- .../quill-table-better/modules/toolbar.ts | 247 ----- .../quill-table-better/quill-table-better.ts | 369 -------- .../utils/formats/quill-table-better/types.ts | 85 -- .../quill-table-better/ui/cell-selection.ts | 718 --------------- .../quill-table-better/ui/operate-line.ts | 446 --------- .../quill-table-better/ui/table-menus.ts | 854 ------------------ .../ui/table-properties-form.ts | 606 ------------- .../quill-table-better/ui/toolbar-table.ts | 123 --- .../utils/clipboard-matchers.ts | 90 -- .../formats/quill-table-better/utils/index.ts | 383 -------- .../src/utils/formats/resizeModuleConfig.ts | 71 -- .../src/utils/formats/softBreak.ts | 17 - .../src/utils/formats/video.scss | 5 - .../rich-text-web/src/utils/formats/video.ts | 35 - .../src/utils/formats/whiteSpace.ts | 22 - .../rich-text-web/src/utils/helpers.ts | 63 ++ .../src/utils/modules/clipboard.ts | 162 ---- .../src/utils/modules/keyboard.ts | 67 -- .../rich-text-web/src/utils/modules/resize.ts | 103 --- .../src/utils/modules/resizeToolbar.ts | 38 - .../rich-text-web/src/utils/modules/scroll.ts | 16 - .../src/utils/modules/toolbarHandlers.ts | 183 ---- .../src/utils/modules/uploader.ts | 35 - .../rich-text-web/src/utils/themes/mxTheme.ts | 143 --- .../src/utils/themes/mxTooltip.ts | 103 --- .../rich-text-web/typings/RichTextProps.d.ts | 6 +- .../rich-text-web/typings/modules.d.ts | 63 -- .../rich-text-web/typings/tiptap-menus.d.ts | 6 + 210 files changed, 14771 insertions(+), 11129 deletions(-) create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/.openspec.yaml create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/design.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/proposal.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/specs/table-column-width-control/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/tasks.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/README.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/design.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/proposal.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/tasks.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/.openspec.yaml create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/design.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/proposal.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/specs/list-style-auto-cycle/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/tasks.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-fix-list-tab-indent/.openspec.yaml create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-fix-list-tab-indent/design.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-fix-list-tab-indent/proposal.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-fix-list-tab-indent/specs/list-tab-indent/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-fix-list-tab-indent/tasks.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-manual-list-style-override/.openspec.yaml create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-manual-list-style-override/design.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-manual-list-style-override/proposal.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-manual-list-style-override/specs/manual-list-style/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-manual-list-style-override/tasks.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-ordered-list-split-button/.openspec.yaml create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-ordered-list-split-button/design.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-ordered-list-split-button/proposal.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/ordered-list-interaction/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/split-button-component/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-ordered-list-split-button/specs/toolbar-configuration/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-ordered-list-split-button/tasks.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-add-link-bubble-menu/.openspec.yaml create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-add-link-bubble-menu/design.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-add-link-bubble-menu/proposal.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-add-link-bubble-menu/specs/rich-text-link-bubble-menu/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-add-link-bubble-menu/tasks.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/.openspec.yaml create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/design.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/proposal.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/specs/rich-text-toolbar-button/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-consolidate-toolbar-button/tasks.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-list-margin-keyboard/proposal.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-list-margin-keyboard/specs/list-margin-indent/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-13-list-margin-keyboard/tasks.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/code-view-highlighting/.openspec.yaml create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/changes/code-view-highlighting/README.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/config.yaml create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/specs/list-margin-indent/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/specs/list-style-auto-cycle/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/specs/list-tab-indent/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/specs/manual-list-style/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/specs/ordered-list-interaction/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/specs/rich-text-link-bubble-menu/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/specs/rich-text-toolbar-button/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/specs/split-button-component/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/openspec/specs/toolbar-configuration/spec.md create mode 100644 packages/pluggableWidgets/rich-text-web/src/__tests__/Indent.spec.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/__tests__/LinkBubbleMenu.spec.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/__tests__/customList.spec.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/__tests__/fonts.spec.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/__tests__/helpers.spec.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/assets/.gitkeep delete mode 100644 packages/pluggableWidgets/rich-text-web/src/assets/Icons.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/Fullscreen.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/ToolbarWrapper.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/UndoRedo.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/constants.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/customToolbars.d.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/presets.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/CustomToolbars/useEmbedModal.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/DynamicTableStyles.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/DynamicTextColorStyles.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/EditorContext.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/EmbedResize.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/HighlightedCodeEditor.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ImageResize.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/LinkBubbleMenu.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/LinkBubbleMenu.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/Dialog.scss delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/Dialog.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/DialogContent.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/ImageDialog.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/LinkDialog.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/VideoDialog.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/ModalDialog/ViewCodeDialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/StatusBar.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/StickySentinel.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/components/Toolbar.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/YouTubeResize.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/Toolbar.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/Toolbar.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/ToolbarConfig.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/CodeView.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ColorPicker.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ColorPicker.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ConfigurationDropdown.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ConfigurationDropdown.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ConfirmDialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/Dialog.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/Dialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ImageDialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/LinkDialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/TableGrid.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/TableGridSelector.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/TableGridSelector.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ToolbarButton.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ToolbarDefaultButton.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ToolbarDropdown.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/ToolbarSplitButton.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/VideoDialog.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/components/__tests__/ToolbarDefaultButton.spec.tsx create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/colorPickerHelpers.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/configurationHelpers.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/fontHelpers.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/listHelpers.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/helpers/toolbarTypes.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/hooks/useDropdown.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/components/toolbars/index.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/FontFamilyClass.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/FontSize.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/Fullscreen.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/GenericEmbed.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/ImageResize.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/Indent.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/KeyboardNavigation.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/OrderedListStyled.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TableBackgroundColor.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TableCellBackgroundColor.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TextAlignClass.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TextColorClass.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TextDirection.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/TextHighlightClass.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/extensions/YouTubeResize.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/store/EditorProvider.tsx delete mode 100644 packages/pluggableWidgets/rich-text-web/src/store/store.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/store/useActionEvents.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/ui/RichTextFormatStyle.scss create mode 100644 packages/pluggableWidgets/rich-text-web/src/ui/TableStyle.scss rename packages/pluggableWidgets/rich-text-web/src/{utils/formats/fonts.scss => ui/variables.scss} (57%) delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/MxQuill.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/customPluginRegisters.ts create mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/embedCodeParser.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/block.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/button.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/customList.scss delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/customList.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/fonts.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/fontsize.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/formula.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/header.scss delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/image.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/indent.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/link.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/assets/css/quill-table-better.scss delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/assets/icon/check.png delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/config/index.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/formats/header.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/formats/list.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/formats/table.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/de_DE.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/en_US.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/fr_FR.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/index.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/pl_PL.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/ru_RU.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/tr_TR.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/language/zh_CN.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/modules/clipboard.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/modules/toolbar.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/quill-table-better.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/types.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/ui/cell-selection.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/ui/operate-line.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/ui/table-menus.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/ui/table-properties-form.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/ui/toolbar-table.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/utils/clipboard-matchers.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/quill-table-better/utils/index.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/resizeModuleConfig.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/softBreak.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/video.scss delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/video.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/formats/whiteSpace.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/clipboard.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/keyboard.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/resize.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/resizeToolbar.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/scroll.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/toolbarHandlers.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/modules/uploader.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/themes/mxTheme.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/src/utils/themes/mxTooltip.ts delete mode 100644 packages/pluggableWidgets/rich-text-web/typings/modules.d.ts create mode 100644 packages/pluggableWidgets/rich-text-web/typings/tiptap-menus.d.ts diff --git a/packages/pluggableWidgets/rich-text-web/CHANGELOG.md b/packages/pluggableWidgets/rich-text-web/CHANGELOG.md index 2ab0b1c870..f1532c0e25 100644 --- a/packages/pluggableWidgets/rich-text-web/CHANGELOG.md +++ b/packages/pluggableWidgets/rich-text-web/CHANGELOG.md @@ -10,6 +10,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We fixed a security vulnerability (CVE-2026-13149). +### Added + +- We added new configuration to allow users to use class names instead of inline styling in generated HTML to support strict CSP. + +### Fixed + +- We fixed Tiptap duplicate extension warnings for `link`, `textStyle`, `underline`, and `textDirection` by properly configuring StarterKit, removing redundant extension registrations in TextColorClass, memoizing the extensions array to prevent re-registration on component re-renders, and disabling the core TextDirection extension that conflicts with our custom implementation. + +- We fixed an issue where empty editor content was saving as `

` instead of an empty string, which incorrectly passed required field validation. Empty content now correctly saves as `""`, ensuring proper validation behavior. Note: This is a breaking change for forms that were relying on the incorrect behavior - required RichText fields will now correctly reject empty content. + +- We fixed an issue where the editor pasting back the whole sentence instead of the single copied word + +### Changed + +- We removed codemirror from code dialog viewer due to unsupported strict CSP policy. A simple internally built code editor using highlightjs is now replacing it. + ## [4.12.0] - 2026-04-22 ### Added diff --git a/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js b/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js index c05860d869..eb0b626134 100644 --- a/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js +++ b/packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js @@ -2,6 +2,7 @@ import { expect, test } from "@mendix/run-e2e/fixtures"; import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers"; test.describe("RichText", () => { + test.describe.configure({ mode: "serial" }); test("compares with a screenshot baseline and checks if inline basic mode are rendered as expected", async ({ page }) => { @@ -115,6 +116,37 @@ test.describe("RichText", () => { await expect(page.locator(".mx-name-richText6")).toHaveScreenshot(`readOnlyModeReadPanel.png`); }); + test("compares with a screenshot baseline and checks if class mode editor is rendered as expected", async ({ + page + }) => { + await page.goto("/p/classmode"); + await waitForMendixApp(page); + await expect(page.locator(".mx-name-richText1")).toBeVisible(); + await expect(page.locator(".mx-name-richText1")).toHaveScreenshot(`classModeEditor.png`, { threshold: 0.4 }); + }); + + test("checks that class mode editor output uses CSS classes instead of inline styles", async ({ page }) => { + await page.goto("/p/classmode"); + await waitForMendixApp(page); + const html = await page.locator(".mx-name-richText1 .ql-editor").innerHTML(); + expect(html).toMatch(/class="ql-color-/); + expect(html).toMatch(/class="ql-bg-/); + expect(html).toMatch(/class="ql-indent-/); + expect(html).toMatch(/data-style-format="class"/); + expect(html).not.toMatch(/style="color:/); + expect(html).not.toMatch(/style="background-color:/); + expect(html).not.toMatch(/style="padding-left:/); + }); + + test("compares with a screenshot baseline of the View/Edit Code dialog in class mode", async ({ page }) => { + await page.goto("/p/classmode"); + await waitForMendixApp(page); + await page.click(".mx-name-richText1 .ql-toolbar button.ql-view-code"); + await expect(page.locator(".widget-rich-text .widget-rich-text-modal-body").first()).toHaveScreenshot( + `classModeViewCodeDialog.png` + ); + }); + test("compares with a screenshot for rich text inside modal popup layout", async ({ page }) => { await page.goto("/"); await waitForMendixApp(page); @@ -134,4 +166,40 @@ test.describe("RichText", () => { `richTextDialogInsidePopupEdit.png` ); }); + + test("empty content persists as empty string, not

", async ({ page }) => { + await page.goto("/"); + await waitForMendixApp(page); + await page.click("text=Generate Data"); + await page.goto("/p/basic"); + await waitForMendixApp(page); + + // Find the first editable rich text editor + const editor = page.locator(".mx-name-richText1 .tiptap"); + await editor.scrollIntoViewIfNeeded(); + await expect(editor).toBeVisible(); + + // Click into the editor and clear all content + await editor.click(); + await page.keyboard.press("Control+A"); + await page.keyboard.press("Backspace"); + + // Blur the editor to trigger save + await page.keyboard.press("Tab"); + await page.waitForTimeout(500); + + // The editor should now be empty (visual check) + const textContent = await editor.textContent(); + expect(textContent?.trim() || "").toBe(""); + + // Reload the page to verify persistence + await page.reload(); + await waitForMendixApp(page); + + // The editor should still be empty after reload + const reloadedEditor = page.locator(".mx-name-richText1 .tiptap"); + await expect(reloadedEditor).toBeVisible(); + const reloadedContent = await reloadedEditor.textContent(); + expect(reloadedContent?.trim() || "").toBe(""); + }); }); diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/.openspec.yaml b/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/.openspec.yaml new file mode 100644 index 0000000000..c53ef21aaa --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-05 diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/design.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/design.md new file mode 100644 index 0000000000..b1909f37d3 --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/design.md @@ -0,0 +1,186 @@ +## Context + +The rich text editor uses TipTap, which already supports column widths via the `colwidth` attribute on table cells. TipTap's base `TableCell` extension includes this attribute, and the `TableBackgroundColor` extension reads it to generate `` elements for column sizing. + +Currently: + +- `colwidth` is an array of pixel values (e.g., `[150]` for single column, `[100, 150, 200]` for colspan) +- TipTap has a `resizable: true` option that should enable drag-to-resize, but the custom `TableBackgroundColorNodeView` doesn't implement resize handles +- Users can click column borders, which sets a width, but cannot drag to adjust +- No UI control exists to set exact pixel widths + +The existing cell configuration dropdown already has sections for background color, border color/style/width using a `ConfigurationSection` pattern. + +## Goals / Non-Goals + +**Goals:** + +- Add a number input control to the cell configuration dropdown for setting column width +- Support exact pixel values (25-1000px range) +- Provide clear button to reset to auto width +- Handle colspan cells by setting only the first column width +- Follow existing configuration dropdown patterns and styling +- Validate input and clamp to acceptable ranges + +**Non-Goals:** + +- Implementing drag-to-resize handles (would require significant NodeView work) +- Supporting percentage or other CSS units (only pixels for now) +- Setting widths for all columns in a colspan cell (only first column) +- Syncing widths across all rows (only first row widths apply per TipTap's design) +- Adding column width presets dropdown (keep it simple with number input only) + +## Decisions + +### Decision 1: Use number input type (not dropdown with presets) + +**Rationale:** User requested "exact pixel values" in requirements. A number input provides precision without limiting users to preset sizes. + +**Alternatives considered:** + +- Dropdown with presets (Small/Medium/Large): Too restrictive, doesn't meet "exact values" requirement +- Hybrid (input + preset buttons): Over-engineered for initial implementation +- Text input: Number input provides better UX with built-in increment/decrement and validation + +**Implementation:** Add new `"numberInput"` type to `ConfigurationSection` interface alongside existing `"colorPicker"` and `"dropdown"` types. + +--- + +### Decision 2: Set only first column width for colspan cells + +**Rationale:** + +- User explicitly requested "set only first column" in requirements +- Simplifies implementation and UX (no need for multi-column width editor) +- Matches TipTap's array-based `colwidth` structure where `colwidth[0]` is the first column + +**Alternatives considered:** + +- Set all spanned columns to same width: Could surprise users if they had different widths set +- Show per-column width array editor: Complex UI for edge case (most cells don't have colspan) +- Disable control for colspan cells: Too restrictive + +**Implementation:** `onChange` handler creates `colwidth` array with single value: `[width]` + +--- + +### Decision 3: Use setCellAttribute command (not custom command) + +**Rationale:** + +- `setCellAttribute("colwidth", value)` should work because `colwidth` is in TipTap's base TableCell +- Other cell properties (backgroundColor, borderColor, etc.) use `setCellAttribute` successfully +- No need to define custom commands in extensions + +**Alternatives considered:** + +- Custom `setCellWidth` command in `TableCellBackgroundColor`: Unnecessary abstraction +- Modify `TableCellBackgroundColor.addAttributes()` to re-declare colwidth: Already inherits via `...this.parent?.()` + +**Implementation:** Direct call to `editor.chain().focus().setCellAttribute("colwidth", [width]).run()` + +--- + +### Decision 4: Render clear button conditionally (when value exists) + +**Rationale:** + +- Provides explicit way to reset to auto width (null) +- Visual indicator that a custom width is set +- Follows common UI pattern (seen in search inputs, etc.) + +**Alternatives considered:** + +- Always show clear button: Clutters UI when empty +- Use empty string to mean auto: Less explicit, could be confusing +- Add separate "Auto" button: Redundant with clear functionality + +**Implementation:** Render `×` button only when `currentValue !== null` + +--- + +### Decision 5: Store null for auto width (not empty array or zero) + +**Rationale:** + +- TipTap treats `colwidth: null` as auto-sizing behavior +- Consistent with how TipTap's base extensions work +- `colwidth: []` or `colwidth: [0]` could cause layout issues + +**Implementation:** When user clears input, call `setCellAttribute("colwidth", null)` + +## Risks / Trade-offs + +### Risk: Parent attribute inheritance might not work + +If `TableCellBackgroundColor` doesn't properly inherit `colwidth` from base `TableCell`, `setCellAttribute` calls will fail silently. + +**Mitigation:** The extension already uses `...this.parent?.()` in `addAttributes()`, which should preserve `colwidth`. If issues arise, explicitly re-declare `colwidth` in the extension's attributes. + +--- + +### Risk: First-row-only behavior might confuse users + +TipTap only uses `colwidth` from the first row of the table to generate ``. Setting widths on other rows has no effect. + +**Mitigation:** Could add tooltip or helper text: "Note: Only first row column widths apply." For initial implementation, leave as-is since it matches TipTap's inherent behavior. + +--- + +### Risk: Custom NodeView might interfere with colwidth updates + +The `TableBackgroundColorNodeView` manually generates `` in `updateColgroup()`. If it's not reactive to `colwidth` changes, widths might not update visually. + +**Mitigation:** The NodeView's `update()` method already calls `updateColgroup()` on node changes, so it should react to `colwidth` updates. Test thoroughly during implementation. + +--- + +### Trade-off: No drag-to-resize (only manual input) + +Users lose the convenience of dragging column borders to resize. + +**Mitigation:** The number input provides precision that drag handles lack. If drag-to-resize is needed later, it would require integrating ProseMirror's `columnResizing` plugin into the custom NodeView (~200 lines of code). For now, manual input meets the stated requirements. + +--- + +### Trade-off: Pixel-only units (no percentages) + +Users cannot set responsive column widths using percentages. + +**Mitigation:** Percentage support would require custom rendering logic (TipTap's `colwidth` is pixel-only). Tables in rich text editors typically use fixed layouts. Can be added later if users request it. + +## Migration Plan + +No migration needed. This is a purely additive feature: + +- Existing tables without `colwidth` continue to work (auto width) +- Existing tables with `colwidth` set (via TipTap's base functionality) display correctly +- No data model changes or breaking API changes + +Deployment: + +1. Merge code changes +2. Run build +3. Deploy widget to Mendix project +4. Feature is immediately available in cell configuration dropdown + +Rollback: + +- If issues arise, remove the new configuration section from `createCellConfigurationSections()` +- No data corruption risk (colwidth attribute is standard TipTap) + +## Open Questions + +1. **Should we show the actual rendered column width vs. the set value?** + - The set `colwidth` might differ from actual rendered width due to table layout constraints + - For initial implementation: Show the set value only (simpler) + - Can add "measured width" tooltip later if needed + +2. **Should we prevent setting widths on non-first-row cells?** + - Pro: Avoids confusion about why widths don't apply + - Con: Restricts user control, might not match mental model + - Decision: Allow setting on any cell (simpler), rely on TipTap's first-row behavior + +3. **Should we add keyboard shortcuts for common widths?** + - Out of scope for initial implementation + - Can be added later if users request it diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/proposal.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/proposal.md new file mode 100644 index 0000000000..282fd1302a --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/proposal.md @@ -0,0 +1,41 @@ +## Why + +Users need precise control over table column widths in the rich text editor. Currently, while TipTap supports column widths via the `colwidth` attribute, there's no UI control to set them. Users can only click column borders which sets widths without drag-to-resize capability, making it difficult to achieve desired layouts. + +## What Changes + +- Add a "Column Width" number input control to the cell configuration dropdown +- Support setting exact pixel widths (25-1000px range) +- Provide "Clear" button to reset to auto width +- Handle colspan cells by setting only the first column width +- Integrate with existing `setCellAttribute` command for `colwidth` attribute + +## Capabilities + +### New Capabilities + +- `table-column-width-control`: UI control in cell configuration dropdown allowing users to set precise column widths in pixels, with validation, auto width support, and colspan handling + +### Modified Capabilities + + + +## Impact + +**Affected Files:** + +- `src/components/toolbars/components/ConfigurationDropdown.tsx` - Add number input type support +- `src/components/toolbars/components/ConfigurationDropdown.scss` - Add number input styling +- `src/components/toolbars/ToolbarConfig.ts` - Update ConfigurationSection type definition +- `src/components/toolbars/helpers/configurationHelpers.ts` - Add column width section + +**User Impact:** + +- Positive: Users gain precise control over table column widths via familiar number input UI +- No breaking changes: Feature is additive, existing tables continue to work + +**Technical Impact:** + +- Uses existing TipTap `colwidth` attribute mechanism (no changes to data model) +- No impact on existing table resize behavior (custom NodeView remains unchanged) +- Follows established configuration dropdown patterns diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/specs/table-column-width-control/spec.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/specs/table-column-width-control/spec.md new file mode 100644 index 0000000000..1bafe2051b --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/specs/table-column-width-control/spec.md @@ -0,0 +1,102 @@ +## ADDED Requirements + +### Requirement: Column width input control + +The cell configuration dropdown SHALL include a number input control labeled "Column Width" that allows users to set the column width in pixels. + +#### Scenario: Opening cell configuration shows current width + +- **WHEN** user selects a table cell and opens the cell configuration dropdown +- **THEN** the "Column Width" input displays the current column width value if set, or shows empty with "Auto" placeholder if width is auto + +#### Scenario: Setting a valid column width + +- **WHEN** user enters a numeric value between 25 and 1000 in the "Column Width" input +- **THEN** the column width is set to that exact pixel value and the column resizes accordingly + +#### Scenario: Clearing column width for auto sizing + +- **WHEN** user clicks the clear button or deletes the value from the "Column Width" input +- **THEN** the column width is reset to auto (null) and the column resizes to fit content + +### Requirement: Column width validation + +The system SHALL validate column width input to ensure values are within acceptable ranges. + +#### Scenario: Entering value below minimum + +- **WHEN** user enters a value less than 25 pixels +- **THEN** the system clamps the value to 25 pixels + +#### Scenario: Entering value above maximum + +- **WHEN** user enters a value greater than 1000 pixels +- **THEN** the system clamps the value to 1000 pixels + +#### Scenario: Entering invalid input + +- **WHEN** user enters non-numeric text +- **THEN** the system ignores the input and retains the previous value + +### Requirement: Colspan cell width handling + +The system SHALL set only the first column width when the selected cell has a colspan greater than 1. + +#### Scenario: Setting width on colspan cell + +- **WHEN** user sets column width on a cell with colspan=3 that currently has colwidth=[100, 150, 200] +- **THEN** the system sets colwidth=[new_value] affecting only the first spanned column + +#### Scenario: Reading width from colspan cell + +- **WHEN** user opens cell configuration for a cell with colspan=3 and colwidth=[100, 150, 200] +- **THEN** the "Column Width" input displays 100 (the first column's width) + +### Requirement: Number input UI component + +The configuration dropdown SHALL support a "numberInput" type with number-specific controls. + +#### Scenario: Number input with unit label + +- **WHEN** a configuration section has type "numberInput" with unit "px" +- **THEN** the UI renders a number input field with "px" unit label beside it + +#### Scenario: Number input with clear button + +- **WHEN** a configuration section has type "numberInput" and the field has a value +- **THEN** the UI displays a clear button (×) that resets the field to empty when clicked + +#### Scenario: Number input with placeholder + +- **WHEN** a configuration section has type "numberInput" with placeholder "Auto" +- **THEN** the empty input field shows "Auto" as placeholder text + +### Requirement: Column width persistence + +The system SHALL persist column width values in the document's colwidth attribute. + +#### Scenario: Width persists after save and reload + +- **WHEN** user sets a column width to 150 pixels, saves the document, and reloads the page +- **THEN** the column width remains 150 pixels and is displayed correctly in the cell configuration + +#### Scenario: Width persists across editing sessions + +- **WHEN** user sets column widths on multiple columns and closes the editor +- **THEN** reopening the document displays all columns at their configured widths + +### Requirement: Visual feedback + +The system SHALL provide clear visual feedback when column width changes. + +#### Scenario: Immediate column resize on input + +- **WHEN** user changes the column width value in the input field +- **THEN** the table column resizes immediately without requiring additional confirmation + +#### Scenario: Clear button visibility + +- **WHEN** the column width input has a value +- **THEN** the clear button is visible +- **WHEN** the column width input is empty +- **THEN** the clear button is hidden diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/tasks.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/tasks.md new file mode 100644 index 0000000000..dbd8a58ee0 --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/add-cell-column-width/tasks.md @@ -0,0 +1,48 @@ +## 1. Type Definition Updates + +- [x] 1.1 Update `ConfigurationSection` interface in `src/components/toolbars/ToolbarConfig.ts` to add `"numberInput"` to type union +- [x] 1.2 Add optional fields to `ConfigurationSection` interface: `min?: number`, `max?: number`, `step?: number`, `placeholder?: string`, `unit?: string` +- [x] 1.3 Update `ConfigurationSection` interface in `src/components/toolbars/components/ConfigurationDropdown.tsx` to match the updated type definition +- [x] 1.4 Update `getCurrentValue` return type to allow `string | number | null` instead of just `string | null` + +## 2. UI Component Implementation + +- [x] 2.1 Add number input rendering case in `ConfigurationDropdown.tsx` after the dropdown case (around line 73) +- [x] 2.2 Create wrapper div with className `configuration-number-input` containing the input, unit label, and clear button +- [x] 2.3 Implement number input with type="number", min/max/step/placeholder props from section config +- [x] 2.4 Implement conditional unit label display using `configuration-unit` className when `section.unit` exists +- [x] 2.5 Implement conditional clear button (×) with `configuration-clear-button` className, shown only when `currentValue !== null` +- [x] 2.6 Wire up onChange handler to call `section.onChange(e.target.value)` on input change +- [x] 2.7 Wire up clear button onClick handler to call `section.onChange("")` to reset to auto width + +## 3. Styling + +- [x] 3.1 Add `.configuration-number-input` styles in `ConfigurationDropdown.scss` with flex layout and 4px gap +- [x] 3.2 Add `.configuration-input` styles matching `.configuration-select` with padding, borders, transitions, and focus states +- [x] 3.3 Add number input specific styles to hide spinner arrows (`::-webkit-inner-spin-button`, `::-webkit-outer-spin-button`, `-moz-appearance: textfield`) +- [x] 3.4 Add `.configuration-input::placeholder` styles with gray color and italic font +- [x] 3.5 Add `.configuration-unit` styles with 12px font size, gray color, and nowrap +- [x] 3.6 Add `.configuration-clear-button` styles with padding, borders, hover/active states matching the design + +## 4. Cell Configuration Logic + +- [x] 4.1 Add new configuration section object in `createCellConfigurationSections()` function in `src/components/toolbars/helpers/configurationHelpers.ts` after `cellBorderWidth` +- [x] 4.2 Set section id to `"cellWidth"`, label to `"Column Width"`, type to `"numberInput"` +- [x] 4.3 Set min to 25, max to 1000, step to 1, placeholder to "Auto", unit to "px" +- [x] 4.4 Implement `getCurrentValue` function that calls `getCellAttributes(editor)`, extracts `colwidth` array, and returns `colwidth[0]` or null +- [x] 4.5 Implement `onChange` function that parses input value, validates it, clamps to min/max range, and calls `setCellAttribute("colwidth", [value])` or `setCellAttribute("colwidth", null)` for empty input +- [x] 4.6 Handle edge case where input is empty string by calling `setCellAttribute("colwidth", null)` to reset to auto width +- [x] 4.7 Handle colspan cells by creating colwidth array with single value `[clampedValue]` regardless of colspan + +## 5. Testing & Verification + +- [x] 5.1 Test setting column width to valid value (e.g., 150px) and verify column resizes +- [x] 5.2 Test clearing column width via clear button and verify column auto-sizes +- [x] 5.3 Test entering value below minimum (e.g., 10) and verify it clamps to 25 +- [x] 5.4 Test entering value above maximum (e.g., 2000) and verify it clamps to 1000 +- [x] 5.5 Test entering invalid input (e.g., "abc") and verify it's ignored +- [x] 5.6 Test with colspan cell (colspan=3) and verify only first column width is set +- [x] 5.7 Test that width persists after save/reload of document +- [x] 5.8 Test that clear button visibility toggles correctly (hidden when empty, shown when value exists) +- [x] 5.9 Test that placeholder "Auto" displays when input is empty +- [x] 5.10 Test keyboard navigation and accessibility of number input control diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/README.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/README.md new file mode 100644 index 0000000000..4bcf59afdc --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/README.md @@ -0,0 +1,84 @@ +# Normalize Empty Content + +**Status:** Proposed +**Type:** Bug Fix +**Impact:** Breaking (Validation behavior) +**Effort:** ~3.5 hours + +## Quick Summary + +Fix the rich text editor to save empty content as `""` instead of `

`, ensuring correct validation and consistent data representation. + +## Problem + +When users delete all content, the editor currently saves `

` to the database. This breaks: + +- Required field validation (incorrectly passes) +- Empty checks (`if (value)` returns true) +- Data consistency (semantically empty ≠ empty string) + +## Solution + +Use Tiptap's `editor.isEmpty` property to return `""` when content is empty, and normalize comparison logic to treat `""` and `

` as equivalent. + +## Files + +- [`proposal.md`](./proposal.md) - Problem statement, scope, and impact +- [`design.md`](./design.md) - Technical design, decisions, and architecture +- [`tasks.md`](./tasks.md) - Implementation checklist and effort estimate + +## Key Decisions + +1. **Trust Tiptap's isEmpty** - Use built-in check, no custom logic +2. **Normalize at persistence** - Convert to `""` when saving, not loading +3. **Lazy migration** - Existing `

` records normalize on next edit +4. **No version bump** - This Tiptap version is unreleased + +## Changes Required + +### Editor.tsx (line 237-240) + +```typescript +// Before: +const html = editor.getHTML(); + +// After: +const html = editor.isEmpty ? "" : editor.getHTML(); +``` + +### EditorWrapper.tsx (line 48-60) + +```typescript +// Add normalization in comparison: +const normalizeEmpty = (val?: string) => (!val || val === "

" ? "" : val); + +const current = normalizeEmpty(stringAttribute.value); +const incoming = normalizeEmpty(html); +if (current !== incoming) { + stringAttribute.setValue(incoming); +} +``` + +## Testing + +- ✓ Unit tests for empty content behavior +- ✓ E2E test for persistence +- ✓ Manual validation testing + +## Breaking Change + +**Required field validation will now correctly reject empty content.** + +Previously: `

` passed validation ❌ +After fix: `""` fails validation ✓ + +This is desired behavior (bug fix), but may affect existing forms. + +## Next Steps + +1. Review proposal and design +2. Approve or request changes +3. Implement following tasks.md +4. Test thoroughly +5. Update CHANGELOG +6. Merge to main branch diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/design.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/design.md new file mode 100644 index 0000000000..fd696c3c70 --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/design.md @@ -0,0 +1,374 @@ +## Design Overview + +The fix normalizes empty editor content at the persistence boundary by checking Tiptap's `isEmpty` property and returning an empty string instead of `

`. A secondary normalization in the comparison layer prevents unnecessary updates when values are semantically equivalent. + +## Architecture + +``` +User Deletes Content + ↓ +Tiptap Editor (internal state:

) + ↓ +onUpdate Event Fires + ↓ +┌─────────────────────────────────────────┐ +│ Editor.tsx (PRIMARY FIX) │ +│ │ +│ onUpdate: ({ editor }) => { │ +│ const html = editor.isEmpty │ +│ ? "" │ ← Use isEmpty check +│ : editor.getHTML(); │ +│ onUpdate?.(html); │ +│ } │ +└─────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────┐ +│ EditorWrapper.tsx (OPTIMIZATION) │ +│ │ +│ const current = stringAttribute.value || ""; │ +│ const incoming = html || ""; │ ← Normalize comparison +│ if (current !== incoming) { │ +│ stringAttribute.setValue(incoming); │ +│ } │ +└─────────────────────────────────────────┘ + ↓ +Mendix EditableValue.setValue("") + ↓ +Database: RichTextField = "" ✓ +``` + +## Key Design Decisions + +### 1. Use Tiptap's `isEmpty` Property + +**Decision:** Trust Tiptap's built-in `editor.isEmpty` check rather than implementing custom logic. + +**Rationale:** + +- Tiptap's `isEmpty` is battle-tested and handles edge cases correctly +- Considers semantic emptiness (ignores empty marks, formatting-only nodes) +- No need to reinvent the wheel with regex or string parsing +- Future-proof as Tiptap evolves + +**Edge Cases Handled by isEmpty:** + +- `

` → true +- `

` → true (empty marks) +- `

` → Implementation depends on Tiptap's definition +- `


` → false (has content - soft break) +- `` → false (has structure) + +### 2. Normalize at Persistence Boundary + +**Decision:** Convert `

` to `""` in the `onUpdate` handler, not on load. + +**Rationale:** + +- Centralizes normalization at the point where data leaves the editor +- Tiptap can maintain whatever internal state it needs +- Rendering is unaffected (Tiptap will render `

` internally regardless) +- Single source of truth for what gets persisted + +**Rejected Alternative:** Normalize on load (`defaultValue`) + +- Would require changing initialization logic +- Adds complexity without user-visible benefit +- Lazy migration is sufficient + +### 3. Comparison Normalization + +**Decision:** Treat `undefined`, `""`, and `

` as equivalent in comparison logic. + +**Why:** + +```javascript +// Without normalization: +stringAttribute.value = ""; +html = "

"; +"" !== "

" // → triggers setValue() unnecessarily + +// With normalization: +current = "" || "" = ""; +incoming = "

" || "" = ""; // Wait, this doesn't work! + +// Correct normalization: +const normalize = (val?: string) => val || ""; +current = normalize(stringAttribute.value); // "" or "

" → "" +incoming = normalize(html); // "" or "

" → "" +``` + +**Actually, we need:** + +```javascript +const normalize = (val?: string) => { + if (!val || val === "

") return ""; + return val; +}; +``` + +**Rationale:** + +- Prevents duplicate setValue() calls during transition period +- Reduces unnecessary database writes +- Handles both `""` and `

` gracefully + +### 4. Lazy Migration + +**Decision:** Don't proactively migrate existing `

` records. + +**Rationale:** + +- Records with `

` load and render fine +- They normalize to `""` on next edit +- No risk of data corruption +- No need for migration script +- Gradual, safe transition + +## Implementation Details + +### File: `src/components/Editor.tsx` + +**Current Code (Line 237-240):** + +```typescript +onUpdate: ({ editor }) => { + const html = editor.getHTML(); + onUpdate?.(html); +}; +``` + +**New Code:** + +```typescript +onUpdate: ({ editor }) => { + const html = editor.isEmpty ? "" : editor.getHTML(); + onUpdate?.(html); +}; +``` + +**Impact:** + +- Every content change triggers this check +- Performance: Negligible (boolean property access) +- Testing: Unit test can mock editor.isEmpty + +### File: `src/components/EditorWrapper.tsx` + +**Current Code (Line 48-60):** + +```typescript +const [setAttributeValueDebounce] = useDebounceWithStatus( + (html?: string) => { + if (stringAttribute.value !== html) { + stringAttribute.setValue(html); + if (onChangeType === "onDataChange") { + executeAction(onChange); + } + } + }, + 200, + false +); +``` + +**New Code:** + +```typescript +const normalizeEmpty = (val?: string): string => { + if (!val || val === "

") return ""; + return val; +}; + +const [setAttributeValueDebounce] = useDebounceWithStatus( + (html?: string) => { + const current = normalizeEmpty(stringAttribute.value); + const incoming = normalizeEmpty(html); + + if (current !== incoming) { + stringAttribute.setValue(incoming); + if (onChangeType === "onDataChange") { + executeAction(onChange); + } + } + }, + 200, + false +); +``` + +**Impact:** + +- Prevents spurious updates when switching representations +- Helper function can be tested independently +- Encapsulates normalization logic + +## Testing Strategy + +### Unit Tests + +**File:** `src/__tests__/RichText.spec.tsx` + +New test cases: + +```typescript +describe("Empty content handling", () => { + it("returns empty string when editor is empty", () => { + const onUpdate = jest.fn(); + const editor = createMockEditor({ isEmpty: true, getHTML: () => "

" }); + + // Trigger onUpdate + editor.onUpdate({ editor }); + + expect(onUpdate).toHaveBeenCalledWith(""); + }); + + it("returns HTML when editor has content", () => { + const onUpdate = jest.fn(); + const editor = createMockEditor({ + isEmpty: false, + getHTML: () => "

Hello

" + }); + + editor.onUpdate({ editor }); + + expect(onUpdate).toHaveBeenCalledWith("

Hello

"); + }); + + it("normalizes

in comparison", () => { + const setValue = jest.fn(); + const stringAttribute = { value: "

", setValue }; + + // Trigger update with empty string + handleUpdate(""); + + // Should NOT call setValue (semantically equivalent) + expect(setValue).not.toHaveBeenCalled(); + }); +}); +``` + +### E2E Tests + +**File:** `e2e/RichText.spec.js` + +New test: + +```javascript +test("empty content persists as empty string", async ({ page }) => { + await page.goto("/p/rich-text-test"); + await waitForMendixApp(page); + + // Type content + const editor = page.locator(".tiptap-editor"); + await editor.click(); + await page.keyboard.type("Hello World"); + + // Delete all content + await page.keyboard.press("Control+A"); + await page.keyboard.press("Backspace"); + + // Blur to trigger save + await page.keyboard.press("Tab"); + + // Verify the attribute value is empty string + const isEmpty = await page.evaluate(() => { + // Access Mendix test helper + const context = window.mx.data.get(/* richTextEntity */); + const value = context.get("RichTextField"); + return value === "" || value === null; + }); + + expect(isEmpty).toBe(true); +}); +``` + +## Risks and Mitigations + +### Risk 1: Validation Breaking Change + +**Risk:** Forms with required RichText that accepted `

` will now fail validation. + +**Mitigation:** + +- This is the desired behavior (bug fix) +- Document as breaking change in CHANGELOG +- Note: Since this is unreleased Tiptap version, no version bump needed + +**Verdict:** Acceptable - improves correctness + +### Risk 2: Tiptap's isEmpty Edge Cases + +**Risk:** Tiptap's `isEmpty` might have edge cases we don't anticipate. + +**Mitigation:** + +- Trust Tiptap's well-tested implementation +- Add unit tests for known edge cases +- Monitor issue reports after release + +**Verdict:** Low risk - Tiptap is mature + +### Risk 3: Performance + +**Risk:** Extra checks on every keystroke. + +**Analysis:** + +- `editor.isEmpty` is a boolean property (O(1)) +- Normalization is string comparison (O(n), but n is small) +- Already debounced (200ms) + +**Verdict:** Negligible impact + +## Alternative Approaches Considered + +### Alternative 1: Normalize on Load + +```typescript +const editor = useEditor({ + content: normalizeEmpty(defaultValue), + ... +}); +``` + +**Rejected because:** + +- Doesn't prevent `

` from being saved in the first place +- Adds complexity to initialization +- No user-visible benefit + +### Alternative 2: Custom isEmpty Logic + +```typescript +const isEmpty = (html: string) => { + return !html || html === "

" || html.trim() === ""; +}; +``` + +**Rejected because:** + +- Fragile (what about `

`, `


`, etc?) +- Reinvents Tiptap's tested logic +- Harder to maintain + +### Alternative 3: Database Migration + +Run a script to convert all `

` → `""` in database. + +**Rejected because:** + +- Unnecessary risk +- Lazy migration is safer +- No user-visible benefit + +## Conclusion + +This design provides a minimal, safe fix for the empty content bug by leveraging Tiptap's built-in `isEmpty` property and normalizing at the persistence boundary. The secondary comparison normalization prevents unnecessary updates during the transition period. + +**Key Benefits:** + +- ✓ Simple implementation (two small changes) +- ✓ Low risk (uses proven Tiptap API) +- ✓ Testable (unit + E2E coverage) +- ✓ Backward compatible (lazy migration) +- ✓ Fixes validation correctness diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/proposal.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/proposal.md new file mode 100644 index 0000000000..5b8eb44b30 --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/proposal.md @@ -0,0 +1,58 @@ +## Why + +When users delete all content from the rich text editor, it currently saves as `

` instead of an empty string. This creates several problems: + +1. **Validation issues**: Required fields incorrectly pass validation with `

` as the value +2. **Comparison inconsistencies**: Checking `if (value === "")` or `if (!value)` fails because `

` is truthy +3. **Data semantics**: Semantically empty content should be represented as an empty string, not HTML markup +4. **Developer confusion**: Backend logic must special-case `

` when checking for empty content + +This is a bug in the current implementation where Tiptap's `getHTML()` returns `

` for an empty editor, but we should normalize this to an empty string before persisting to the database. + +## What Changes + +- Modify `onUpdate` handler in `Editor.tsx` to return empty string (`""`) when editor is empty using Tiptap's built-in `isEmpty` property +- Normalize value comparison in `EditorWrapper.tsx` to treat `undefined`, `""`, and `

` as equivalent, preventing unnecessary database updates +- Add unit tests for empty content handling +- Add E2E test to verify empty content persists as empty string + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + +- `rich-text-persistence`: Empty editor content now correctly saves as empty string (`""`) instead of `

`, ensuring proper validation and consistent data representation + +## Impact + +**Affected Files:** + +- `src/components/Editor.tsx` - Modify `onUpdate` handler to check `editor.isEmpty` and return `""` when true +- `src/components/EditorWrapper.tsx` - Normalize value comparison to prevent spurious updates +- `src/__tests__/RichText.spec.tsx` - Add tests for empty content handling +- `e2e/RichText.spec.js` - Add E2E test for empty content persistence + +**User Impact:** + +- **Validation improvement** (Breaking): Required RichText fields will now correctly reject empty content. Previously, `

` incorrectly passed validation. +- **Data consistency**: Empty content is consistently represented as `""` across all scenarios +- **Backward compatibility**: Existing records with `

` will load normally and get normalized to `""` on next edit (lazy migration) + +**Technical Impact:** + +- Leverages Tiptap's built-in `isEmpty` property (battle-tested, no custom logic needed) +- Minimal performance impact (one boolean property check per update) +- No changes to Tiptap configuration or extensions +- No version bump needed (Tiptap version is unreleased) + +## Migration + +**Lazy Migration Strategy:** + +- Existing records with `

` in database remain unchanged +- Values normalize to `""` when user next edits the content +- No database migration script required +- Safe, gradual transition diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/tasks.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/tasks.md new file mode 100644 index 0000000000..b3ab006932 --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-06-normalize-empty-content/tasks.md @@ -0,0 +1,107 @@ +## Implementation Tasks + +### Core Implementation + +- [ ] **Modify Editor.tsx onUpdate handler** + - File: `src/components/Editor.tsx` + - Line: 237-240 + - Change: Replace `const html = editor.getHTML();` with `const html = editor.isEmpty ? "" : editor.getHTML();` + - Validation: Verify editor.isEmpty property is accessible via TypeScript types + +- [ ] **Add normalizeEmpty helper in EditorWrapper.tsx** + - File: `src/components/EditorWrapper.tsx` + - Add helper function above component: + ```typescript + function normalizeEmpty(value?: string): string { + if (!value || value === "

") return ""; + return value; + } + ``` + - Update comparison logic in setAttributeValueDebounce (line 48-60) + - Use `normalizeEmpty()` for both current and incoming values + +### Testing + +- [ ] **Add unit tests for empty content handling** + - File: `src/__tests__/RichText.spec.tsx` + - Test cases: + - Empty editor returns empty string + - Editor with content returns HTML + - Normalization treats "" and "

" as equivalent + - Edge case: whitespace-only content behavior + +- [ ] **Add E2E test for empty content persistence** + - File: `e2e/RichText.spec.js` + - Test scenario: + - Type content in editor + - Delete all content + - Blur to trigger save + - Verify attribute value is empty string (not "

") + +- [ ] **Manual testing checklist** + - [ ] Create new record with required RichText field + - [ ] Try to save with empty content → validation should fail + - [ ] Add content and save → should succeed + - [ ] Delete all content and save → validation should fail + - [ ] Verify database stores "" not "

" + - [ ] Load existing record with "

" → should render correctly + - [ ] Edit and save → should normalize to "" + - [ ] Test undo/redo with empty content + - [ ] Test code view → code view toggle with empty content + +### Documentation + +- [ ] **Update CHANGELOG.md** + - Add entry under "Fixed" section: + - "Empty editor content now correctly saves as empty string instead of `

`, fixing validation and comparison issues" + - Note breaking change: Required field validation now correctly rejects empty content + +- [ ] **Update README.md (if needed)** + - Check if empty content behavior is documented + - Add note about empty value representation if relevant + +### Code Review Checklist + +- [ ] Verify isEmpty property is correctly used +- [ ] Confirm no TypeScript errors +- [ ] Check that normalization logic handles all cases (undefined, "", "

") +- [ ] Verify debounce behavior is maintained +- [ ] Ensure onChange events still fire correctly +- [ ] Confirm no regression in existing functionality +- [ ] Review test coverage for edge cases + +### Pre-Merge Verification + +- [ ] All unit tests pass: `pnpm test` +- [ ] All E2E tests pass: `pnpm e2e` +- [ ] Linting passes: `pnpm lint` +- [ ] Format check passes: `pnpm format` +- [ ] Build succeeds: `pnpm build` +- [ ] Manual testing completed +- [ ] CHANGELOG updated +- [ ] No console errors in browser during testing + +## Estimated Effort + +- Core implementation: **30 minutes** +- Unit tests: **1 hour** +- E2E tests: **1 hour** +- Manual testing: **30 minutes** +- Documentation: **15 minutes** +- Code review and fixes: **30 minutes** + +**Total: ~3.5 hours** + +## Dependencies + +None - this is a self-contained fix within the RichText widget. + +## Success Criteria + +✓ Empty editor persists as `""` instead of `

` +✓ Required field validation correctly rejects empty content +✓ Existing `

` records load and normalize on next edit +✓ No regression in content editing functionality +✓ All tests pass (unit + E2E) +✓ No TypeScript errors +✓ Code review approved diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/.openspec.yaml b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/.openspec.yaml new file mode 100644 index 0000000000..8803b473ec --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-12 diff --git a/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/design.md b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/design.md new file mode 100644 index 0000000000..1076cbebc3 --- /dev/null +++ b/packages/pluggableWidgets/rich-text-web/openspec/changes/archive/2026-07-12-auto-cycle-list-styles/design.md @@ -0,0 +1,176 @@ +## Context + +Rich text widget renders ordered (`
    `) and unordered (`