From eb3d9948e343974907b59d777c15b530d2eba0f4 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:03:58 +0200 Subject: [PATCH 1/8] 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 --- .../barcode-generator-web/package.json | 1 + pnpm-lock.yaml | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) 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 40105448c2..7964de966b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,7 +361,7 @@ importers: version: link:../../shared/eslint-config-web-widgets '@mendix/pluggable-widgets-tools': specifier: 11.11.0 - version: 11.11.0(patch_hash=036a1e3d1a57e7418725babb71e5eef5220ae90fc481ad7e04fa7e8901b25801)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@swc/core@1.15.41)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.4(jiti@2.6.1))(jest-util@30.4.1)(picomatch@4.0.4)(prettier@3.8.4)(react-dom@18.3.1(react@18.3.1))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@18.3.1))(react@18.3.1)(tslib@2.8.1) + version: 11.11.0(patch_hash=036a1e3d1a57e7418725babb71e5eef5220ae90fc481ad7e04fa7e8901b25801)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@swc/core@1.15.41)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.4(jiti@2.6.1))(jest-util@30.4.1)(prettier@3.8.4)(react-dom@18.3.1(react@18.3.1))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@18.3.1))(react@18.3.1)(tslib@2.8.1) '@mendix/prettier-config-web-widgets': specifier: workspace:* version: link:../../shared/prettier-config-web-widgets @@ -538,6 +538,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 @@ -556,7 +559,7 @@ importers: version: link:../../shared/eslint-config-web-widgets '@mendix/pluggable-widgets-tools': specifier: 11.11.0 - version: 11.11.0(patch_hash=036a1e3d1a57e7418725babb71e5eef5220ae90fc481ad7e04fa7e8901b25801)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@swc/core@1.15.41)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.4(jiti@2.6.1))(jest-util@30.4.1)(prettier@3.8.4)(react-dom@18.3.1(react@18.3.1))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@18.3.1))(react@18.3.1)(tslib@2.8.1) + version: 11.11.0(patch_hash=036a1e3d1a57e7418725babb71e5eef5220ae90fc481ad7e04fa7e8901b25801)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@swc/core@1.15.41)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.4(jiti@2.6.1))(jest-util@30.4.1)(picomatch@4.0.4)(prettier@3.8.4)(react-dom@18.3.1(react@18.3.1))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@18.3.1))(react@18.3.1)(tslib@2.8.1) '@mendix/prettier-config-web-widgets': specifier: workspace:* version: link:../../shared/prettier-config-web-widgets @@ -2791,7 +2794,7 @@ importers: devDependencies: '@mendix/pluggable-widgets-tools': specifier: 11.11.0 - version: 11.11.0(patch_hash=036a1e3d1a57e7418725babb71e5eef5220ae90fc481ad7e04fa7e8901b25801)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@swc/core@1.15.41)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.4(jiti@2.6.1))(jest-util@30.4.1)(picomatch@4.0.4)(prettier@3.8.4)(react-dom@18.3.1(react@18.3.1))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@18.3.1))(react@18.3.1)(tslib@2.8.1) + version: 11.11.0(patch_hash=036a1e3d1a57e7418725babb71e5eef5220ae90fc481ad7e04fa7e8901b25801)(@jest/transform@30.3.0)(@jest/types@30.4.1)(@swc/core@1.15.41)(@types/babel__core@7.20.5)(@types/node@24.12.4)(canvas@3.2.3)(eslint@9.39.4(jiti@2.6.1))(jest-util@30.4.1)(prettier@3.8.4)(react-dom@18.3.1(react@18.3.1))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@18.3.1))(react@18.3.1)(tslib@2.8.1) rollup-plugin-copy: specifier: ^3.5.0 version: 3.5.0 @@ -3846,6 +3849,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 @@ -12074,6 +12080,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 83e5b95bd76fa33a01eb6d225ac163c1fee41bf7 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:04:17 +0200 Subject: [PATCH 2/8] 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 874b33bbbcc76e5e4ee6a1c498dc9ea145eece4d Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:04:29 +0200 Subject: [PATCH 3/8] 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 356e8f1e71ea58bce0fd029795272f461d001947 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:04:43 +0200 Subject: [PATCH 4/8] 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 b91d2e85786055e11c635ddb4371bf920cedc5ef Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:04:55 +0200 Subject: [PATCH 5/8] 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 28f61996075548c97ea8274696c753a468efe1fc Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:05:07 +0200 Subject: [PATCH 6/8] 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 27a8cbb91d375ced24deb957d2ce1d48dea900c2 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:05:23 +0200 Subject: [PATCH 7/8] 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 d9b57b4a2c59e6781037b330c7ea33ec846d6956 Mon Sep 17 00:00:00 2001 From: Samuel Reichert Date: Thu, 9 Jul 2026 16:05:34 +0200 Subject: [PATCH 8/8] 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)_