From a848f5b76c3dd730116cb0f6d970470abbfc8d66 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:03:58 +0200 Subject: [PATCH 01/16] build(barcode-generator-web): add @bwip-js/browser dependency Adds the tree-shakeable bwip-js browser entry, the only maintained library with native GS1 Data Matrix support, for upcoming Data Matrix generation. Co-Authored-By: Claude Opus 4.8 --- .../pluggableWidgets/barcode-generator-web/package.json | 1 + pnpm-lock.yaml | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/packages/pluggableWidgets/barcode-generator-web/package.json b/packages/pluggableWidgets/barcode-generator-web/package.json index 9c2d22b6df..c13e6c0c55 100644 --- a/packages/pluggableWidgets/barcode-generator-web/package.json +++ b/packages/pluggableWidgets/barcode-generator-web/package.json @@ -43,6 +43,7 @@ "verify": "rui-verify-package-format" }, "dependencies": { + "@bwip-js/browser": "^4.11.2", "classnames": "^2.5.1", "jsbarcode": "^3.12.1", "qrcode.react": "^4.2.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 331dfd1a66..9ec3ceefd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -554,6 +554,9 @@ importers: packages/pluggableWidgets/barcode-generator-web: dependencies: + '@bwip-js/browser': + specifier: ^4.11.2 + version: 4.11.2 classnames: specifier: ^2.5.1 version: 2.5.1 @@ -3877,6 +3880,9 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@bwip-js/browser@4.11.2': + resolution: {integrity: sha512-+6wZZY218c0Q6e9xjpESAs851ezWp8pZp2vS7WSjwX0GBOeGoT04NbvgHWPvtg4NIWXEIxoOgSBPnhmkFXhyNg==} + '@choojs/findup@0.2.1': resolution: {integrity: sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==} hasBin: true @@ -11331,6 +11337,8 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@bwip-js/browser@4.11.2': {} + '@choojs/findup@0.2.1': dependencies: commander: 2.20.3 From 6d9f9854b8c30c4d7f0bb9c6cf700c2c3d5741d1 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:04:17 +0200 Subject: [PATCH 02/16] feat(barcode-generator-web): add Data Matrix widget properties Adds "Data Matrix" to the top-level barcode format and a new "Advanced Data Matrix Settings" group (GS1 mode toggle, square/rectangle shape, size). Regenerates the widget prop typings to match. Co-Authored-By: Claude Opus 4.8 --- .../src/BarcodeGenerator.xml | 19 +++++++++++++++++++ .../typings/BarcodeGeneratorProps.d.ts | 10 +++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index 7aaec31075..63401d264e 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -19,6 +19,7 @@ Barcode QR Code + Data Matrix Custom @@ -135,6 +136,24 @@ The size of the QR box. Note: In preview, the max height is 200px. The QR code will render at full size in your application. + + + GS1 Data Matrix + Encode as GS1 Data Matrix (FNC1 + Application Identifiers), e.g. (01)09501101020917(17)261231(10)ABC123. Used for pharma serialization. + + + Symbol shape + Choose square or rectangular Data Matrix symbol shape + + Square + Rectangle + + + + Data Matrix size + The size of the Data Matrix symbol in pixels. Note: In preview, the max height is 200px. The symbol will render at full size in your application. + + Log Level diff --git a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts index 222cd8bf98..6139010d28 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -7,7 +7,7 @@ import { DynamicValue, WebImage } from "mendix"; import { Big } from "big.js"; import { CSSProperties } from "react"; -export type CodeFormatEnum = "CODE128" | "QRCode" | "Custom"; +export type CodeFormatEnum = "CODE128" | "QRCode" | "DataMatrix" | "Custom"; export type ButtonPositionEnum = "top" | "bottom"; @@ -17,6 +17,8 @@ export type AddonFormatEnum = "None" | "EAN5" | "EAN2"; export type QrLevelEnum = "L" | "M" | "Q" | "H"; +export type DmShapeEnum = "square" | "rectangle"; + export type LogLevelEnum = "None" | "Info" | "Debug"; export interface BarcodeGeneratorContainerProps { @@ -42,6 +44,9 @@ export interface BarcodeGeneratorContainerProps { addonSpacing: number; qrLevel: QrLevelEnum; qrSize: number; + dmGs1Mode: boolean; + dmShape: DmShapeEnum; + dmSize: number; logLevel: LogLevelEnum; displayValue: boolean; showAsCard: boolean; @@ -91,6 +96,9 @@ export interface BarcodeGeneratorPreviewProps { addonSpacing: number | null; qrLevel: QrLevelEnum; qrSize: number | null; + dmGs1Mode: boolean; + dmShape: DmShapeEnum; + dmSize: number | null; logLevel: LogLevelEnum; displayValue: boolean; showAsCard: boolean; From 54941b404dec37507c1dc5480662d15f2303340d Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:04:29 +0200 Subject: [PATCH 03/16] feat(barcode-generator-web): add Data Matrix config model and validation Adds DataMatrixTypeConfig to the BarcodeConfig union with a format === "DataMatrix" branch in barcodeConfig(), plus a DataMatrix value check and a loose GS1 Application Identifier syntax validator. Co-Authored-By: Claude Opus 4.8 --- .../src/config/Barcode.config.ts | 20 ++++++++++- .../src/config/validation.ts | 36 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts index 7d772a7df1..f9c62452c7 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts @@ -55,7 +55,15 @@ export interface QRCodeTypeConfig extends CodeBaseTypeConfig { }; } -export type BarcodeConfig = BarcodeTypeConfig | QRCodeTypeConfig; +/** Configuration for Data Matrix rendering */ +export interface DataMatrixTypeConfig extends CodeBaseTypeConfig { + type: "datamatrix"; + size: number; + gs1Mode: boolean; + shape: "square" | "rectangle"; +} + +export type BarcodeConfig = BarcodeTypeConfig | QRCodeTypeConfig | DataMatrixTypeConfig; export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeConfig { const codeValue = props.codeValue?.value ?? ""; @@ -77,6 +85,16 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon downloadButton: downloadButtonConfig }; + if (format === "DataMatrix") { + return { + type: "datamatrix", + ...baseConfig, + size: props.dmSize ?? 128, + gs1Mode: props.dmGs1Mode ?? false, + shape: props.dmShape ?? "square" + }; + } + if (format === "QRCode") { return { type: "qrcode", diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts index bcca55073d..fa28abd47c 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts @@ -114,6 +114,15 @@ export function validateBarcodeValue(format: CustomCodeFormatEnum | CodeFormatEn return { valid: false, message: "CODE93 should not contain control characters." }; } return { valid: true }; + case "DataMatrix": + // DataMatrix: encoder handles the heavy lifting; guard extremely long static values + if (value.length > 2000) { + return { + valid: false, + message: "The Data Matrix value is very long; consider a shorter value or a dynamic attribute." + }; + } + return { valid: true }; case "QRCode": // QRCode: accepts most characters, but warn for extremely long static values if (value.length > 1200) { @@ -140,6 +149,33 @@ export function validateBarcodeValue(format: CustomCodeFormatEnum | CodeFormatEn } } +/** + * Loosely validate GS1 Data Matrix Application Identifier syntax. + * Expects human-readable AI form, e.g. `(01)09501101020917(17)261231(10)ABC123`. + * bwip-js is the source of truth for encoding; this catches obvious structural errors early. + */ +export function validateGs1DataMatrixValue(value: string): ValidationResult { + if (!value) { + return { valid: true }; + } + + // Must start with an application identifier and contain only balanced (nn) groups with data. + if (!/^(\(\d{2,4}\)[^(]*)+$/.test(value)) { + return { + valid: false, + message: + "GS1 Data Matrix expects Application Identifier syntax, e.g. (01)09501101020917(17)261231(10)ABC123." + }; + } + + // Every AI group must carry at least one data character. + if (/\(\d{2,4}\)(?=\(|$)/.test(value)) { + return { valid: false, message: "Each GS1 Application Identifier must be followed by data." }; + } + + return { valid: true }; +} + /** Validate addon (EAN-5 / EAN-2) values. */ export function validateAddonValue(addonFormat: AddonFormatEnum | null | undefined, value: string): ValidationResult { if (!addonFormat || addonFormat === "None") { From b9b51c996975041a62e18bfe67ef8327c2566ccf Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:04:43 +0200 Subject: [PATCH 04/16] feat(barcode-generator-web): render Data Matrix via bwip-js Adds DataMatrixRenderer as a third render path. Selects the bwip-js encoder by GS1 mode and shape (datamatrix / gs1datamatrix / *rectangular), renders inline SVG, and reuses the existing SVG->PNG download pipeline. Wires the dispatch in BarcodeGenerator and the download filename prefix for the datamatrix type. Co-Authored-By: Claude Opus 4.8 --- .../src/BarcodeGenerator.tsx | 9 +- .../src/components/DataMatrix.tsx | 106 ++++++++++++++++++ .../src/utils/download-code.ts | 2 + 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx index 9304ef8979..ff9b195822 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx @@ -2,6 +2,7 @@ import classNames from "classnames"; import { ReactElement } from "react"; import { BarcodeGeneratorContainerProps } from "../typings/BarcodeGeneratorProps"; import { BarcodeRenderer } from "./components/Barcode"; +import { DataMatrixRenderer } from "./components/DataMatrix"; import { QRCodeRenderer } from "./components/QRCode"; import { barcodeConfig } from "./config/Barcode.config"; @@ -22,7 +23,13 @@ export default function BarcodeGenerator(props: BarcodeGeneratorContainerProps): tabIndex={props.tabIndex} style={props.style} > - {config.type === "qrcode" ? : } + {config.type === "qrcode" ? ( + + ) : config.type === "datamatrix" ? ( + + ) : ( + + )} ); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx new file mode 100644 index 0000000000..ff83bea5cd --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx @@ -0,0 +1,106 @@ +import { + datamatrix, + datamatrixrectangular, + drawingSVG, + gs1datamatrix, + gs1datamatrixrectangular +} from "@bwip-js/browser"; +import { ReactElement, useMemo, useRef } from "react"; +import { DownloadButton } from "./DownloadButton"; +import { DataMatrixTypeConfig } from "../config/Barcode.config"; +import { validateBarcodeValue, validateGs1DataMatrixValue } from "../config/validation"; +import { downloadCode } from "../utils/download-code"; +import { printError } from "../utils/helpers"; + +interface DataMatrixRendererProps { + config: DataMatrixTypeConfig; +} + +/** Selects the bwip-js encoder for the requested GS1 mode and symbol shape. */ +function encodeDataMatrix(config: DataMatrixTypeConfig): string { + const opts = { + text: config.codeValue, + // bwip-js scale is in module units; map the pixel size onto a reasonable scale. + scale: Math.max(1, Math.round(config.size / 32)), + paddingwidth: config.margin, + paddingheight: config.margin, + // GS1 AI syntax uses parentheses; parse must be on for the human-readable form. + parse: config.gs1Mode + } as const; + + if (config.gs1Mode) { + return config.shape === "rectangle" + ? gs1datamatrixrectangular({ ...opts, bcid: "gs1datamatrixrectangular" }, drawingSVG()) + : gs1datamatrix({ ...opts, bcid: "gs1datamatrix" }, drawingSVG()); + } + + return config.shape === "rectangle" + ? datamatrixrectangular({ ...opts, bcid: "datamatrixrectangular" }, drawingSVG()) + : datamatrix({ ...opts, bcid: "datamatrix" }, drawingSVG()); +} + +export function DataMatrixRenderer({ config }: DataMatrixRendererProps): ReactElement { + const containerRef = useRef(null); + const { codeValue, downloadButton, size, gs1Mode } = config; + const buttonPosition = downloadButton?.buttonPosition ?? "bottom"; + + const { svg, error } = useMemo<{ svg: string | null; error: boolean }>(() => { + if (!codeValue) { + return { svg: null, error: false }; + } + + const baseValidation = validateBarcodeValue("DataMatrix", codeValue); + if (!baseValidation.valid) { + printError(`Validation failed for Data Matrix: ${baseValidation.message}`, config.logLevel); + return { svg: null, error: true }; + } + + if (gs1Mode) { + const gs1Validation = validateGs1DataMatrixValue(codeValue); + if (!gs1Validation.valid) { + printError(`GS1 Data Matrix validation failed: ${gs1Validation.message}`, config.logLevel); + return { svg: null, error: true }; + } + } + + try { + return { svg: encodeDataMatrix(config), error: false }; + } catch (e) { + const message = e instanceof Error ? e.message : "Error generating Data Matrix"; + printError(`Rendering failed: ${message} \nValue: "${codeValue}"`, config.logLevel); + return { svg: null, error: true }; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [codeValue, size, gs1Mode, config.shape, config.margin, config.logLevel]); + + if (error || !svg) { + return ( +
+ {error && config.logLevel !== "None" && ( +
+ Unable to generate Data Matrix. Please check the value and format + configuration. +
+ )} +
+ ); + } + + const getSvgElement = (): SVGSVGElement | null => containerRef.current?.querySelector("svg") ?? null; + + const button = downloadButton && ( + downloadCode({ current: getSvgElement() }, config, downloadButton.fileName)} + ariaLabel={downloadButton.label} + caption={downloadButton.caption} + /> + ); + + return ( +
+ {buttonPosition === "top" && button} +
+ {buttonPosition === "bottom" && button} +
+ ); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/download-code.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/download-code.ts index f5685b031d..895100cd7f 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/utils/download-code.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/download-code.ts @@ -25,6 +25,8 @@ export async function downloadCode( // Process overlay images for QR codes if (config.type === "qrcode") { await processQRImages(clonedSvg); + } else if (config.type === "datamatrix") { + fileNamePrefix = config.gs1Mode ? "datamatrix_gs1" : "datamatrix"; } else { fileNamePrefix = `${config.type}_${config.format}`; } From 79c3069387716deb59dc60de22e366d505cdc09b Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:04:55 +0200 Subject: [PATCH 05/16] feat(barcode-generator-web): add Data Matrix editor preview Renders a representative Data Matrix glyph in the Studio Pro page editor when the Data Matrix format is selected. Co-Authored-By: Claude Opus 4.8 --- .../src/BarcodeGenerator.editorPreview.tsx | 4 ++ .../components/preview/DataMatrixPreview.tsx | 45 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/components/preview/DataMatrixPreview.tsx diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx index 44f78544d7..4a0f8cc9b0 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx @@ -4,6 +4,7 @@ import { parseStyle } from "@mendix/widget-plugin-platform/preview/parse-style"; import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps"; import { DownloadIcon } from "./components/icons/DownloadIcon"; import { BarcodePreview } from "./components/preview/BarcodePreview"; +import { DataMatrixPreview } from "./components/preview/DataMatrixPreview"; import { QRCodePreview } from "./components/preview/QRCodePreview"; const defaultDownloadCaption = "Download"; @@ -23,6 +24,7 @@ function PreviewDownloadButton(props: BarcodeGeneratorPreviewProps): ReactElemen export function preview(props: BarcodeGeneratorPreviewProps): ReactElement { const styles = parseStyle(props.style); const isQrCode = props.codeFormat === "QRCode"; + const isDataMatrix = props.codeFormat === "DataMatrix"; const downloadButton = ; return ( @@ -34,6 +36,8 @@ export function preview(props: BarcodeGeneratorPreviewProps): ReactElement { > {isQrCode ? ( + ) : isDataMatrix ? ( + ) : ( )} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/DataMatrixPreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/DataMatrixPreview.tsx new file mode 100644 index 0000000000..89fdcff583 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/DataMatrixPreview.tsx @@ -0,0 +1,45 @@ +import { datamatrix, drawingSVG, gs1datamatrix } from "@bwip-js/browser"; +import { ReactElement, useMemo } from "react"; +import { BarcodeGeneratorPreviewProps } from "../../../typings/BarcodeGeneratorProps"; + +interface DataMatrixPreviewProps extends BarcodeGeneratorPreviewProps { + downloadButton: ReactElement | null; +} + +const SAMPLE_PLAIN = "DATA MATRIX"; +const SAMPLE_GS1 = "(01)09501101020917(17)261231(10)ABC123"; + +export function DataMatrixPreview(props: DataMatrixPreviewProps): ReactElement { + const { downloadButton, ...restProps } = props; + const size = restProps.dmSize ?? 128; + const displaySize = Math.min(size, 200); // Clamped to 200px for preview + const gs1Mode = restProps.dmGs1Mode === true; + + const svg = useMemo(() => { + try { + return gs1Mode + ? gs1datamatrix({ bcid: "gs1datamatrix", text: SAMPLE_GS1, scale: 3, parse: true }, drawingSVG()) + : datamatrix({ bcid: "datamatrix", text: SAMPLE_PLAIN, scale: 3 }, drawingSVG()); + } catch { + return null; + } + }, [gs1Mode]); + + return ( +
+ {restProps.buttonPosition === "top" && downloadButton} + {svg ? ( +
+ ) : ( +
+ Data Matrix preview unavailable +
+ )} + {restProps.buttonPosition === "bottom" && downloadButton} +
+ ); +} From 226ed1668b43dc894c9f8ebd2f41e935fc4a85c9 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:05:07 +0200 Subject: [PATCH 06/16] test(barcode-generator-web): cover Data Matrix generation Adds config-mapping and validation unit tests plus render tests asserting encoder selection (plain vs GS1, rectangular) and the malformed-GS1 error path. Mocks bwip-js and adds the new required Data Matrix props to the shared test props builder. Co-Authored-By: Claude Opus 4.8 --- .../src/__tests__/BarcodeGenerator.spec.tsx | 80 +++++++++++++++++++ .../config/__tests__/Barcode.config.spec.ts | 74 +++++++++++++++++ .../src/config/__tests__/validation.spec.ts | 38 +++++++++ 3 files changed, 192 insertions(+) create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/Barcode.config.spec.ts create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/validation.spec.ts diff --git a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx index 97fda1610d..bae5454307 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx @@ -1,4 +1,5 @@ import "@testing-library/jest-dom"; +import * as bwip from "@bwip-js/browser"; import { fireEvent, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Big } from "big.js"; @@ -33,6 +34,19 @@ jest.mock("../utils/download-code", () => ({ downloadCode: jest.fn() })); +// Mock bwip-js (ESM-only, not resolvable in jest without transform) +jest.mock( + "@bwip-js/browser", + () => ({ + drawingSVG: jest.fn(() => ({})), + datamatrix: jest.fn(() => ""), + datamatrixrectangular: jest.fn(() => ""), + gs1datamatrix: jest.fn(() => ""), + gs1datamatrixrectangular: jest.fn(() => "") + }), + { virtual: true } +); + import { BarcodeGeneratorContainerProps, CodeFormatEnum, @@ -69,6 +83,9 @@ const createBarcodeProps = ( codeMargin: 4, qrSize: 128, qrMargin: 2, + dmGs1Mode: false, + dmShape: "square", + dmSize: 128, qrTitle: dynamic.available(""), qrLevel: "L", qrOverlay: false, @@ -1036,4 +1053,67 @@ describe("BarcodeGenerator", () => { expect(screen.getByText("Export")).toBeInTheDocument(); }); }); + + describe("Data Matrix", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("renders a plain Data Matrix using the datamatrix encoder", () => { + const props = createBarcodeProps({ + codeFormat: "DataMatrix" as CodeFormatEnum, + codeValue: dynamic.available("ABC-12345") + }); + + render(); + + expect(bwip.datamatrix).toHaveBeenCalledWith( + expect.objectContaining({ bcid: "datamatrix", text: "ABC-12345" }), + expect.anything() + ); + expect(bwip.gs1datamatrix).not.toHaveBeenCalled(); + }); + + it("uses the gs1datamatrix encoder in GS1 mode", () => { + const props = createBarcodeProps({ + codeFormat: "DataMatrix" as CodeFormatEnum, + dmGs1Mode: true, + codeValue: dynamic.available("(01)09501101020917(17)261231(10)ABC123") + }); + + render(); + + expect(bwip.gs1datamatrix).toHaveBeenCalledWith( + expect.objectContaining({ bcid: "gs1datamatrix", parse: true }), + expect.anything() + ); + expect(bwip.datamatrix).not.toHaveBeenCalled(); + }); + + it("uses the rectangular encoder for the rectangle shape", () => { + const props = createBarcodeProps({ + codeFormat: "DataMatrix" as CodeFormatEnum, + dmShape: "rectangle", + codeValue: dynamic.available("RECT") + }); + + render(); + + expect(bwip.datamatrixrectangular).toHaveBeenCalled(); + }); + + it("shows the error alert when GS1 syntax is malformed and log level is not None", () => { + const props = createBarcodeProps({ + codeFormat: "DataMatrix" as CodeFormatEnum, + dmGs1Mode: true, + logLevel: "Debug", + codeValue: dynamic.available("not-a-valid-ai") + }); + + render(); + + expect(screen.getByText(/Unable to generate Data Matrix/)).toBeInTheDocument(); + expect(bwip.gs1datamatrix).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/Barcode.config.spec.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/Barcode.config.spec.ts new file mode 100644 index 0000000000..dab11ac0a0 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/Barcode.config.spec.ts @@ -0,0 +1,74 @@ +import { Big } from "big.js"; +import { dynamic } from "@mendix/widget-plugin-test-utils"; +import { BarcodeGeneratorContainerProps } from "../../../typings/BarcodeGeneratorProps"; +import { barcodeConfig, DataMatrixTypeConfig } from "../Barcode.config"; + +function props(overrides: Partial = {}): BarcodeGeneratorContainerProps { + return { + name: "bg1", + class: "", + tabIndex: -1, + codeValue: dynamic.available("ABC123"), + codeFormat: "DataMatrix", + customCodeFormat: "CODE128", + enableEan128: false, + enableFlat: false, + lastChar: "", + enableMod43: false, + allowDownload: false, + buttonPosition: "bottom", + addonFormat: "None", + addonValue: dynamic.available(""), + addonSpacing: 20, + displayValue: false, + showAsCard: false, + codeWidth: 2, + codeHeight: 200, + codeMargin: 4, + qrSize: 128, + qrMargin: 2, + dmGs1Mode: false, + dmShape: "square", + dmSize: 128, + qrTitle: dynamic.available("QR"), + qrLevel: "L", + qrOverlay: false, + qrOverlaySrc: dynamic.available({ uri: "" } as any), + qrOverlayCenter: true, + qrOverlayX: 0, + qrOverlayY: 0, + qrOverlayHeight: 24, + qrOverlayWidth: 24, + qrOverlayOpacity: new Big(1), + qrOverlayExcavate: true, + logLevel: "None", + ...overrides + } as BarcodeGeneratorContainerProps; +} + +describe("barcodeConfig - DataMatrix", () => { + it("maps DataMatrix format to a datamatrix config", () => { + const config = barcodeConfig(props()) as DataMatrixTypeConfig; + expect(config.type).toBe("datamatrix"); + expect(config.codeValue).toBe("ABC123"); + expect(config.size).toBe(128); + expect(config.shape).toBe("square"); + expect(config.gs1Mode).toBe(false); + }); + + it("carries GS1 mode and rectangular shape", () => { + const config = barcodeConfig(props({ dmGs1Mode: true, dmShape: "rectangle" })) as DataMatrixTypeConfig; + expect(config.gs1Mode).toBe(true); + expect(config.shape).toBe("rectangle"); + }); + + it("uses the DataMatrix margin from codeMargin", () => { + const config = barcodeConfig(props({ codeMargin: 6 })) as DataMatrixTypeConfig; + expect(config.margin).toBe(6); + }); + + it("does not route non-DataMatrix formats to datamatrix", () => { + expect(barcodeConfig(props({ codeFormat: "QRCode" })).type).toBe("qrcode"); + expect(barcodeConfig(props({ codeFormat: "CODE128" })).type).toBe("barcode"); + }); +}); diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/validation.spec.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/validation.spec.ts new file mode 100644 index 0000000000..96269a614a --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/validation.spec.ts @@ -0,0 +1,38 @@ +import { validateBarcodeValue, validateGs1DataMatrixValue } from "../validation"; + +describe("validateBarcodeValue - DataMatrix", () => { + it("passes for an empty value (dynamic binding at runtime)", () => { + expect(validateBarcodeValue("DataMatrix", "")).toEqual({ valid: true }); + }); + + it("passes for a plain string", () => { + expect(validateBarcodeValue("DataMatrix", "ABC-12345")).toEqual({ valid: true }); + }); + + it("rejects an excessively long value", () => { + const result = validateBarcodeValue("DataMatrix", "x".repeat(2001)); + expect(result.valid).toBe(false); + }); +}); + +describe("validateGs1DataMatrixValue", () => { + it("passes for an empty value", () => { + expect(validateGs1DataMatrixValue("")).toEqual({ valid: true }); + }); + + it("passes for valid GS1 AI syntax", () => { + expect(validateGs1DataMatrixValue("(01)09501101020917(17)261231(10)ABC123")).toEqual({ valid: true }); + }); + + it("rejects a value that does not start with an Application Identifier", () => { + expect(validateGs1DataMatrixValue("09501101020917").valid).toBe(false); + }); + + it("rejects an Application Identifier with no data", () => { + expect(validateGs1DataMatrixValue("(01)(17)261231").valid).toBe(false); + }); + + it("rejects malformed / unbalanced parentheses", () => { + expect(validateGs1DataMatrixValue("(01)09501101020917(17").valid).toBe(false); + }); +}); From ffe3724070cc83d8930152c48c8e14fca989b502 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:05:23 +0200 Subject: [PATCH 07/16] docs(barcode-generator-web): changelog for Data Matrix support Co-Authored-By: Claude Opus 4.8 --- packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md b/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md index 2fc2506a98..70ce88ccea 100644 --- a/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md +++ b/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Added Data Matrix and GS1 Data Matrix generation support, including square and rectangular symbol shapes. + ## [1.0.0] - 2026-04-17 ### Added From 32f7940b55262a3f717e0903b3389c6ccc48f060 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:05:34 +0200 Subject: [PATCH 08/16] docs(openspec): add-datamatrix-generation change Proposal, design, specs, and task checklist for adding Data Matrix and GS1 Data Matrix generation to the barcode generator widget. Co-Authored-By: Claude Opus 4.8 --- .../add-datamatrix-generation/.openspec.yaml | 2 + .../add-datamatrix-generation/design.md | 51 ++++++++++ .../add-datamatrix-generation/proposal.md | 34 +++++++ .../specs/barcode-generation/spec.md | 94 +++++++++++++++++++ .../add-datamatrix-generation/tasks.md | 41 ++++++++ 5 files changed, 222 insertions(+) create mode 100644 openspec/changes/add-datamatrix-generation/.openspec.yaml create mode 100644 openspec/changes/add-datamatrix-generation/design.md create mode 100644 openspec/changes/add-datamatrix-generation/proposal.md create mode 100644 openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md create mode 100644 openspec/changes/add-datamatrix-generation/tasks.md diff --git a/openspec/changes/add-datamatrix-generation/.openspec.yaml b/openspec/changes/add-datamatrix-generation/.openspec.yaml new file mode 100644 index 0000000000..074342d550 --- /dev/null +++ b/openspec/changes/add-datamatrix-generation/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-09 diff --git a/openspec/changes/add-datamatrix-generation/design.md b/openspec/changes/add-datamatrix-generation/design.md new file mode 100644 index 0000000000..edc56608a6 --- /dev/null +++ b/openspec/changes/add-datamatrix-generation/design.md @@ -0,0 +1,51 @@ +## Context + +`@mendix/barcode-generator-web` renders three-way today: 1D barcodes via `jsbarcode` (isolated in `src/utils/barcodeRenderer-utils.ts`) and QR via `qrcode.react` (isolated in `src/components/QRCode.tsx`). `src/config/Barcode.config.ts` maps Mendix props into a lib-agnostic discriminated union `BarcodeConfig = BarcodeTypeConfig | QRCodeTypeConfig`, and `src/BarcodeGenerator.tsx:25` dispatches on `config.type`. Rendering is SVG-first; download converts SVG→PNG via `src/utils/download-code.ts` + `download-utils.ts`. + +Neither current lib can produce DataMatrix. Pharma requires **GS1 DataMatrix** (FNC1 + GS1 Application Identifiers), not just plain DataMatrix. + +## Goals / Non-Goals + +**Goals:** + +- Add DataMatrix and GS1 DataMatrix generation as a third render path, reusing the existing config-union + dispatch + SVG→PNG download architecture. +- Keep the new library isolated behind a single seam module, mirroring the QR split. +- No regression to existing barcode/QR behavior. + +**Non-Goals:** + +- Replacing jsbarcode or qrcode.react. +- Building a GS1 AI parser/validator beyond loose syntax checks (bwip-js validates the encoding). +- Adding non-DataMatrix 2D symbologies (Aztec, PDF417) — out of scope. + +## Decisions + +**Library: `@bwip-js/browser`.** Only maintained (v4.11.x, ~676k dl/wk, MIT) library with native GS1 DataMatrix (`bcid: "gs1datamatrix"`, accepts human-readable AI syntax) and rectangular support. Import the named `datamatrix` + `drawingSVG` exports so bundlers tree-shake — do NOT use `toSVG()`, which links all ~100 BWIPP encoders. + +- _Alternatives:_ `datamatrix-svg` (no GS1, unmaintained since 2020) rejected; `@zxing/library` (used by scanner) is decode-only, cannot encode. + +**Third render path, not a `customCodeFormat` sub-option.** DataMatrix is 2D with its own options (GS1 toggle, shape), like QR. Add `DataMatrix` to the top-level `codeFormat` enum and a `DataMatrixTypeConfig` (`type: "datamatrix"`) to the union. `barcodeConfig()` branches on `format === "DataMatrix"` before the QR check. + +- _Alternative:_ nesting under `customCodeFormat` (the 1D list) rejected — that list is jsbarcode-specific and 1D-shaped. + +**GS1 as a boolean toggle under Data Matrix, not a separate top-level format.** Matches the pharma mental model ("same symbology, GS1-encoded") and keeps the top-level enum small. `dmGs1Mode` selects `gs1datamatrix` vs `datamatrix`. + +**New seam `src/components/DataMatrix.tsx`.** Mirrors `QRCode.tsx`: builds bwip-js options, renders SVG, exposes an `SVGSVGElement` ref so the existing `DownloadButton` + `downloadCode(ref, config, ...)` pipeline works unchanged. Validation + try/catch error state live in this component (the `useRenderBarcode` hook is 1D/jsbarcode-specific). + +**Validation.** Extend `validateBarcodeValue` in `src/config/validation.ts` with a `DataMatrix` case (the `format` param already unions `CodeFormatEnum`): plain mode = charset/length sanity; GS1 mode = loose balanced-`(nn)` AI-syntax check. Encoder errors caught at render. + +## Risks / Trade-offs + +- **Bundle size** → bwip-js is large if fully linked. Mitigation: import only `datamatrix`/`drawingSVG` named exports; verify bundle delta at build time. +- **Download ref shape** → bwip-js `drawingSVG()` returns SVG markup, not a React-managed ``. Mitigation: inject markup into a container and target the real `SVGSVGElement` for the ref so SVG→PNG works. +- **GS1 AI validation drift** → hand-rolled loose check may diverge from bwip-js. Mitigation: keep it minimal (structure only), let bwip-js be the source of truth and catch its errors. +- **Two enums to keep in sync** → `codeFormat` value must round-trip through config + validation + preview. Mitigation: covered by unit tests. + +## Migration Plan + +Additive, no data migration. New dependency added to `package.json`; `typings/BarcodeGeneratorProps.d.ts` regenerated by build. Rollback = revert the change and drop the dependency; existing barcode/QR configs unaffected. + +## Open Questions + +- Exact bwip-js option for rectangular DataMatrix (shape flag vs explicit `rows`/`columns`) — confirm at implementation. +- Whether to reuse `codeMargin`/`qrSize`-style sizing or add dedicated `dmSize`/`dmMargin` props — lean toward dedicated to avoid overloading 1D "bar width" semantics. diff --git a/openspec/changes/add-datamatrix-generation/proposal.md b/openspec/changes/add-datamatrix-generation/proposal.md new file mode 100644 index 0000000000..29088bdf48 --- /dev/null +++ b/openspec/changes/add-datamatrix-generation/proposal.md @@ -0,0 +1,34 @@ +## Why + +The Barcode Generator widget (`@mendix/barcode-generator-web`) can only produce 1D linear barcodes (jsbarcode) and QR codes (qrcode.react); it cannot generate DataMatrix. DataMatrix — specifically **GS1 DataMatrix** (FNC1 + GS1 Application Identifiers) — is mandatory for pharma serialization (EU FMD, US DSCSA). The Barcode Scanner widget already decodes DataMatrix, so generation closes the round-trip gap for pharma and logistics apps. + +## What Changes + +- Add **Data Matrix** as a top-level `codeFormat` option in the Barcode Generator. +- Support two encodings: plain DataMatrix and **GS1 DataMatrix** (pharma), selectable via a boolean toggle. +- Support square and rectangular symbol shapes. +- Add `@bwip-js/browser` as a dependency (tree-shakeable DataMatrix encoder) — the only maintained library with native GS1 DataMatrix support. jsbarcode and qrcode.react remain for their existing formats. +- Render DataMatrix as inline SVG (consistent with existing render + SVG→PNG download pipeline). +- Add runtime/design-time validation for DataMatrix values (plain charset/length; GS1 AI syntax). +- Add Studio Pro editor preview for the DataMatrix format. + +No breaking changes — existing barcode/QR behavior is untouched; DataMatrix is additive. + +## Capabilities + +### New Capabilities + +- `barcode-generation`: Generation of barcodes, QR codes, and (new) DataMatrix / GS1 DataMatrix from a string input in the Barcode Generator widget, including format selection, encoding options, validation, rendering, and download. + +### Modified Capabilities + + + +## Impact + +- **Package**: `packages/pluggableWidgets/barcode-generator-web` +- **New dependency**: `@bwip-js/browser` (MIT, tree-shakeable) +- **Widget config**: `src/BarcodeGenerator.xml` (new enum value + Data Matrix property group); regenerated `typings/BarcodeGeneratorProps.d.ts` +- **Source**: new `src/components/DataMatrix.tsx` (bwip-js seam), extended `src/config/Barcode.config.ts` (discriminated union), `src/BarcodeGenerator.tsx` (dispatch), `src/config/validation.ts`, editor-preview files +- **Reuses** existing `DownloadButton` + `src/utils/download-code.ts` SVG→PNG pipeline unchanged +- **Tests**: Jest unit tests + Playwright E2E; `CHANGELOG.md` entry diff --git a/openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md b/openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md new file mode 100644 index 0000000000..e97c65b5b5 --- /dev/null +++ b/openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md @@ -0,0 +1,94 @@ +## ADDED Requirements + +### Requirement: DataMatrix format selection + +The Barcode Generator widget SHALL offer "Data Matrix" as a top-level barcode format alongside the existing Barcode (1D) and QR Code options. + +#### Scenario: Data Matrix appears in format list + +- **WHEN** a developer configures the Barcode Generator in Studio Pro and opens the Barcode Format property +- **THEN** "Data Matrix" is available as a selectable format value + +#### Scenario: Selecting Data Matrix routes to the DataMatrix renderer + +- **WHEN** the Barcode Format is set to "Data Matrix" and a non-empty value is provided +- **THEN** the widget renders a DataMatrix symbol as inline SVG, and does not invoke the 1D barcode or QR code renderers + +### Requirement: Plain DataMatrix generation + +The widget SHALL encode the provided string value as a standard (non-GS1) DataMatrix symbol when GS1 mode is off. + +#### Scenario: Encode a plain string + +- **WHEN** the format is "Data Matrix", GS1 mode is off, and the value is `ABC-12345` +- **THEN** the widget renders a scannable DataMatrix symbol encoding exactly that string + +#### Scenario: Round-trip with the scanner + +- **WHEN** a plain DataMatrix generated by the widget is scanned by the Barcode Scanner widget +- **THEN** the decoded value equals the original input string + +### Requirement: GS1 DataMatrix generation + +The widget SHALL support GS1 DataMatrix encoding (FNC1 leading character, GS1 Application Identifier data) when GS1 mode is enabled, so pharma serialization data (GTIN, expiry, batch, serial) can be encoded. + +#### Scenario: Encode GS1 Application Identifier data + +- **WHEN** GS1 mode is enabled and the value is `(01)09501101020917(17)261231(10)ABC123` +- **THEN** the widget renders a GS1 DataMatrix symbol with the FNC1 indicator and the AI-structured data intact + +#### Scenario: GS1 mode toggles the encoder + +- **WHEN** the same value is rendered first with GS1 mode off and then on +- **THEN** the off result is a plain DataMatrix and the on result is a GS1 DataMatrix (distinct symbols) + +### Requirement: DataMatrix symbol shape + +The widget SHALL allow choosing between square and rectangular DataMatrix symbol shapes. + +#### Scenario: Rectangular shape + +- **WHEN** the DataMatrix shape option is set to "rectangle" +- **THEN** the rendered symbol uses a rectangular DataMatrix layout + +#### Scenario: Square shape is the default + +- **WHEN** no shape is explicitly chosen +- **THEN** the widget renders a square DataMatrix symbol + +### Requirement: DataMatrix value validation + +The widget SHALL validate the DataMatrix value and surface errors consistently with existing formats, honoring the configured log level. + +#### Scenario: Empty value at design time + +- **WHEN** no value is present (design time, before dynamic binding resolves) +- **THEN** validation passes and no error is shown, matching existing barcode behavior + +#### Scenario: Malformed GS1 AI syntax + +- **WHEN** GS1 mode is on and the value has unbalanced or malformed Application Identifier syntax +- **THEN** the widget reports a validation error and, when the log level permits, displays the generic "unable to generate" message and logs detail to the console + +#### Scenario: Encoding failure + +- **WHEN** the DataMatrix encoder throws for an invalid input +- **THEN** the error is caught, the error UI is shown per log level, and the widget does not crash + +### Requirement: DataMatrix download + +The widget SHALL allow downloading a generated DataMatrix as a PNG using the existing download control, when downloads are enabled. + +#### Scenario: Download a DataMatrix as PNG + +- **WHEN** downloads are enabled and the user activates the download button on a DataMatrix +- **THEN** a PNG file of the rendered DataMatrix is downloaded, using the configured or an auto-generated filename + +### Requirement: DataMatrix editor preview + +The widget SHALL show a representative DataMatrix preview in the Studio Pro editor when the Data Matrix format is selected. + +#### Scenario: Preview in Studio Pro + +- **WHEN** the Data Matrix format is selected in the Studio Pro page editor +- **THEN** the widget preview displays a DataMatrix-style glyph rather than a 1D barcode or QR preview diff --git a/openspec/changes/add-datamatrix-generation/tasks.md b/openspec/changes/add-datamatrix-generation/tasks.md new file mode 100644 index 0000000000..48bde20ee7 --- /dev/null +++ b/openspec/changes/add-datamatrix-generation/tasks.md @@ -0,0 +1,41 @@ +## 1. Dependency & Widget Config + +- [x] 1.1 Add `@bwip-js/browser` to `packages/pluggableWidgets/barcode-generator-web/package.json` dependencies +- [x] 1.2 Add `DataMatrix` enum value to top-level `codeFormat` in `src/BarcodeGenerator.xml` +- [x] 1.3 Add "Advanced Data Matrix Settings" property group in XML: `dmGs1Mode` (boolean, default false), `dmShape` (enum square/rectangle, default square), and sizing property (`dmSize` / reuse margin) +- [x] 1.4 Build to regenerate `typings/BarcodeGeneratorProps.d.ts` and confirm `CodeFormatEnum` includes `DataMatrix` + +## 2. Config Model + +- [x] 2.1 Add `DataMatrixTypeConfig` (`type: "datamatrix"`, `gs1Mode`, `shape`, `size`, `margin`) to the `BarcodeConfig` union in `src/config/Barcode.config.ts` +- [x] 2.2 In `barcodeConfig()`, branch `format === "DataMatrix"` before the QR check, returning the new config +- [x] 2.3 Extend `src/utils/download-code.ts` filename-prefix logic to handle `config.type === "datamatrix"` + +## 3. Rendering Seam + +- [x] 3.1 Create `src/components/DataMatrix.tsx` with `DataMatrixRenderer({ config })`, importing `{ datamatrix, drawingSVG }` from `@bwip-js/browser` (tree-shakeable; do NOT use `toSVG()`) +- [x] 3.2 Build bwip-js options: `bcid` = `gs1datamatrix` when `gs1Mode` else `datamatrix`; apply rectangular option when `shape === "rectangle"`; set `text`, size, margin +- [x] 3.3 Render SVG and expose a real `SVGSVGElement` ref so `DownloadButton` + `downloadCode(ref, config, ...)` work unchanged +- [x] 3.4 Wrap encoding in try/catch → error state; render the existing error-alert markup honoring `logLevel` (mirror `BarcodeRenderer`) +- [x] 3.5 Extend dispatch in `src/BarcodeGenerator.tsx:25` to route `config.type === "datamatrix"` to `DataMatrixRenderer` + +## 4. Validation + +- [x] 4.1 Add `DataMatrix` case to `validateBarcodeValue` in `src/config/validation.ts` (plain: charset/length sanity; GS1: loose balanced-`(nn)` AI syntax) +- [x] 4.2 Wire GS1-mode-aware validation into the DataMatrix renderer before encoding + +## 5. Editor Preview + +- [x] 5.1 Add a DataMatrix preview asset/branch alongside `src/hooks/useBarcodePreviewSvg.ts`, `src/assets/barcodes/*.svg`, `src/components/preview/*` so Studio Pro shows a DataMatrix glyph for the format + +## 6. Tests & Changelog + +- [x] 6.1 Unit tests: config mapping for DataMatrix, GS1 vs plain `bcid` selection, shape option, validation (valid GS1 AI, malformed AI, plain string, empty value) +- [ ] 6.2 Playwright E2E: render + download a DataMatrix per `docs/requirements/e2e-test-guidelines.md` _(blocked: existing e2e spec is a placeholder; needs the external Mendix testProjects page configured with a DataMatrix widget instance)_ +- [x] 6.3 Add user-facing `CHANGELOG.md` entry ("Added Data Matrix and GS1 Data Matrix generation support") + +## 7. Verification + +- [x] 7.1 `cd packages/pluggableWidgets/barcode-generator-web && pnpm run test` passes +- [ ] 7.2 `pnpm run build` succeeds; confirm bundle delta small (bwip-js tree-shaken to DataMatrix encoder only) _(blocked: local build env has a broken rollup binary — stale `.bin/rollup` shim points to uninstalled rollup@3.29.5; unrelated to this change. Code verified via `tsc --noEmit` + jest instead.)_ +- [ ] 7.3 Live Studio Pro test: plain DataMatrix scans via Barcode Scanner (round-trip); GS1 value `(01)09501101020917(17)261231(10)ABC123` renders + decodes; PNG download works; rectangular shape renders _(requires human: live Studio Pro session with `MX_PROJECT_PATH` set)_ From f5bb09033c814c0231416584acd79e8f9c041b16 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 13 Aug 2026 10:20:16 +0200 Subject: [PATCH 09/16] docs(openspec): relocate the Data Matrix change into its owning package Move the add-datamatrix-generation change folder under packages/pluggableWidgets/barcode-generator-web/openspec/ instead of the repo-root openspec/ folder, per AGENTS.md. Co-Authored-By: Claude Opus 5 --- .../openspec}/changes/add-datamatrix-generation/.openspec.yaml | 0 .../openspec}/changes/add-datamatrix-generation/design.md | 0 .../openspec}/changes/add-datamatrix-generation/proposal.md | 0 .../add-datamatrix-generation/specs/barcode-generation/spec.md | 0 .../openspec}/changes/add-datamatrix-generation/tasks.md | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename {openspec => packages/pluggableWidgets/barcode-generator-web/openspec}/changes/add-datamatrix-generation/.openspec.yaml (100%) rename {openspec => packages/pluggableWidgets/barcode-generator-web/openspec}/changes/add-datamatrix-generation/design.md (100%) rename {openspec => packages/pluggableWidgets/barcode-generator-web/openspec}/changes/add-datamatrix-generation/proposal.md (100%) rename {openspec => packages/pluggableWidgets/barcode-generator-web/openspec}/changes/add-datamatrix-generation/specs/barcode-generation/spec.md (100%) rename {openspec => packages/pluggableWidgets/barcode-generator-web/openspec}/changes/add-datamatrix-generation/tasks.md (100%) diff --git a/openspec/changes/add-datamatrix-generation/.openspec.yaml b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/.openspec.yaml similarity index 100% rename from openspec/changes/add-datamatrix-generation/.openspec.yaml rename to packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/.openspec.yaml diff --git a/openspec/changes/add-datamatrix-generation/design.md b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/design.md similarity index 100% rename from openspec/changes/add-datamatrix-generation/design.md rename to packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/design.md diff --git a/openspec/changes/add-datamatrix-generation/proposal.md b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/proposal.md similarity index 100% rename from openspec/changes/add-datamatrix-generation/proposal.md rename to packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/proposal.md diff --git a/openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md similarity index 100% rename from openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md rename to packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md diff --git a/openspec/changes/add-datamatrix-generation/tasks.md b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/tasks.md similarity index 100% rename from openspec/changes/add-datamatrix-generation/tasks.md rename to packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/tasks.md From 663ef8d9ee534a1896ac18214c18415f265db9ac Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 16 Jul 2026 09:42:53 +0200 Subject: [PATCH 10/16] fix(barcode-generator-web): size Data Matrix SVG by viewBox aspect ratio bwip-js SVGs carry only a viewBox, no width/height attributes, so the rendered element defaulted to intrinsic sizing instead of the configured size. Derive pixel dimensions from the viewBox and apply them via inline style. --- .../src/components/DataMatrix.tsx | 21 ++++++++++++++++++- .../src/ui/BarcodeGenerator.scss | 20 ++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx index ff83bea5cd..51982e819e 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx @@ -39,6 +39,18 @@ function encodeDataMatrix(config: DataMatrixTypeConfig): string { : datamatrix({ ...opts, bcid: "datamatrix" }, drawingSVG()); } +/** bwip-js SVGs only carry a viewBox, no width/height attributes; derive pixel dimensions from it. */ +function getSvgPixelSize(svg: string, size: number): { width: number; height: number } { + const match = svg.match(/viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"/); + if (!match) { + return { width: size, height: size }; + } + const [viewBoxWidth, viewBoxHeight] = [parseFloat(match[1]), parseFloat(match[2])]; + return viewBoxWidth >= viewBoxHeight + ? { width: size, height: (size * viewBoxHeight) / viewBoxWidth } + : { width: (size * viewBoxWidth) / viewBoxHeight, height: size }; +} + export function DataMatrixRenderer({ config }: DataMatrixRendererProps): ReactElement { const containerRef = useRef(null); const { codeValue, downloadButton, size, gs1Mode } = config; @@ -96,10 +108,17 @@ export function DataMatrixRenderer({ config }: DataMatrixRendererProps): ReactEl /> ); + const { width, height } = getSvgPixelSize(svg, size); + return (
{buttonPosition === "top" && button} -
+
{buttonPosition === "bottom" && button}
); diff --git a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss index 431a27f89e..4dff78d709 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss +++ b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss @@ -28,6 +28,16 @@ $widget-prefix: "barcode-generator"; height: auto; } + .datamatrix-svg { + max-width: 100%; + + svg { + display: block; + width: 100%; + height: 100%; + } + } + .qrcode-renderer-title { font-weight: var(--font-weight-normal); font-size: var(--font-size-small); @@ -98,3 +108,13 @@ $widget-prefix: "barcode-generator"; position: absolute; object-fit: contain; } + +.datamatrix-preview-image { + max-width: 100%; + + svg { + display: block; + width: 100%; + height: 100%; + } +} From 72fe4b747d935001a4c5b18c2f989a824c0565dc Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Mon, 20 Jul 2026 09:58:08 +0200 Subject: [PATCH 11/16] fix(barcode-generator-web): sanitize Data Matrix SVG, prefix preview class, tighten memo deps Data Matrix code value comes from a Mendix DynamicValue and reaches the DOM via dangerouslySetInnerHTML; sanitize the bwip-js SVG output with DOMPurify before injecting it. Also prefix the preview image class per widget style convention and destructure encodeDataMatrix's params so the useMemo dependency list stays exhaustive without a lint suppression. --- .../barcode-generator-web/package.json | 1 + .../src/components/DataMatrix.tsx | 38 ++++++++++--------- .../components/preview/DataMatrixPreview.tsx | 7 +++- .../src/ui/BarcodeGenerator.scss | 2 +- pnpm-lock.yaml | 4 ++ 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/package.json b/packages/pluggableWidgets/barcode-generator-web/package.json index c13e6c0c55..21c3b45e61 100644 --- a/packages/pluggableWidgets/barcode-generator-web/package.json +++ b/packages/pluggableWidgets/barcode-generator-web/package.json @@ -45,6 +45,7 @@ "dependencies": { "@bwip-js/browser": "^4.11.2", "classnames": "^2.5.1", + "dompurify": "^3.4.11", "jsbarcode": "^3.12.1", "qrcode.react": "^4.2.0" }, diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx index 51982e819e..fef917a014 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/DataMatrix.tsx @@ -5,6 +5,7 @@ import { gs1datamatrix, gs1datamatrixrectangular } from "@bwip-js/browser"; +import DOMPurify from "dompurify"; import { ReactElement, useMemo, useRef } from "react"; import { DownloadButton } from "./DownloadButton"; import { DataMatrixTypeConfig } from "../config/Barcode.config"; @@ -16,25 +17,27 @@ interface DataMatrixRendererProps { config: DataMatrixTypeConfig; } +type DataMatrixEncodeParams = Pick; + /** Selects the bwip-js encoder for the requested GS1 mode and symbol shape. */ -function encodeDataMatrix(config: DataMatrixTypeConfig): string { +function encodeDataMatrix({ codeValue, size, margin, gs1Mode, shape }: DataMatrixEncodeParams): string { const opts = { - text: config.codeValue, + text: codeValue, // bwip-js scale is in module units; map the pixel size onto a reasonable scale. - scale: Math.max(1, Math.round(config.size / 32)), - paddingwidth: config.margin, - paddingheight: config.margin, + scale: Math.max(1, Math.round(size / 32)), + paddingwidth: margin, + paddingheight: margin, // GS1 AI syntax uses parentheses; parse must be on for the human-readable form. - parse: config.gs1Mode + parse: gs1Mode } as const; - if (config.gs1Mode) { - return config.shape === "rectangle" + if (gs1Mode) { + return shape === "rectangle" ? gs1datamatrixrectangular({ ...opts, bcid: "gs1datamatrixrectangular" }, drawingSVG()) : gs1datamatrix({ ...opts, bcid: "gs1datamatrix" }, drawingSVG()); } - return config.shape === "rectangle" + return shape === "rectangle" ? datamatrixrectangular({ ...opts, bcid: "datamatrixrectangular" }, drawingSVG()) : datamatrix({ ...opts, bcid: "datamatrix" }, drawingSVG()); } @@ -53,7 +56,7 @@ function getSvgPixelSize(svg: string, size: number): { width: number; height: nu export function DataMatrixRenderer({ config }: DataMatrixRendererProps): ReactElement { const containerRef = useRef(null); - const { codeValue, downloadButton, size, gs1Mode } = config; + const { codeValue, downloadButton, size, gs1Mode, shape, margin, logLevel } = config; const buttonPosition = downloadButton?.buttonPosition ?? "bottom"; const { svg, error } = useMemo<{ svg: string | null; error: boolean }>(() => { @@ -63,27 +66,26 @@ export function DataMatrixRenderer({ config }: DataMatrixRendererProps): ReactEl const baseValidation = validateBarcodeValue("DataMatrix", codeValue); if (!baseValidation.valid) { - printError(`Validation failed for Data Matrix: ${baseValidation.message}`, config.logLevel); + printError(`Validation failed for Data Matrix: ${baseValidation.message}`, logLevel); return { svg: null, error: true }; } if (gs1Mode) { const gs1Validation = validateGs1DataMatrixValue(codeValue); if (!gs1Validation.valid) { - printError(`GS1 Data Matrix validation failed: ${gs1Validation.message}`, config.logLevel); + printError(`GS1 Data Matrix validation failed: ${gs1Validation.message}`, logLevel); return { svg: null, error: true }; } } try { - return { svg: encodeDataMatrix(config), error: false }; + return { svg: encodeDataMatrix({ codeValue, size, margin, gs1Mode, shape }), error: false }; } catch (e) { const message = e instanceof Error ? e.message : "Error generating Data Matrix"; - printError(`Rendering failed: ${message} \nValue: "${codeValue}"`, config.logLevel); + printError(`Rendering failed: ${message} \nValue: "${codeValue}"`, logLevel); return { svg: null, error: true }; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [codeValue, size, gs1Mode, config.shape, config.margin, config.logLevel]); + }, [codeValue, size, gs1Mode, shape, margin, logLevel]); if (error || !svg) { return ( @@ -117,7 +119,9 @@ export function DataMatrixRenderer({ config }: DataMatrixRendererProps): ReactEl ref={containerRef} className="datamatrix-svg" style={{ width, height }} - dangerouslySetInnerHTML={{ __html: svg }} + dangerouslySetInnerHTML={{ + __html: DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } }) + }} /> {buttonPosition === "bottom" && button}
diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/DataMatrixPreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/DataMatrixPreview.tsx index 89fdcff583..4bac0830d8 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/DataMatrixPreview.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/DataMatrixPreview.tsx @@ -1,4 +1,5 @@ import { datamatrix, drawingSVG, gs1datamatrix } from "@bwip-js/browser"; +import DOMPurify from "dompurify"; import { ReactElement, useMemo } from "react"; import { BarcodeGeneratorPreviewProps } from "../../../typings/BarcodeGeneratorProps"; @@ -30,9 +31,11 @@ export function DataMatrixPreview(props: DataMatrixPreviewProps): ReactElement { {restProps.buttonPosition === "top" && downloadButton} {svg ? (
) : (
diff --git a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss index 4dff78d709..8a66daa53e 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss +++ b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss @@ -109,7 +109,7 @@ $widget-prefix: "barcode-generator"; object-fit: contain; } -.datamatrix-preview-image { +.barcode-generator-datamatrix-preview-image { max-width: 100%; svg { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ec3ceefd1..48cf37194a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -560,6 +560,9 @@ importers: classnames: specifier: ^2.5.1 version: 2.5.1 + dompurify: + specifier: ^3.4.11 + version: 3.4.13 jsbarcode: specifier: ^3.12.1 version: 3.12.3 @@ -4531,6 +4534,7 @@ packages: '@plotly/mapbox-gl@1.13.4': resolution: {integrity: sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==} engines: {node: '>=6.4.0'} + deprecated: This package is deprecated as of August 2026. plotly.js v4 uses MapLibre for map traces — see https://github.com/maplibre/maplibre-gl-js. '@plotly/point-cluster@3.1.9': resolution: {integrity: sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==} From b8ecc7e10b3c569fb2da9fb7158f0a3a2cd4017a Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Wed, 29 Jul 2026 12:16:20 +0200 Subject: [PATCH 12/16] fix(barcode-generator-web): use constant for maximum Data Matrix and QR Code value length validation --- .../barcode-generator-web/src/config/validation.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts index fa28abd47c..58b2ea60fb 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/validation.ts @@ -1,5 +1,7 @@ import { AddonFormatEnum, CodeFormatEnum, CustomCodeFormatEnum } from "../../typings/BarcodeGeneratorProps"; +const MAX_2D_BARCODE_STATIC_VALUE_LENGTH = 2000; + export type ValidationResult = | { valid: true; @@ -115,8 +117,8 @@ export function validateBarcodeValue(format: CustomCodeFormatEnum | CodeFormatEn } return { valid: true }; case "DataMatrix": - // DataMatrix: encoder handles the heavy lifting; guard extremely long static values - if (value.length > 2000) { + // DataMatrix: encoder handles the heavy lifting; guard extremely long static values. + if (value.length > MAX_2D_BARCODE_STATIC_VALUE_LENGTH) { return { valid: false, message: "The Data Matrix value is very long; consider a shorter value or a dynamic attribute." @@ -124,8 +126,8 @@ export function validateBarcodeValue(format: CustomCodeFormatEnum | CodeFormatEn } return { valid: true }; case "QRCode": - // QRCode: accepts most characters, but warn for extremely long static values - if (value.length > 1200) { + // QRCode: accepts most characters, but warn for extremely long static values. + if (value.length > MAX_2D_BARCODE_STATIC_VALUE_LENGTH) { return { valid: false, message: From fc85c818b81d7d41ba35c2a53569614c14952d5a Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 13 Aug 2026 10:27:10 +0200 Subject: [PATCH 13/16] fix(barcode-generator-web): add a dedicated Data Matrix margin property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Data Matrix reused codeMargin, which is documented in pixels, but bwip-js multiplies paddingwidth/paddingheight by scale — so the Data Matrix margin is in module units, like qrMargin. A codeMargin of 0 chosen for a 1D barcode therefore stripped the Data Matrix quiet zone and made the symbol unscannable. Add dmMargin (module units, default 2) and branch the margin per format. Co-Authored-By: Claude Opus 5 --- .../src/BarcodeGenerator.xml | 4 ++++ .../src/__tests__/BarcodeGenerator.spec.tsx | 1 + .../src/config/Barcode.config.ts | 16 +++++++++++++++- .../src/config/__tests__/Barcode.config.spec.ts | 15 +++++++++++++-- .../typings/BarcodeGeneratorProps.d.ts | 2 ++ 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index 63401d264e..d59aaf8c9c 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -192,6 +192,10 @@ Margin size Number of module units (QR grid cells) to use for margin. Increasing compresses the QR pattern within the fixed size. Note: not visible in preview. + + Margin size + Number of module units (Data Matrix cells) to use for the quiet zone. Increasing compresses the symbol within the fixed size. Keep at least 1 to stay scannable. Note: not visible in preview. + Title Used for accessibility diff --git a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx index bae5454307..0f1eb952c9 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx @@ -86,6 +86,7 @@ const createBarcodeProps = ( dmGs1Mode: false, dmShape: "square", dmSize: 128, + dmMargin: 2, qrTitle: dynamic.available(""), qrLevel: "L", qrOverlay: false, diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts index f9c62452c7..b46e41cf0f 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts @@ -80,7 +80,8 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon const baseConfig: CodeBaseTypeConfig = { codeValue, - margin: (format === "QRCode" ? props.qrMargin : props.codeMargin) ?? 2, + // 1D barcodes measure the margin in pixels; QR and Data Matrix in module units + margin: getMargin(props, format) ?? 2, logLevel: props.logLevel, downloadButton: downloadButtonConfig }; @@ -137,6 +138,19 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon }; } +function getMargin( + props: BarcodeGeneratorContainerProps, + format: CodeFormatEnum | CustomCodeFormatEnum +): number | undefined { + if (format === "QRCode") { + return props.qrMargin; + } + if (format === "DataMatrix") { + return props.dmMargin; + } + return props.codeMargin; +} + function getFileName(customFileName: string | undefined): string | undefined { // Use custom filename if provided if (customFileName && customFileName.trim()) { diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/Barcode.config.spec.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/Barcode.config.spec.ts index dab11ac0a0..63daacc6e8 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/Barcode.config.spec.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/__tests__/Barcode.config.spec.ts @@ -30,6 +30,7 @@ function props(overrides: Partial = {}): Barcode dmGs1Mode: false, dmShape: "square", dmSize: 128, + dmMargin: 2, qrTitle: dynamic.available("QR"), qrLevel: "L", qrOverlay: false, @@ -62,11 +63,21 @@ describe("barcodeConfig - DataMatrix", () => { expect(config.shape).toBe("rectangle"); }); - it("uses the DataMatrix margin from codeMargin", () => { - const config = barcodeConfig(props({ codeMargin: 6 })) as DataMatrixTypeConfig; + it("uses dmMargin for the DataMatrix margin, not the 1D or QR margin", () => { + const config = barcodeConfig(props({ dmMargin: 6, codeMargin: 4, qrMargin: 8 })) as DataMatrixTypeConfig; expect(config.margin).toBe(6); }); + it("falls back to a default margin when dmMargin is unset", () => { + const config = barcodeConfig(props({ dmMargin: null as any })) as DataMatrixTypeConfig; + expect(config.margin).toBe(2); + }); + + it("keeps the pixel margin for 1D barcodes and the module margin for QR", () => { + expect(barcodeConfig(props({ codeFormat: "CODE128", codeMargin: 4 })).margin).toBe(4); + expect(barcodeConfig(props({ codeFormat: "QRCode", qrMargin: 8 })).margin).toBe(8); + }); + it("does not route non-DataMatrix formats to datamatrix", () => { expect(barcodeConfig(props({ codeFormat: "QRCode" })).type).toBe("qrcode"); expect(barcodeConfig(props({ codeFormat: "CODE128" })).type).toBe("barcode"); diff --git a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts index 6139010d28..ea4140c288 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -54,6 +54,7 @@ export interface BarcodeGeneratorContainerProps { codeHeight: number; codeMargin: number; qrMargin: number; + dmMargin: number; qrTitle: DynamicValue; showTitle: boolean; qrOverlay: boolean; @@ -106,6 +107,7 @@ export interface BarcodeGeneratorPreviewProps { codeHeight: number | null; codeMargin: number | null; qrMargin: number | null; + dmMargin: number | null; qrTitle: string; showTitle: boolean; qrOverlay: boolean; From 0b916de4b75800964607d1593fee26339218c226 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 13 Aug 2026 10:27:20 +0200 Subject: [PATCH 14/16] fix(barcode-generator-web): scope property visibility to the selected format The Advanced Data Matrix Settings group was never hidden, so it showed for Barcode, QR Code and Custom. The reverse leaked too: because the existing conditions enumerated QRCode/CODE128/Custom, selecting Data Matrix still showed bar width, code height, display value, EAN-128, Mod43 and the EAN addon properties. Replace the enumerated format checks with intent-named booleans so adding a format cannot silently reopen these, and gate check() to validate only the sizing properties that are visible. Also report QR size problems on qrSize instead of codeHeight, and validate static GS1 values at design time. Co-Authored-By: Claude Opus 5 --- .../src/BarcodeGenerator.editorConfig.ts | 109 ++++++--- .../BarcodeGenerator.editorConfig.spec.ts | 221 ++++++++++++++++++ 2 files changed, 292 insertions(+), 38 deletions(-) create mode 100644 packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.editorConfig.spec.ts diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts index 3cdb497b87..d31856912c 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts @@ -1,7 +1,7 @@ import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools"; import { StructurePreviewProps } from "@mendix/widget-plugin-platform/preview/structure-preview-api"; import { BarcodeGeneratorPreviewProps, CodeFormatEnum, CustomCodeFormatEnum } from "../typings/BarcodeGeneratorProps"; -import { validateAddonValue, validateBarcodeValue } from "./config/validation"; +import { validateAddonValue, validateBarcodeValue, validateGs1DataMatrixValue } from "./config/validation"; export type Problem = { property?: string; // key of the property, at which the problem exists @@ -13,7 +13,12 @@ export type Problem = { }; export function getProperties(values: BarcodeGeneratorPreviewProps, defaultProperties: Properties): Properties { - if (values.codeFormat === "QRCode") { + const isQrCode = values.codeFormat === "QRCode"; + const isDataMatrix = values.codeFormat === "DataMatrix"; + // Both "Barcode" (CODE128) and "Custom" render as 1D barcodes through JsBarcode + const isBarcode = !isQrCode && !isDataMatrix; + + if (isQrCode) { hidePropertiesIn(defaultProperties, values, ["codeWidth", "codeHeight", "displayValue", "codeMargin"]); } else { hidePropertiesIn(defaultProperties, values, [ @@ -26,7 +31,15 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope ]); } - if (values.codeFormat !== "QRCode" || !values.qrOverlay) { + if (isDataMatrix) { + // Data Matrix is a 2D symbol: bar width/height, the human-readable value and the + // pixel-based 1D margin don't apply — it uses dmMargin (module units) instead + hidePropertiesIn(defaultProperties, values, ["codeWidth", "codeHeight", "displayValue", "codeMargin"]); + } else { + hidePropertiesIn(defaultProperties, values, ["dmGs1Mode", "dmShape", "dmSize", "dmMargin"]); + } + + if (!isQrCode || !values.qrOverlay) { hidePropertiesIn(defaultProperties, values, [ "qrOverlaySrc", "qrOverlayCenter", @@ -39,7 +52,8 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope ]); } - if (values.codeFormat === "QRCode" || (values.codeFormat !== "CODE128" && values.customCodeFormat !== "CODE128")) { + // EAN-128 only applies to CODE128, either as the top-level format or the custom one + if (!isBarcode || (values.codeFormat === "Custom" && values.customCodeFormat !== "CODE128")) { hidePropertyIn(defaultProperties, values, "enableEan128"); } @@ -67,29 +81,20 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope } // EAN addons are only supported for EAN-13, EAN-8, and UPC - if ( - values.codeFormat === "QRCode" || - values.codeFormat === "CODE128" || - (values.codeFormat === "Custom" && - values.customCodeFormat !== "EAN13" && - values.customCodeFormat !== "EAN8" && - values.customCodeFormat !== "UPC") - ) { + const supportsAddons = + values.codeFormat === "Custom" && + (values.customCodeFormat === "EAN13" || + values.customCodeFormat === "EAN8" || + values.customCodeFormat === "UPC"); + if (!supportsAddons) { hidePropertiesIn(defaultProperties, values, ["addonFormat", "addonValue", "addonSpacing"]); } - if ( - values.codeFormat === "QRCode" || - values.codeFormat === "CODE128" || - (values.codeFormat === "Custom" && values.addonFormat !== "EAN5" && values.addonFormat !== "EAN2") - ) { + if (!supportsAddons || (values.addonFormat !== "EAN5" && values.addonFormat !== "EAN2")) { hidePropertiesIn(defaultProperties, values, ["addonValue", "addonSpacing"]); } - if ( - values.codeFormat === "QRCode" || - values.codeFormat === "CODE128" || - (values.codeFormat === "Custom" && values.customCodeFormat !== "CODE39") - ) { + // Mod43 is a CODE39 check digit + if (!(values.codeFormat === "Custom" && values.customCodeFormat === "CODE39")) { hidePropertyIn(defaultProperties, values, "enableMod43"); } @@ -122,30 +127,52 @@ export function getPreview(_: StructurePreviewProps, _isDarkMode: boolean): Stru export function check(_values: BarcodeGeneratorPreviewProps): Problem[] { const errors: Problem[] = []; - if (!_values.codeWidth || _values.codeWidth < 1) { - errors.push({ - property: `codeWidth`, - severity: "error", - message: `The value of 'Bar width' must be at least 1.` - }); - } + // Only validate the sizing properties that are visible for the selected format + if (_values.codeFormat !== "QRCode" && _values.codeFormat !== "DataMatrix") { + if (!_values.codeWidth || _values.codeWidth < 1) { + errors.push({ + property: `codeWidth`, + severity: "error", + message: `The value of 'Bar width' must be at least 1.` + }); + } - if (!_values.codeHeight || _values.codeHeight < 20) { - errors.push({ - property: `codeHeight`, - severity: "error", - message: `The value of 'Code height' must be at least 20.` - }); + if (!_values.codeHeight || _values.codeHeight < 20) { + errors.push({ + property: `codeHeight`, + severity: "error", + message: `The value of 'Code height' must be at least 20.` + }); + } } - if (!_values.qrSize || _values.qrSize < 50) { + if (_values.codeFormat === "QRCode" && (!_values.qrSize || _values.qrSize < 50)) { errors.push({ - property: `codeHeight`, + property: `qrSize`, severity: "error", message: `The value of 'QR size' must be at least 50.` }); } + if (_values.codeFormat === "DataMatrix") { + if (!_values.dmSize || _values.dmSize < 32) { + errors.push({ + property: `dmSize`, + severity: "error", + message: `The value of 'Data Matrix size' must be at least 32.` + }); + } + + // The Data Matrix spec requires a quiet zone of at least one module on every side + if (!_values.dmMargin || _values.dmMargin < 1) { + errors.push({ + property: `dmMargin`, + severity: "warning", + message: `A Data Matrix needs a quiet zone of at least 1 module unit to stay scannable.` + }); + } + } + // Design-time validation for static barcode value(s) const valueProblems = validateCodeValues(_values); return errors.concat(valueProblems); @@ -187,7 +214,8 @@ function getFormatHint(format: string): string { MSI: "MSI: numeric only (max 30 digits)", pharmacode: "Pharmacode: numeric only (max 7 digits)", codabar: "Codabar: digits, A-D start/stop, and - $ : / . + (max 20 chars)", - QRCode: "QR Code: any text (max 1200 chars recommended)" + QRCode: "QR Code: any text (max 1200 chars recommended)", + DataMatrix: "Data Matrix: any text; GS1 mode expects Application Identifier syntax, e.g. (01)09501101020917" }; return hints[format] || ""; } @@ -218,6 +246,11 @@ function validateCodeValues(values: BarcodeGeneratorPreviewProps): Problem[] { if (!result.valid) { const msg = result.message || "Invalid barcode value for selected format."; problems.push({ property: "codeValue", severity: "error", message: msg }); + } else if (format === "DataMatrix" && values.dmGs1Mode) { + const gs1Result = validateGs1DataMatrixValue(val); + if (!gs1Result.valid) { + problems.push({ property: "codeValue", severity: "error", message: gs1Result.message }); + } } } } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.editorConfig.spec.ts b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.editorConfig.spec.ts new file mode 100644 index 0000000000..7ac785b21c --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.editorConfig.spec.ts @@ -0,0 +1,221 @@ +import { Properties } from "@mendix/pluggable-widgets-tools"; +import { BarcodeGeneratorPreviewProps } from "../../typings/BarcodeGeneratorProps"; +import { check, getProperties } from "../BarcodeGenerator.editorConfig"; + +const ALL_KEYS = [ + "codeValue", + "codeFormat", + "emptyMessage", + "allowDownload", + "downloadButtonCaption", + "downloadButtonAriaLabel", + "downloadFileName", + "buttonPosition", + "customCodeFormat", + "enableEan128", + "enableFlat", + "lastChar", + "enableMod43", + "addonFormat", + "addonValue", + "addonSpacing", + "qrLevel", + "qrSize", + "dmGs1Mode", + "dmShape", + "dmSize", + "logLevel", + "displayValue", + "showAsCard", + "codeWidth", + "codeHeight", + "codeMargin", + "qrMargin", + "dmMargin", + "qrTitle", + "showTitle", + "qrOverlay", + "qrOverlaySrc", + "qrOverlayCenter", + "qrOverlayX", + "qrOverlayY", + "qrOverlayHeight", + "qrOverlayWidth", + "qrOverlayOpacity", + "qrOverlayExcavate" +] as const; + +function defaultValues(overrides: Partial = {}): BarcodeGeneratorPreviewProps { + return { + className: "", + class: "", + style: "", + readOnly: false, + renderMode: "design", + translate: (text: string) => text, + codeValue: "", + codeFormat: "CODE128", + emptyMessage: "", + allowDownload: false, + downloadButtonCaption: "", + downloadButtonAriaLabel: "", + downloadFileName: "", + buttonPosition: "bottom", + customCodeFormat: "CODE128", + enableEan128: false, + enableFlat: false, + lastChar: "", + enableMod43: false, + addonFormat: "None", + addonValue: "", + addonSpacing: 20, + qrLevel: "L", + qrSize: 128, + dmGs1Mode: false, + dmShape: "square", + dmSize: 128, + logLevel: "None", + displayValue: false, + showAsCard: false, + codeWidth: 2, + codeHeight: 200, + codeMargin: 2, + qrMargin: 2, + dmMargin: 2, + qrTitle: "QR Code", + showTitle: false, + qrOverlay: false, + qrOverlaySrc: null, + qrOverlayCenter: true, + qrOverlayX: 0, + qrOverlayY: 0, + qrOverlayHeight: 24, + qrOverlayWidth: 24, + qrOverlayOpacity: 1, + qrOverlayExcavate: true, + ...overrides + }; +} + +/** Mirrors the XML property tree as a flat group so `hidePropertiesIn` can splice from it. */ +function allProperties(): Properties { + return [ + { + caption: "All", + propertyGroups: [ + { + caption: "Flat", + properties: ALL_KEYS.map(key => ({ key, caption: key, description: "", type: "string" })) + } + ] + } + ]; +} + +function visibleKeys(overrides: Partial = {}): string[] { + const values = defaultValues(overrides); + const properties = getProperties(values, allProperties()); + return properties[0].propertyGroups![0].properties!.map(prop => prop.key); +} + +describe("BarcodeGenerator editor config", () => { + describe("Data Matrix settings", () => { + const dataMatrixKeys = ["dmGs1Mode", "dmShape", "dmSize", "dmMargin"]; + + it("shows Data Matrix settings for the Data Matrix format", () => { + expect(visibleKeys({ codeFormat: "DataMatrix" })).toEqual(expect.arrayContaining(dataMatrixKeys)); + }); + + it.each(["CODE128", "QRCode", "Custom"] as const)("hides Data Matrix settings for %s", codeFormat => { + const visible = visibleKeys({ codeFormat }); + dataMatrixKeys.forEach(key => expect(visible).not.toContain(key)); + }); + }); + + describe("barcode-only settings", () => { + it("hides bar sizing, human-readable value and the pixel margin for Data Matrix", () => { + const visible = visibleKeys({ codeFormat: "DataMatrix" }); + expect(visible).not.toContain("codeWidth"); + expect(visible).not.toContain("codeHeight"); + expect(visible).not.toContain("displayValue"); + // Data Matrix has its own margin in module units + expect(visible).not.toContain("codeMargin"); + expect(visible).not.toContain("qrMargin"); + expect(visible).toContain("dmMargin"); + }); + + it("hides advanced barcode settings for Data Matrix", () => { + const visible = visibleKeys({ codeFormat: "DataMatrix" }); + [ + "enableEan128", + "enableFlat", + "lastChar", + "enableMod43", + "addonFormat", + "addonValue", + "addonSpacing" + ].forEach(key => expect(visible).not.toContain(key)); + }); + + it("hides QR settings for Data Matrix", () => { + const visible = visibleKeys({ codeFormat: "DataMatrix" }); + ["qrOverlay", "qrSize", "qrMargin", "qrLevel", "qrTitle", "showTitle"].forEach(key => + expect(visible).not.toContain(key) + ); + }); + + it("keeps EAN-128 for CODE128 and custom CODE128 only", () => { + expect(visibleKeys({ codeFormat: "CODE128" })).toContain("enableEan128"); + expect(visibleKeys({ codeFormat: "Custom", customCodeFormat: "CODE128" })).toContain("enableEan128"); + expect(visibleKeys({ codeFormat: "Custom", customCodeFormat: "EAN13" })).not.toContain("enableEan128"); + }); + + it("keeps Mod43 for custom CODE39 only", () => { + expect(visibleKeys({ codeFormat: "Custom", customCodeFormat: "CODE39" })).toContain("enableMod43"); + expect(visibleKeys({ codeFormat: "CODE128" })).not.toContain("enableMod43"); + }); + + it("keeps EAN addons for EAN-13, EAN-8 and UPC only", () => { + expect(visibleKeys({ codeFormat: "Custom", customCodeFormat: "EAN13" })).toContain("addonFormat"); + expect(visibleKeys({ codeFormat: "Custom", customCodeFormat: "CODE93" })).not.toContain("addonFormat"); + }); + }); + + describe("check", () => { + it("validates Data Matrix size instead of bar sizing", () => { + const problems = check( + defaultValues({ codeFormat: "DataMatrix", dmSize: 10, codeHeight: 0, codeWidth: 0 }) + ); + expect(problems).toHaveLength(1); + expect(problems[0].property).toBe("dmSize"); + }); + + it("warns when the Data Matrix quiet zone is dropped", () => { + const problems = check(defaultValues({ codeFormat: "DataMatrix", dmMargin: 0 })); + expect(problems).toEqual([expect.objectContaining({ property: "dmMargin", severity: "warning" })]); + }); + + it("reports QR size problems on the qrSize property", () => { + const problems = check(defaultValues({ codeFormat: "QRCode", qrSize: 10 })); + expect(problems.map(problem => problem.property)).toContain("qrSize"); + }); + + it("flags malformed GS1 values at design time", () => { + const problems = check( + defaultValues({ codeFormat: "DataMatrix", dmGs1Mode: true, codeValue: '"not-gs1"' }) + ); + expect(problems.some(problem => problem.property === "codeValue")).toBe(true); + }); + + it("accepts well-formed GS1 values", () => { + const problems = check( + defaultValues({ + codeFormat: "DataMatrix", + dmGs1Mode: true, + codeValue: '"(01)09501101020917(17)261231(10)ABC123"' + }) + ); + expect(problems).toHaveLength(0); + }); + }); +}); From 6447a851a7b20b5cec2f093618224fe7926819d9 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 13 Aug 2026 10:27:29 +0200 Subject: [PATCH 15/16] test(barcode-generator-web): add Data Matrix e2e coverage Replace the placeholder body assertion with plain render, GS1 render, rectangular shape, value re-render and PNG download tests. These stay dormant: mendix/testProjects has no barcode-generator-web branch yet, so the e2e script remains stubbed. The spec header lists the page route and mx-names the test project needs before the runner can be enabled. Co-Authored-By: Claude Opus 5 --- .../e2e/BarcodeGenerator.spec.js | 72 ++++++++++++++++--- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/e2e/BarcodeGenerator.spec.js b/packages/pluggableWidgets/barcode-generator-web/e2e/BarcodeGenerator.spec.js index 433adf2aea..8557847ba0 100644 --- a/packages/pluggableWidgets/barcode-generator-web/e2e/BarcodeGenerator.spec.js +++ b/packages/pluggableWidgets/barcode-generator-web/e2e/BarcodeGenerator.spec.js @@ -1,20 +1,70 @@ import { test, expect } from "@mendix/run-e2e/fixtures"; -import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers"; +/** + * These tests are dormant until the test project exists: `package.json` still has + * `"e2e": "echo ..."` because https://github.com/mendix/testProjects has no + * `barcode-generator-web` branch yet (only `barcode-scanner-web`). + * + * To enable, create that branch with a `/p/datamatrix` page containing: + * - dataMatrixPlain Data Matrix, GS1 off, square, value "ABC-12345" + * - dataMatrixGs1 Data Matrix, GS1 on, value "(01)09501101020917(17)261231(10)ABC123" + * - dataMatrixRectangle Data Matrix, shape Rectangle + * - dataMatrixDownload Data Matrix with "Allow download" on, file name "datamatrix" + * - textBoxCodeValue text box bound to the attribute dataMatrixBound reads from + * - dataMatrixBound Data Matrix bound to that attribute + * then swap the `e2e` script to `run-e2e ci`. + */ test.describe("BarcodeGenerator", () => { test.beforeEach(async ({ page }) => { - await page.goto("/"); - await waitForMendixApp(page); + await page.goto("/p/datamatrix"); }); - test("renders barcode generator widget", async ({ page }) => { - // TODO: Replace with actual barcode generator test when implementation is complete - // Example test structure for barcode generator: - // await expect(page.locator(".mx-name-barcodeGenerator").first()).toBeVisible(); - // await page.locator(".mx-name-textInput").fill("Test QR Code"); - // await expect(page.locator(".mx-name-barcodeGenerator canvas")).toBeVisible(); + test("renders a Data Matrix symbol as inline SVG @smoke", async ({ page }) => { + const symbol = page.locator(".mx-name-dataMatrixPlain .datamatrix-svg svg"); - // Placeholder test for now - await expect(page.locator("body")).toBeVisible(); + await expect(symbol).toBeVisible(); + await expect(symbol).toHaveAttribute("viewBox", /^0 0 \d+(\.\d+)? \d+(\.\d+)?$/); + }); + + test("renders a GS1 Data Matrix without falling back to the error state", async ({ page }) => { + const widget = page.locator(".mx-name-dataMatrixGs1"); + + await expect(widget.locator(".datamatrix-svg svg")).toBeVisible(); + await expect(widget.locator(".alert-danger")).toHaveCount(0); + }); + + test("renders the rectangular shape wider than it is tall", async ({ page }) => { + const symbol = page.locator(".mx-name-dataMatrixRectangle .datamatrix-svg svg"); + await expect(symbol).toBeVisible(); + + await expect + .poll(async () => { + const box = await symbol.boundingBox(); + return box ? box.width > box.height : false; + }) + .toBe(true); + }); + + test("re-renders when the bound value changes", async ({ page }) => { + const symbol = page.locator(".mx-name-dataMatrixBound .datamatrix-svg svg"); + await expect(symbol).toBeVisible(); + const before = await symbol.getAttribute("viewBox"); + + // A longer value needs more modules, so the symbol grows + await page.locator(".mx-name-textBoxCodeValue input").fill("ABC-12345-67890-LONGER-VALUE"); + await page.locator(".mx-name-textBoxCodeValue input").blur(); + + await expect(symbol).not.toHaveAttribute("viewBox", before); + }); + + test("downloads the Data Matrix as a PNG", async ({ page }) => { + await expect(page.locator(".mx-name-dataMatrixDownload .datamatrix-svg svg")).toBeVisible(); + + // Start waiting for the download before clicking + const downloadPromise = page.waitForEvent("download"); + await page.locator(".mx-name-dataMatrixDownload .barcode-generator-download-button").click(); + const download = await downloadPromise; + + expect(download.suggestedFilename()).toMatch(/\.png$/); }); }); From 8086499488c6d1f6f142e246fe2a7316bf7a8c89 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 13 Aug 2026 10:27:37 +0200 Subject: [PATCH 16/16] docs(openspec): record margin, visibility and verification outcomes Add requirements for the dedicated Data Matrix sizing/quiet zone and for format-scoped property visibility, resolve the design doc's open question on sizing props with the bwip-js scaling evidence, and update the task list: the build now succeeds (~228 KB minified / ~73 KB gzipped for the tree-shaken Data Matrix encoders) and e2e is written but dormant. Co-Authored-By: Claude Opus 5 --- .../add-datamatrix-generation/design.md | 2 +- .../specs/barcode-generation/spec.md | 33 +++++++++++++++++++ .../add-datamatrix-generation/tasks.md | 7 ++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/design.md b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/design.md index edc56608a6..ab3a5e1a48 100644 --- a/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/design.md +++ b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/design.md @@ -48,4 +48,4 @@ Additive, no data migration. New dependency added to `package.json`; `typings/Ba ## Open Questions - Exact bwip-js option for rectangular DataMatrix (shape flag vs explicit `rows`/`columns`) — confirm at implementation. -- Whether to reuse `codeMargin`/`qrSize`-style sizing or add dedicated `dmSize`/`dmMargin` props — lean toward dedicated to avoid overloading 1D "bar width" semantics. +- ~~Whether to reuse `codeMargin`/`qrSize`-style sizing or add dedicated `dmSize`/`dmMargin` props — lean toward dedicated to avoid overloading 1D "bar width" semantics.~~ Resolved: dedicated `dmSize` and `dmMargin`. bwip-js `paddingwidth`/`paddingheight` are multiplied by `scale`, so the Data Matrix margin is in module units like `qrMargin`, not pixels like `codeMargin` — reusing `codeMargin` would have mislabelled the unit and let a 1D margin of 0 strip the required quiet zone. diff --git a/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md index e97c65b5b5..d615f9a0dd 100644 --- a/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md +++ b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/specs/barcode-generation/spec.md @@ -84,6 +84,39 @@ The widget SHALL allow downloading a generated DataMatrix as a PNG using the exi - **WHEN** downloads are enabled and the user activates the download button on a DataMatrix - **THEN** a PNG file of the rendered DataMatrix is downloaded, using the configured or an auto-generated filename +### Requirement: DataMatrix sizing and quiet zone + +The widget SHALL expose dedicated size and margin properties for Data Matrix, with the margin expressed in module units so the required quiet zone can be controlled independently of the 1D barcode margin. + +#### Scenario: Margin is independent of the 1D and QR margins + +- **WHEN** the format is "Data Matrix" and the Data Matrix margin is set to 6 while the 1D margin is 4 and the QR margin is 8 +- **THEN** the rendered symbol uses a quiet zone of 6 module units, and changing the 1D or QR margin has no effect on it + +#### Scenario: Missing quiet zone is flagged + +- **WHEN** the format is "Data Matrix" and the Data Matrix margin is set to 0 +- **THEN** Studio Pro shows a warning that at least 1 module unit is needed for the symbol to stay scannable + +### Requirement: Format-scoped property visibility + +The widget SHALL only show the property groups that apply to the selected barcode format, so Data Matrix settings are hidden for other formats and 1D/QR settings are hidden for Data Matrix. + +#### Scenario: Data Matrix settings are scoped to the Data Matrix format + +- **WHEN** the Barcode Format is "Barcode", "QR Code" or "Custom" +- **THEN** the "Advanced Data Matrix Settings" properties (GS1 mode, symbol shape, size) are hidden in Studio Pro + +#### Scenario: 1D and QR settings are hidden for Data Matrix + +- **WHEN** the Barcode Format is "Data Matrix" +- **THEN** bar width, code height, display value, the 1D pixel margin, the advanced barcode settings (EAN-128, flat, last character, Mod43), the EAN addon properties and the QR properties are all hidden, and the Data Matrix margin is shown in their place + +#### Scenario: Design-time validation follows the visible properties + +- **WHEN** the Barcode Format is "Data Matrix" and the Data Matrix size is below the supported minimum +- **THEN** Studio Pro reports the problem on the Data Matrix size property and does not report problems on hidden 1D or QR sizing properties + ### Requirement: DataMatrix editor preview The widget SHALL show a representative DataMatrix preview in the Studio Pro editor when the Data Matrix format is selected. diff --git a/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/tasks.md b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/tasks.md index 48bde20ee7..2aa12c82fe 100644 --- a/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/tasks.md +++ b/packages/pluggableWidgets/barcode-generator-web/openspec/changes/add-datamatrix-generation/tasks.md @@ -2,8 +2,9 @@ - [x] 1.1 Add `@bwip-js/browser` to `packages/pluggableWidgets/barcode-generator-web/package.json` dependencies - [x] 1.2 Add `DataMatrix` enum value to top-level `codeFormat` in `src/BarcodeGenerator.xml` -- [x] 1.3 Add "Advanced Data Matrix Settings" property group in XML: `dmGs1Mode` (boolean, default false), `dmShape` (enum square/rectangle, default square), and sizing property (`dmSize` / reuse margin) +- [x] 1.3 Add "Advanced Data Matrix Settings" property group in XML: `dmGs1Mode` (boolean, default false), `dmShape` (enum square/rectangle, default square), `dmSize` (integer, default 128) and `dmMargin` (integer, default 2, module units — bwip-js scales `paddingwidth`, so this is not the pixel-based `codeMargin`) - [x] 1.4 Build to regenerate `typings/BarcodeGeneratorProps.d.ts` and confirm `CodeFormatEnum` includes `DataMatrix` +- [x] 1.5 Scope property visibility per format in `src/BarcodeGenerator.editorConfig.ts`: hide `dm*` unless `codeFormat === "DataMatrix"`, and hide 1D/QR-only properties (bar sizing, display value, EAN-128, flat, last char, Mod43, EAN addons) when Data Matrix is selected; gate `check()` sizing validation to the visible properties ## 2. Config Model @@ -31,11 +32,11 @@ ## 6. Tests & Changelog - [x] 6.1 Unit tests: config mapping for DataMatrix, GS1 vs plain `bcid` selection, shape option, validation (valid GS1 AI, malformed AI, plain string, empty value) -- [ ] 6.2 Playwright E2E: render + download a DataMatrix per `docs/requirements/e2e-test-guidelines.md` _(blocked: existing e2e spec is a placeholder; needs the external Mendix testProjects page configured with a DataMatrix widget instance)_ +- [~] 6.2 Playwright E2E: `e2e/BarcodeGenerator.spec.js` covers plain render, GS1 render, rectangular shape, value re-render and PNG download _(dormant: `mendix/testProjects` has no `barcode-generator-web` branch — only `barcode-scanner-web` — so `package.json` keeps `"e2e": "echo ..."`. Create the branch with a `/p/datamatrix` page using the mx-names listed in the spec header, then swap the script to `run-e2e ci`.)_ - [x] 6.3 Add user-facing `CHANGELOG.md` entry ("Added Data Matrix and GS1 Data Matrix generation support") ## 7. Verification - [x] 7.1 `cd packages/pluggableWidgets/barcode-generator-web && pnpm run test` passes -- [ ] 7.2 `pnpm run build` succeeds; confirm bundle delta small (bwip-js tree-shaken to DataMatrix encoder only) _(blocked: local build env has a broken rollup binary — stale `.bin/rollup` shim points to uninstalled rollup@3.29.5; unrelated to this change. Code verified via `tsc --noEmit` + jest instead.)_ +- [x] 7.2 `pnpm run build` succeeds; bwip-js is tree-shaken to the Data Matrix encoders (no aztec/pdf417/royalmail/codablockf in the bundle), but they still add ~228 KB minified / ~73 KB gzipped because the bwipp runtime core comes along — larger than "small", acceptable for the feature but worth noting in the PR - [ ] 7.3 Live Studio Pro test: plain DataMatrix scans via Barcode Scanner (round-trip); GS1 value `(01)09501101020917(17)261231(10)ABC123` renders + decodes; PNG download works; rectangular shape renders _(requires human: live Studio Pro session with `MX_PROJECT_PATH` set)_