Skip to content

Commit aef4699

Browse files
author
gjulivan
committed
chore: add i18, fix stylings, add help, fix table resize, fix image upload tabs
1 parent 3b77e9e commit aef4699

121 files changed

Lines changed: 8425 additions & 7107 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/pluggableWidgets/rich-text-web/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1414

1515
- We added new configuration to allow users to use class names instead of inline styling in generated HTML to support strict CSP.
1616

17+
- We added a help button to the toolbar that opens a dialog listing the editor's keyboard shortcuts. The button is shown when the full toolbar is enabled and can be turned off with the new "Keyboard shortcuts" setting.
18+
19+
- We added translations for the toolbar, dialogs, and help texts. The editor UI now follows the page language automatically, with bundled translations for English, Dutch, German, French, and Spanish (falling back to English).
20+
21+
- We added a clear button to every color picker (font color, font background, and the table and cell background and border color pickers) so a color can be reset to none.
22+
1723
### Fixed
1824

25+
- We fixed the image dialog to only show the image source tabs that are available: the Media Library tab now appears only when an image data source is configured, and the Upload tab is hidden when default upload is disabled.
26+
1927
- 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.
2028

2129
- We fixed an issue where empty editor content was saving as `<p></p>` 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.
2230

2331
- We fixed an issue where the editor pasting back the whole sentence instead of the single copied word
2432

33+
- We fixed an issue where table header cells did not honor background color and border (color, style, width) configuration. Header cells can now be styled the same way as regular cells.
34+
2535
### Changed
2636

37+
- We aligned the image dialog's "Upload" tab dropzone with the File Uploader widget's dropzone so both share the same look, states, and messages.
38+
2739
- 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.
2840

2941
## [4.12.0] - 2026-04-22

packages/pluggableWidgets/rich-text-web/e2e/RichText.spec.js

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ test.describe("RichText", () => {
3333
await expect(page.locator(".mx-name-richText1")).toBeVisible();
3434
await expect(page.locator(".mx-name-richText1")).toHaveScreenshot(`bottomToolbarAdvancedMode.png`);
3535

36-
await page.click(".mx-name-richText1 .ql-toolbar button.ql-image");
37-
await expect(page.locator(".widget-rich-text .widget-rich-text-modal-body").first()).toHaveScreenshot(
36+
await page.click('.mx-name-richText1 .tiptap-toolbar button[title="Insert Image"]');
37+
await expect(page.locator(".mx-name-richText1 .toolbar-dialog.image-dialog").first()).toHaveScreenshot(
3838
`insertImageDialog.png`
3939
);
4040
});
@@ -48,8 +48,8 @@ test.describe("RichText", () => {
4848
await expect(page.locator(".mx-name-richText4")).toBeVisible();
4949
await expect(page.locator(".mx-name-richText4")).toHaveScreenshot(`toolbarAdvancedMode.png`);
5050

51-
await page.click(".mx-name-richText1 .ql-toolbar button.ql-view-code");
52-
await expect(page.locator(".widget-rich-text .widget-rich-text-modal-body").first()).toHaveScreenshot(
51+
await page.click('.mx-name-richText4 .tiptap-toolbar button[title="View/Edit Code"]');
52+
await expect(page.locator(".mx-name-richText4 .highlighted-code-editor").first()).toHaveScreenshot(
5353
`viewCodeDialog.png`
5454
);
5555
});
@@ -128,21 +128,45 @@ test.describe("RichText", () => {
128128
test("checks that class mode editor output uses CSS classes instead of inline styles", async ({ page }) => {
129129
await page.goto("/p/classmode");
130130
await waitForMendixApp(page);
131-
const html = await page.locator(".mx-name-richText1 .ql-editor").innerHTML();
132-
expect(html).toMatch(/class="ql-color-/);
133-
expect(html).toMatch(/class="ql-bg-/);
134-
expect(html).toMatch(/class="ql-indent-/);
135-
expect(html).toMatch(/data-style-format="class"/);
136-
expect(html).not.toMatch(/style="color:/);
137-
expect(html).not.toMatch(/style="background-color:/);
138-
expect(html).not.toMatch(/style="padding-left:/);
131+
132+
const editor = page.locator(".mx-name-richText1 .tiptap");
133+
await expect(editor).toBeVisible();
134+
135+
// Apply text color, highlight and indent to the first block so the
136+
// class-based output can be asserted. Re-select before each command
137+
// because clicking a toolbar control collapses the DOM selection.
138+
const firstBlock = editor.locator("h1, h2, h3, p").first();
139+
140+
await firstBlock.click({ clickCount: 3 });
141+
await page.click('.mx-name-richText1 button[title="Text Color"]');
142+
await page.locator(".color-picker-dropdown div[title]").nth(10).click();
143+
144+
await firstBlock.click({ clickCount: 3 });
145+
await page.click('.mx-name-richText1 button[title="Background Color"]');
146+
await page.locator(".color-picker-dropdown div[title]").nth(10).click();
147+
148+
await firstBlock.click({ clickCount: 3 });
149+
await page.click('.mx-name-richText1 button[title="Increase Indent"]');
150+
151+
const html = await editor.innerHTML();
152+
153+
// Class mode emits class + data-* attributes, not inline styles.
154+
expect(html).toMatch(/class="[^"]*has-text-color/);
155+
expect(html).toMatch(/data-text-color="/);
156+
expect(html).toMatch(/class="[^"]*has-text-highlight/);
157+
expect(html).toMatch(/data-text-highlight="/);
158+
expect(html).toMatch(/class="[^"]*indent-\d/);
159+
expect(html).toMatch(/data-indent="/);
160+
expect(html).not.toMatch(/style="[^"]*color:/);
161+
expect(html).not.toMatch(/style="[^"]*background-color:/);
162+
expect(html).not.toMatch(/style="[^"]*padding-left:/);
139163
});
140164

141165
test("compares with a screenshot baseline of the View/Edit Code dialog in class mode", async ({ page }) => {
142166
await page.goto("/p/classmode");
143167
await waitForMendixApp(page);
144-
await page.click(".mx-name-richText1 .ql-toolbar button.ql-view-code");
145-
await expect(page.locator(".widget-rich-text .widget-rich-text-modal-body").first()).toHaveScreenshot(
168+
await page.click('.mx-name-richText1 .tiptap-toolbar button[title="View/Edit Code"]');
169+
await expect(page.locator(".mx-name-richText1 .highlighted-code-editor").first()).toHaveScreenshot(
146170
`classModeViewCodeDialog.png`
147171
);
148172
});
@@ -153,21 +177,18 @@ test.describe("RichText", () => {
153177

154178
await page.click(".mx-navbar-item [title='Demo']");
155179

156-
await page.click(".mx-name-customWidget1 .ql-toolbar button.ql-video");
157-
await expect(page.locator(".widget-rich-text .widget-rich-text-modal-body").first()).toHaveScreenshot(
180+
await page.click('.mx-name-customWidget1 .tiptap-toolbar button[title="Insert YouTube Video"]');
181+
await expect(page.locator(".toolbar-dialog.video-dialog").first()).toHaveScreenshot(
158182
`richTextDialogInsidePopup.png`
159183
);
160184

161-
await page.click(".widget-rich-text .widget-rich-text-modal-body #rich-text-video-src-input");
162-
await page
163-
.locator(".widget-rich-text .widget-rich-text-modal-body #rich-text-video-src-input")
164-
.fill("https://www.mendix.com");
165-
await expect(page.locator(".widget-rich-text .widget-rich-text-modal-body").first()).toHaveScreenshot(
185+
await page.locator(".toolbar-dialog.video-dialog #video-url").fill("https://www.mendix.com");
186+
await expect(page.locator(".toolbar-dialog.video-dialog").first()).toHaveScreenshot(
166187
`richTextDialogInsidePopupEdit.png`
167188
);
168189
});
169190

170-
test("empty content persists as empty string, not <p></p>", async ({ page }) => {
191+
test("clearing all content leaves the editor empty, not a stray <p></p>", async ({ page }) => {
171192
await page.goto("/");
172193
await waitForMendixApp(page);
173194
await page.click("text=Generate Data");
@@ -179,27 +200,23 @@ test.describe("RichText", () => {
179200
await editor.scrollIntoViewIfNeeded();
180201
await expect(editor).toBeVisible();
181202

182-
// Click into the editor and clear all content
203+
// Click into the editor and clear all content. selectText() reliably
204+
// selects the editor contents across platforms, unlike Control+A which
205+
// depends on OS focus behaviour.
183206
await editor.click();
184-
await page.keyboard.press("Control+A");
207+
await editor.selectText();
185208
await page.keyboard.press("Backspace");
186209

187-
// Blur the editor to trigger save
210+
// Blur the editor to trigger the save/normalize path.
188211
await page.keyboard.press("Tab");
189212
await page.waitForTimeout(500);
190213

191-
// The editor should now be empty (visual check)
192-
const textContent = await editor.textContent();
193-
expect(textContent?.trim() || "").toBe("");
194-
195-
// Reload the page to verify persistence
196-
await page.reload();
197-
await waitForMendixApp(page);
198-
199-
// The editor should still be empty after reload
200-
const reloadedEditor = page.locator(".mx-name-richText1 .tiptap");
201-
await expect(reloadedEditor).toBeVisible();
202-
const reloadedContent = await reloadedEditor.textContent();
203-
expect(reloadedContent?.trim() || "").toBe("");
214+
// The editor should now be empty. Tiptap keeps a placeholder paragraph
215+
// in the DOM, but the widget normalizes that empty paragraph to an
216+
// empty string on save (see normalizeEmpty in EditorWrapper).
217+
expect((await editor.textContent())?.trim() || "").toBe("");
218+
// No text nodes remain — only an empty placeholder paragraph/break.
219+
const strippedText = (await editor.innerHTML()).replace(/<[^>]*>/g, "").trim();
220+
expect(strippedText).toBe("");
204221
});
205222
});
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-07-21
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
## Context
2+
3+
The just-completed `add-table-size-resize` change added `cellWidth` — a CSS-size string attribute on `TableCellBackgroundColor`, rendered as `width` on the cell and routed through `<colgroup>` (authoritative under `table-layout: fixed`). Cell height is the natural mirror.
4+
5+
## Goals / Non-Goals
6+
7+
**Goals:**
8+
9+
- Add a cell height control that accepts any CSS size string (`100px`, `50%`, bare number → px).
10+
- Mirror `cellWidth`: same attribute shape, same `normalizeCssSize` validation, same commit-on-blur input.
11+
12+
**Non-Goals:**
13+
14+
- A dedicated table-row extension / true `<tr>` row-height attribute (no row extension exists; cell height achieves the visible result).
15+
- Fixed/clipping height — table cell height is a minimum; content still grows the cell.
16+
17+
## Decisions
18+
19+
### Decision 1: Cell attribute `cellHeight`, rendered on `<td>` style
20+
21+
**Rationale:** Mirrors `cellWidth` exactly. Unlike width, height needs **no** `<colgroup>` routing — `table-layout: fixed` only governs column widths, so `height` on the `<td>` works directly. Simpler than width.
22+
23+
### Decision 2: "Column Height" label (matches the existing "Column Width")
24+
25+
**Rationale:** The width control is labeled "Column Width"; the paired control reads "Column Height" for UI symmetry, even though height technically applies to the cell/row. Terminology consistency beats pedantic accuracy here.
26+
27+
### Decision 3: Height is a minimum; whole row follows
28+
29+
**Rationale / behavior notes:**
30+
31+
- Browsers treat table-cell `height` as a minimum — the cell grows if content is taller (no clipping). Consistent with the table `min-height` decision.
32+
- A `<td>` height stretches its entire row (sibling cells share the row height). So setting "cell height" visually behaves as row height — which is what a user asking for "column/row height" expects.
33+
34+
## Migration Plan
35+
36+
Additive. Existing tables (no `cellHeight`) unaffected. No data model or breaking changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## Why
2+
3+
Users can set column (cell) width via a CSS-size text input, but there is no equivalent control for cell height. Completing the pair lets users size table cells in both dimensions. (Requested as "column height"; height in a table applies to cells/rows, not columns — see design.md.)
4+
5+
## What Changes
6+
7+
- Add a `cellHeight` string attribute to the table cell (`TableCellBackgroundColor`), mirroring `cellWidth`.
8+
- Render it as `height: <value>` in the cell's inline style (and `data-cell-height` in class format).
9+
- Add a "Column Height" text input to the cell configuration dropdown, reusing the `textInput` type + `normalizeCssSize` validation + commit-on-blur behavior.
10+
11+
## Capabilities
12+
13+
### Modified Capabilities
14+
15+
- `table-size-control`: add a cell height control alongside the existing column width control.
16+
17+
## Impact
18+
19+
**Affected Files:**
20+
21+
- `src/extensions/TableCellBackgroundColor.ts` — add `cellHeight` attribute; render into cell style + `data-cell-height`.
22+
- `src/components/toolbars/helpers/configurationHelpers.ts` — add "Column Height" text input section.
23+
24+
**User Impact:** additive; existing tables unaffected. No colgroup/NodeView changes — height lives directly on the cell (unlike width, which routes through `<colgroup>`).
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Column height as CSS size
4+
5+
The cell configuration dropdown SHALL include a "Column Height" text input that stores the height as a CSS size string (e.g. `100px`, `50%`) on the cell's inline style, applying a minimum height to the cell's row.
6+
7+
#### Scenario: Setting a cell height
8+
9+
- **WHEN** user enters a valid CSS size in the "Column Height" input for a cell
10+
- **THEN** the cell (and its row) renders at least that tall
11+
12+
#### Scenario: Row grows beyond the set height
13+
14+
- **WHEN** a cell has a height set and its content requires more vertical space
15+
- **THEN** the cell grows to fit the content, exceeding the set height
16+
17+
#### Scenario: Bare number treated as pixels
18+
19+
- **WHEN** user enters a bare number such as `80` for the cell height
20+
- **THEN** the value is normalized to `80px` and applied
21+
22+
#### Scenario: Invalid or unsafe height is rejected
23+
24+
- **WHEN** user enters a value that is not a valid CSS size
25+
- **THEN** the value is not applied
26+
27+
#### Scenario: Clearing cell height
28+
29+
- **WHEN** user clears the "Column Height" input
30+
- **THEN** the cell height is reset to auto (null)
31+
32+
#### Scenario: Height serializes in the active style-data format
33+
34+
- **WHEN** a cell height is set
35+
- **THEN** it is rendered as inline `height` in `inline` mode and as `data-cell-height` in `class` mode, consistent with the column width control
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## 1. Cell height attribute
2+
3+
- [x] 1.1 Add `cellHeight` string attribute to `TableCellBackgroundColor.addAttributes()` (parse from `style.height` / `data-cell-height`; renderHTML returns {} — handled in main renderHTML), mirroring `cellWidth`
4+
- [x] 1.2 In `renderHTML`, append `height: ${cellHeight}` to the cell style string (both formats)
5+
- [x] 1.3 In class mode, emit `data-cell-height`
6+
7+
## 2. Cell configuration control
8+
9+
- [x] 2.1 Add "Column Height" `textInput` section after `cellWidth` in `createCellConfigurationSections()` (placeholder e.g. "Auto (e.g. 100px, 50%)")
10+
- [x] 2.2 `getCurrentValue``getCellAttributes(editor)?.cellHeight`; `onChange``normalizeCssSize` + `setCellAttribute("cellHeight", …)` (empty clears to null, invalid ignored)
11+
12+
## 3. Verification
13+
14+
- [x] 3.1 Typecheck + lint clean; unit suite passes (82 tests)
15+
- [x] 3.2 Manual: set cell height (px + %) → row grows to that height; content taller → grows past it; clear → auto (verified by user in Mendix editor)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-07-21

0 commit comments

Comments
 (0)