diff --git a/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md b/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md index 21b358e32a..3a3175b3dc 100644 --- a/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md +++ b/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added +- Error handling for incompatible barcode types +- Enhanced preview for all barcode types - Comprehensive configuration and styling settings for various barcode types - Download functionality for barcodes diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts index fc1902a028..0af9c4962f 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts @@ -1,3 +1,4 @@ +import { StructurePreviewProps } from "@mendix/widget-plugin-platform/preview/structure-preview-api"; import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools"; import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps"; import { validateAddonValue, validateBarcodeValue } from "./config/validation"; @@ -15,49 +16,57 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope if (values.codeFormat === "QRCode") { hidePropertiesIn(defaultProperties, values, ["codeWidth", "codeHeight", "displayValue", "codeMargin"]); } else { - hidePropertiesIn(defaultProperties, values, ["qrImage", "qrSize", "qrMargin", "qrLevel", "qrTitle"]); + hidePropertiesIn(defaultProperties, values, ["qrOverlay", "qrSize", "qrMargin", "qrLevel", "qrTitle"]); } - if (values.codeFormat !== "QRCode" || !values.qrImage) { + if (values.codeFormat !== "QRCode" || !values.qrOverlay) { hidePropertiesIn(defaultProperties, values, [ - "qrImageSrc", - "qrImageCenter", - "qrImageWidth", - "qrImageHeight", - "qrImageX", - "qrImageY", - "qrImageOpacity", - "qrImageExcavate" + "qrOverlaySrc", + "qrOverlayCenter", + "qrOverlayWidth", + "qrOverlayHeight", + "qrOverlayX", + "qrOverlayY", + "qrOverlayOpacity", + "qrOverlayExcavate" ]); } - if (values.codeFormat !== "CODE128" && values.customCodeFormat !== "CODE128") { + if (values.codeFormat === "QRCode" || (values.codeFormat !== "CODE128" && values.customCodeFormat !== "CODE128")) { hidePropertyIn(defaultProperties, values, "enableEan128"); } + // enableFlat is only supported for EAN-13 and EAN-8, and NOT when addons are enabled if ( - values.codeFormat === "QRCode" || - values.codeFormat === "CODE128" || - (values.codeFormat === "Custom" && - values.customCodeFormat !== "EAN13" && - values.customCodeFormat !== "EAN8" && - values.customCodeFormat !== "UPC") + !( + values.codeFormat === "Custom" && + (values.customCodeFormat === "EAN13" || values.customCodeFormat === "EAN8") && + values.addonFormat === "None" + ) ) { hidePropertyIn(defaultProperties, values, "enableFlat"); } + // lastChar is only supported for EAN-13, and NOT when flat is enabled or addons are present if ( - values.codeFormat === "QRCode" || - values.codeFormat === "CODE128" || - (values.codeFormat === "Custom" && values.customCodeFormat !== "EAN13") + !( + values.codeFormat === "Custom" && + values.customCodeFormat === "EAN13" && + !values.enableFlat && + values.addonFormat === "None" + ) ) { hidePropertyIn(defaultProperties, values, "lastChar"); } + // 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.codeFormat === "Custom" && + values.customCodeFormat !== "EAN13" && + values.customCodeFormat !== "EAN8" && + values.customCodeFormat !== "UPC") ) { hidePropertiesIn(defaultProperties, values, ["addonFormat", "addonValue", "addonSpacing"]); } @@ -77,8 +86,8 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope hidePropertyIn(defaultProperties, values, "enableMod43"); } - if (values.qrImageCenter) { - hidePropertiesIn(defaultProperties, values, ["qrImageX", "qrImageY"]); + if (values.qrOverlayCenter) { + hidePropertiesIn(defaultProperties, values, ["qrOverlayX", "qrOverlayY"]); } if (values.codeFormat !== "Custom") { @@ -88,6 +97,12 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope return defaultProperties; } +export function getPreview(_: StructurePreviewProps, _isDarkMode: boolean): StructurePreviewProps | null { + // Return null to use the widget icon (BarcodeGenerator.icon.png or BarcodeGenerator.icon.dark.png) + // based on the user's theme settings + return null; +} + export function check(_values: BarcodeGeneratorPreviewProps): Problem[] { const errors: Problem[] = []; @@ -128,28 +143,95 @@ function getActiveFormat(values: BarcodeGeneratorPreviewProps): string { return values.codeFormat; } +function stripQuotes(value: string): string { + // Remove leading/trailing quotes and whitespace from expression values + let trimmed = value.trim(); + // Match and remove surrounding quotes (single or double) + if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) { + trimmed = trimmed.slice(1, -1); + } + return trimmed; +} + +function isDynamicExpression(value: string): boolean { + // Check if the value is a dynamic expression (attribute binding, variable, etc.) + // Dynamic expressions start with $ or contain / paths or are empty + return !value || value.startsWith("$") || value.includes("/"); +} + +function getFormatHint(format: string): string { + const hints: Record = { + EAN13: "EAN-13 requires 12 or 13 numeric digits", + EAN8: "EAN-8 requires 7 or 8 numeric digits", + UPC: "UPC requires 11 or 12 numeric digits", + ITF14: "ITF-14 requires exactly 14 numeric digits", + CODE39: "CODE39: uppercase A-Z, digits, space and - . $ / + % (max 43 chars)", + CODE128: "CODE128: alphanumeric, no control characters (max 80 chars)", + CODE93: "CODE93: alphanumeric, no control characters (max 47 chars)", + 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)" + }; + return hints[format] || ""; +} + function validateCodeValues(values: BarcodeGeneratorPreviewProps): Problem[] { const problems: Problem[] = []; - const val = values.codeValue ?? ""; - const addon = values.addonValue ?? ""; + const rawVal = values.codeValue ?? ""; + const rawAddon = values.addonValue ?? ""; const format = getActiveFormat(values); - // Only validate static (design-time) values — if empty, skip (user may bind dynamically) - if (!val) { - // still validate addon if present - } else { - const result = validateBarcodeValue(format, val); - if (!result.valid) { - const msg = result.message || "Invalid barcode value for selected format."; - problems.push({ property: "codeValue", severity: "warning", message: msg }); + // Add informational hint for dynamic expressions + if (isDynamicExpression(rawVal) && rawVal) { + const hint = getFormatHint(format); + if (hint) { + problems.push({ + property: "codeValue", + severity: "warning", + message: `Dynamic value provided. Ensure runtime value matches format: ${hint}` + }); + } + } + + // Only validate static literal values, skip dynamic expressions (attribute bindings, variables, etc.) + if (!isDynamicExpression(rawVal)) { + const val = stripQuotes(rawVal); + if (val) { + const result = validateBarcodeValue(format, val); + if (!result.valid) { + const msg = result.message || "Invalid barcode value for selected format."; + problems.push({ property: "codeValue", severity: "error", message: msg }); + } } } - // Validate addon value if visible - const addonResult = validateAddonValue(values.addonFormat, addon); - if (!addonResult.valid) { - const msg = addonResult.message || "Invalid addon value."; - problems.push({ property: "addonValue", severity: "warning", message: msg }); + // Validate addon value if visible and format is selected + if (values.addonFormat !== "None") { + // Add informational hint for dynamic addon expressions + if (isDynamicExpression(rawAddon) && rawAddon) { + const addonHint = + values.addonFormat === "EAN5" + ? "EAN-5 addon requires exactly 5 numeric digits" + : "EAN-2 addon requires exactly 2 numeric digits"; + problems.push({ + property: "addonValue", + severity: "warning", + message: `Dynamic addon value provided. Ensure runtime value matches format: ${addonHint}` + }); + } + + // Validate static addon values + if (!isDynamicExpression(rawAddon)) { + const addon = stripQuotes(rawAddon); + if (addon) { + const addonResult = validateAddonValue(values.addonFormat, addon); + if (!addonResult.valid) { + const msg = addonResult.message || "Invalid addon value."; + problems.push({ property: "addonValue", severity: "error", message: msg }); + } + } + } } return problems; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx index 3d0cebc9ba..44f78544d7 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorPreview.tsx @@ -1,13 +1,46 @@ +import classNames from "classnames"; import { ReactElement } from "react"; +import { parseStyle } from "@mendix/widget-plugin-platform/preview/parse-style"; import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps"; -import BarcodePreviewSVG from "./assets/BarcodeGeneratorPreview.svg"; +import { DownloadIcon } from "./components/icons/DownloadIcon"; +import { BarcodePreview } from "./components/preview/BarcodePreview"; +import { QRCodePreview } from "./components/preview/QRCodePreview"; -export function preview(_props: BarcodeGeneratorPreviewProps): ReactElement { - const doc = decodeURI(BarcodePreviewSVG); +const defaultDownloadCaption = "Download"; + +function PreviewDownloadButton(props: BarcodeGeneratorPreviewProps): ReactElement | null { + if (!props.allowDownload) { + return null; + } + + return ( + + {props.downloadButtonCaption || defaultDownloadCaption} + + ); +} + +export function preview(props: BarcodeGeneratorPreviewProps): ReactElement { + const styles = parseStyle(props.style); + const isQrCode = props.codeFormat === "QRCode"; + const downloadButton = ; return ( -
- +
+ {isQrCode ? ( + + ) : ( + + )}
); } + +export function getPreviewCss(): string { + return require("./ui/BarcodeGenerator.scss"); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.icon.dark.png b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.icon.dark.png index b64899dac6..77810f30f8 100644 Binary files a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.icon.dark.png and b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.icon.dark.png differ diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.icon.png b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.icon.png index 7f5366d1e9..86fcee5b4b 100644 Binary files a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.icon.png and b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.icon.png differ diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tile.dark.png b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tile.dark.png index 863f1f715d..879da05625 100644 Binary files a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tile.dark.png and b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tile.dark.png differ diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tile.png b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tile.png index 57de645b44..3a64ac89be 100644 Binary files a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tile.png and b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tile.png differ diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx index 444dc6df37..9296cd48f4 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx @@ -1,32 +1,28 @@ +import classNames from "classnames"; import { ReactElement } from "react"; import { BarcodeGeneratorContainerProps } from "../typings/BarcodeGeneratorProps"; import { barcodeConfig } from "./config/Barcode.config"; -import { BarcodeContextProvider, useBarcodeConfig } from "./config/BarcodeContext"; import { QRCodeRenderer } from "./components/QRCode"; import { BarcodeRenderer } from "./components/Barcode"; import "./ui/BarcodeGenerator.scss"; -function BarcodeContainer({ tabIndex }: { tabIndex?: number }): ReactElement { - const config = useBarcodeConfig(); - - return ( -
- {config.isQRCode ? : } -
- ); -} - export default function BarcodeGenerator(props: BarcodeGeneratorContainerProps): ReactElement { const config = barcodeConfig(props); - if (!config.value) { + if (!config.codeValue) { return No barcode value provided; } return ( - - - +
+ {config.type === "qrcode" ? : } +
); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index 284517b5da..7291f849d5 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -8,12 +8,10 @@ - + Dynamic value - String to encode in the QR code - - - + String to encode as a barcode or QR code + Barcode Format @@ -28,9 +26,33 @@ Allow download Adds a download button - + + Button text + + + Download + Downloaden + + + Button aria-label + + Download code as file + Sla code op als bestand + + + + File name + Custom filename for the downloaded file (without extension). If empty, generates automatically based on format and value. + + + Button position + Position of the download button relative to the barcode + + Top + Bottom + @@ -58,11 +80,11 @@ Flat - Enable flat barcode, skip guard bars + Enable flat barcode, skip guard bars. Note: Doesn't work with EAN addons. Last character - Character after the barcode + Character after the barcode. Note: Doesn't work when 'Flat' is enabled or with EAN addons. Mod43 @@ -79,12 +101,10 @@ EAN-2 - + Addon value Value for the addon barcode (5 digits for EAN-5, 2 digits for EAN-2) - - - + Addon spacing @@ -97,13 +117,17 @@ Display value Display the value below the code + + Show as card + Display the widget with a border, background and padding + Bar width Width of a single bar Code height - Height of the barcode + Height of the barcode. Note: In preview, the max height is 200px. The barcode will render at full height in your application. Margin size @@ -111,11 +135,11 @@ QR Size - The size of the QR box + 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. 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. Title @@ -131,39 +155,39 @@ H - - Image + + Overlay image Include an image on top the QR code - + Image source URL or path to the image to display on the QR code - + Center image Center the image in the QR code - + Image X position Horizontal position of the image - + Image Y position Vertical position of the image - + Image height Height of the image in pixels - + Image width Width of the image in pixels - + Image opacity Opacity of the image (0.0 to 1.0) - + Excavate background Remove QR code dots behind the image 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 9c04ef785e..86ee9225dd 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx @@ -1,277 +1,1031 @@ import "@testing-library/jest-dom"; -import { render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { EditableValueBuilder } from "@mendix/widget-plugin-test-utils"; // Mock JsBarcode const mockJsBarcode = jest.fn(); -const barcodeDefaultValue = `default barcode value`; jest.mock("jsbarcode", () => mockJsBarcode); // Mock the QRCodeSVG component jest.mock("qrcode.react", () => ({ - QRCodeSVG: ({ value, size }: { value: string; size: number }) => ( -
+ QRCodeSVG: ({ value, size, level, marginSize, title, imageSettings }: any) => ( +
QR Code: {value}
) })); +// Mock download functionality +jest.mock("../utils/download-code", () => ({ + downloadCode: jest.fn() +})); + import BarcodeGenerator from "../BarcodeGenerator"; import { CodeFormatEnum, CustomCodeFormatEnum } from "typings/BarcodeGeneratorProps"; +import { downloadCode } from "../utils/download-code"; -describe("BarcodeGenerator", () => { - const defaultProps = { - name: "barcodeGenerator1", - class: "mx-barcode-generator", - tabIndex: -1, - codeFormat: "QRCode" as CodeFormatEnum, - customCodeFormat: "CODE128" as CustomCodeFormatEnum, - enableEan128: false, - enableFlat: false, - lastChar: "", - enableMod43: false, - allowDownload: false, - downloadAriaLabel: "Download barcode", - displayValue: false, - codeWidth: 2, - codeHeight: 200, - codeMargin: 4, - qrSize: 128, - qrMargin: 2, - qrTitle: "", - qrLevel: "L" as any, - qrImage: false, - qrImageSrc: { status: "unavailable" } as any, - qrImageCenter: true, - qrImageX: 0, - qrImageY: 0, - qrImageHeight: 24, - qrImageWidth: 24, - qrImageOpacity: { toNumber: () => 1 } as any, - qrImageExcavate: true, - addonFormat: "None" as any, - addonValue: { status: "unavailable" } as any, - addonSpacing: 20, - codeValue: new EditableValueBuilder().withValue(barcodeDefaultValue).build() - }; +// Test utilities +const createMockWebImage = (status: "available" | "loading" | "unavailable" = "unavailable"): any => { + if (status === "available") { + return { + status: "available" as const, + value: { uri: "data:image/png;base64,test123" } + } as any; + } + return { status } as any; +}; + +const createBarcodeProps = (overrides: any = {}): any => ({ + name: "barcodeGenerator1", + class: "mx-barcode-generator", + tabIndex: -1, + codeFormat: "QRCode" as CodeFormatEnum, + customCodeFormat: "CODE128" as CustomCodeFormatEnum, + enableEan128: false, + enableFlat: false, + lastChar: "", + enableMod43: false, + allowDownload: false, + downloadButtonCaption: { status: "available" as const, value: "Download" } as any, + downloadButtonAriaLabel: { status: "available" as const, value: "Download barcode" } as any, + displayValue: false, + showAsCard: false, + codeWidth: 2, + codeHeight: 200, + codeMargin: 4, + qrSize: 128, + qrMargin: 2, + qrTitle: "", + qrLevel: "L" as any, + qrImage: false, + qrImageSrc: createMockWebImage(), + qrImageCenter: true, + qrImageX: 0, + qrImageY: 0, + qrImageHeight: 24, + qrImageWidth: 24, + qrImageOpacity: { toNumber: () => 1 } as any, + qrImageExcavate: true, + addonFormat: "None" as any, + addonValue: { status: "unavailable" as const } as any, + addonSpacing: 20, + buttonPosition: "bottom" as const, + codeValue: new EditableValueBuilder().withValue("test-barcode-value").build(), + ...overrides +}); +describe("BarcodeGenerator", () => { beforeEach(() => { jest.clearAllMocks(); }); - it("renders QR code when value is available", () => { - const props = { - ...defaultProps, - codeValue: { - value: "Hello World", - status: "available" - } as any - }; + // ============= Core Rendering Tests ============= + describe("core rendering", () => { + it("renders QR code when codeValue is available", () => { + const props = createBarcodeProps({ + codeValue: { + value: "Hello World", + status: "available" + } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-value", "Hello World"); + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-size", "128"); + }); + + it("shows fallback message when codeValue is loading", () => { + const props = createBarcodeProps({ + codeValue: { value: "", status: "loading" } as any + }); + + render(); + + expect(screen.queryByTestId("qr-code")).not.toBeInTheDocument(); + expect(screen.getByText("No barcode value provided")).toBeInTheDocument(); + }); + + it("shows fallback message when codeValue is unavailable", () => { + const props = createBarcodeProps({ + codeValue: { value: "", status: "unavailable" } as any + }); + + render(); + + expect(screen.queryByTestId("qr-code")).not.toBeInTheDocument(); + expect(screen.getByText("No barcode value provided")).toBeInTheDocument(); + }); + + it("applies correct CSS classes and tabIndex", () => { + const props = createBarcodeProps({ + class: "custom-class", + tabIndex: 2, + codeValue: { value: "test", status: "available" } as any + }); + + const { container } = render(); + const widget = container.firstChild as HTMLElement; + + expect(widget).toHaveClass("barcode-generator", "custom-class"); + expect(widget).toHaveAttribute("tabIndex", "2"); + }); + + it("applies card styling when showAsCard is true", () => { + const props = createBarcodeProps({ + showAsCard: true, + codeValue: { value: "test", status: "available" } as any + }); + + const { container } = render(); + const widget = container.firstChild as HTMLElement; + + expect(widget).toHaveClass("barcode-generator--as-card"); + }); + }); + + // ============= Barcode Format Tests ============= + describe("barcode formats", () => { + it("renders CODE128 barcode correctly", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "123456789", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "123456789", + expect.objectContaining({ format: "CODE128" }) + ); + }); + + it("renders CODE39 barcode with uppercase letters and special characters", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "CODE39" as CustomCodeFormatEnum, + codeValue: { value: "ABC-123", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "ABC-123", + expect.objectContaining({ format: "CODE39" }) + ); + }); + + it("renders CODE93 barcode", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "CODE93" as CustomCodeFormatEnum, + codeValue: { value: "CODE93VALUE", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "CODE93VALUE", + expect.objectContaining({ format: "CODE93" }) + ); + }); + + it("renders EAN-13 barcode with 13 digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN13" as CustomCodeFormatEnum, + codeValue: { value: "1234567890128", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "1234567890128", + expect.objectContaining({ format: "EAN13" }) + ); + }); + + it("renders EAN-8 barcode with 8 digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN8" as CustomCodeFormatEnum, + codeValue: { value: "12345678", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "12345678", + expect.objectContaining({ format: "EAN8" }) + ); + }); + + it("renders UPC barcode with 12 digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "UPC" as CustomCodeFormatEnum, + codeValue: { value: "123456789012", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "123456789012", + expect.objectContaining({ format: "UPC" }) + ); + }); + + it("renders ITF-14 barcode with exactly 14 digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "ITF14" as CustomCodeFormatEnum, + codeValue: { value: "12345678901234", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "12345678901234", + expect.objectContaining({ format: "ITF14" }) + ); + }); + + it("renders MSI barcode with numeric digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "MSI" as CustomCodeFormatEnum, + codeValue: { value: "123456", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "123456", + expect.objectContaining({ format: "MSI" }) + ); + }); + + it("renders Pharmacode barcode with numeric digits", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "pharmacode" as CustomCodeFormatEnum, + codeValue: { value: "1234567", status: "available" } as any + }); - render(); + render(); - expect(screen.getByTestId("qr-code")).toBeInTheDocument(); - expect(screen.getByTestId("qr-code")).toHaveAttribute("data-value", "Hello World"); - expect(screen.getByTestId("qr-code")).toHaveAttribute("data-size", "128"); + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "1234567", + expect.objectContaining({ format: "pharmacode" }) + ); + }); + + it("renders Codabar barcode", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "codabar" as CustomCodeFormatEnum, + codeValue: { value: "123-456", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "123-456", + expect.objectContaining({ format: "codabar" }) + ); + }); }); - it("shows no barcode message when data is loading", () => { - const props = { - ...defaultProps, - codeValue: { - value: "", - status: "loading" - } as any - }; + // ============= QR Code Tests ============= + describe("QR code rendering", () => { + it("renders QR code with custom size", () => { + const props = createBarcodeProps({ + qrSize: 256, + codeValue: { value: "Custom Size QR", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-size", "256"); + }); + + it("renders QR code with custom margin", () => { + const props = createBarcodeProps({ + qrMargin: 5, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-margin", "5"); + }); + + it("renders QR code with all error correction levels", () => { + const levels: any[] = ["L", "M", "Q", "H"]; + + levels.forEach(level => { + const props = createBarcodeProps({ + qrLevel: level, + codeValue: { value: "test", status: "available" } as any + }); + + const { unmount } = render(); - render(); + expect(screen.getAllByTestId("qr-code")[0]).toHaveAttribute("data-level", level); + unmount(); + }); + }); + + it("renders QR code with title", () => { + const props = createBarcodeProps({ + qrTitle: "QR Code Title", + codeValue: { value: "test", status: "available" } as any + }); - expect(screen.queryByTestId("qr-code")).not.toBeInTheDocument(); - expect(screen.getByText("No barcode value provided")).toBeInTheDocument(); + render(); + + expect(screen.getByText("QR Code Title")).toBeInTheDocument(); + }); }); - it("shows no barcode message when data is unavailable", () => { - const props = { - ...defaultProps, - codeValue: { - value: "", - status: "unavailable" - } as any - }; + // ============= QR Image Overlay Tests ============= + describe("QR image overlay functionality", () => { + it("renders QR code with image overlay when qrImage is true", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); + + it("renders QR code with centered image overlay", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + qrImageCenter: true, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); + + it("renders QR code with positioned image overlay", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + qrImageCenter: false, + qrImageX: 10, + qrImageY: 20, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); - render(); + it("renders QR code with image overlay custom dimensions", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + qrImageWidth: 50, + qrImageHeight: 50, + codeValue: { value: "test", status: "available" } as any + }); - expect(screen.queryByTestId("qr-code")).not.toBeInTheDocument(); - expect(screen.getByText("No barcode value provided")).toBeInTheDocument(); + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); + + it("renders QR code with image overlay opacity", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + qrImageOpacity: { toNumber: () => 0.75 } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); + + it("renders QR code with image excavation enabled", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("available"), + qrImageExcavate: true, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); + + it("does not render image overlay when qrImageSrc is unavailable", () => { + const props = createBarcodeProps({ + qrImage: true, + qrImageSrc: createMockWebImage("unavailable"), + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + // QR code should render but without image + expect(screen.getByTestId("qr-code")).toHaveAttribute("data-image", "false"); + }); }); - it("renders CODE128 barcode when format is not QR", () => { - const props = { - ...defaultProps, - codeFormat: "CODE128" as CodeFormatEnum, - codeValue: { - value: "123456789", - status: "available" - } as any - }; - - render(); - - // Should not render QR code - expect(screen.queryByTestId("qr-code")).not.toBeInTheDocument(); - - // Should have called JsBarcode - expect(mockJsBarcode).toHaveBeenCalledWith( - expect.any(Object), // SVG element - "123456789", - { - format: "CODE128", - width: 2, - height: 200, - margin: 4, - displayValue: false, - ean128: false, - flat: false, - lastChar: "", - mod43: false - } - ); + // ============= Download Button Tests ============= + describe("download button functionality", () => { + it("does not render download button when allowDownload is false", () => { + const props = createBarcodeProps({ + allowDownload: false, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.queryByRole("button")).not.toBeInTheDocument(); + }); + + it("renders download button with custom caption", () => { + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Export Code" } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByText("Export Code")).toBeInTheDocument(); + }); + + it("renders download button with correct aria-label for QR code", () => { + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonAriaLabel: { status: "available" as const, value: "Download QR code" } as any, + codeFormat: "QRCode" as CodeFormatEnum, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.getByRole("button")).toHaveAttribute("aria-label", "Download QR code"); + }); + + it("renders download button at top position", () => { + const props = createBarcodeProps({ + allowDownload: true, + buttonPosition: "top" as const, + downloadButtonCaption: { status: "available" as const, value: "Download" } as any, + codeValue: { value: "test", status: "available" } as any + }); + + const { container } = render(); + + const renderer = container.querySelector(".qrcode-renderer"); + expect(renderer).toBeInTheDocument(); + // Get all children + const children = Array.from((renderer as HTMLElement).children); + // Download button should be first child + const firstChild = children[0] as HTMLElement; + expect(firstChild).toHaveClass("barcode-generator-download-button"); + }); + + it("renders download button at bottom position", () => { + const props = createBarcodeProps({ + allowDownload: true, + buttonPosition: "bottom" as const, + downloadButtonCaption: { status: "available" as const, value: "Download" } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const button = screen.getByRole("button"); + expect(button).toBeInTheDocument(); + }); + + it("calls downloadCode when download button is clicked", () => { + const mockDownloadCode = downloadCode as jest.Mock; + + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Download" } as any, + codeFormat: "QRCode" as CodeFormatEnum, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const button = screen.getByRole("button"); + fireEvent.click(button); + + expect(mockDownloadCode).toHaveBeenCalled(); + }); + + it("renders download button with icon and caption", () => { + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Save" } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveTextContent("Save"); + }); }); - it("renders QR code with custom size", () => { - const props = { - ...defaultProps, - qrSize: 256, - codeValue: { - value: "Custom Size QR", - status: "available" - } as any - }; + // ============= Barcode Display Options Tests ============= + describe("barcode display options", () => { + it("passes displayValue option to JsBarcode correctly", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + displayValue: true, + codeValue: { value: "DISPLAY123", status: "available" } as any + }); - render(); + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "DISPLAY123", + expect.objectContaining({ displayValue: true }) + ); + }); + + it("does not display value when displayValue is false", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + displayValue: false, + codeValue: { value: "NODISPLAY", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "NODISPLAY", + expect.objectContaining({ displayValue: false }) + ); + }); + + it("applies custom width to barcode", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeWidth: 3, + codeValue: { value: "WIDTH_TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "WIDTH_TEST", + expect.objectContaining({ width: 3 }) + ); + }); + + it("applies custom height to barcode", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeHeight: 300, + codeValue: { value: "HEIGHT_TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "HEIGHT_TEST", + expect.objectContaining({ height: 300 }) + ); + }); - expect(screen.getByTestId("qr-code")).toHaveAttribute("data-size", "256"); + it("applies custom margin to barcode", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeMargin: 8, + codeValue: { value: "MARGIN_TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "MARGIN_TEST", + expect.objectContaining({ margin: 8 }) + ); + }); }); - it("passes displayValue option to JSBarcode for non-QR codes", () => { - const props = { - ...defaultProps, - codeFormat: "CODE128" as const, - displayValue: true, - codeValue: { - value: "DISPLAY123", - status: "available" - } as any - }; - - render(); - - expect(mockJsBarcode).toHaveBeenCalledWith( - expect.any(Object), - "DISPLAY123", - expect.objectContaining({ - displayValue: true - }) - ); + // ============= Advanced Barcode Options Tests ============= + describe("advanced barcode options", () => { + it("applies EAN-128 encoding when enabled", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + enableEan128: true, + codeValue: { value: "EAN128TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "EAN128TEST", + expect.objectContaining({ ean128: true }) + ); + }); + + it("applies flat mode when enabled", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + enableFlat: true, + codeValue: { value: "FLATTEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "FLATTEST", + expect.objectContaining({ flat: true }) + ); + }); + + it("applies MOD43 checksum when enabled", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + enableMod43: true, + codeValue: { value: "MOD43TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "MOD43TEST", + expect.objectContaining({ mod43: true }) + ); + }); + + it("applies custom last character", () => { + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + lastChar: "X", + codeValue: { value: "LASTCHARTEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "LASTCHARTEST", + expect.objectContaining({ lastChar: "X" }) + ); + }); }); - it("handles JSBarcode errors gracefully", () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); - mockJsBarcode.mockImplementation(() => { - throw new Error("Invalid barcode format"); + // ============= EAN Addon Tests ============= + describe("EAN addon functionality", () => { + it("supports EAN-5 addon format", () => { + const mockBarcodeInstance = { + EAN13: jest.fn().mockReturnThis(), + blank: jest.fn().mockReturnThis(), + EAN5: jest.fn().mockReturnThis(), + render: jest.fn() + }; + + mockJsBarcode.mockReturnValue(mockBarcodeInstance); + + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN13" as CustomCodeFormatEnum, + addonValue: { value: "12345", status: "available" } as any, + addonFormat: "EAN5" as any, + addonSpacing: 25, + codeValue: { value: "1234567890128", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalled(); + expect(mockBarcodeInstance.EAN13).toHaveBeenCalledWith("1234567890128", expect.any(Object)); + expect(mockBarcodeInstance.blank).toHaveBeenCalledWith(25); + expect(mockBarcodeInstance.EAN5).toHaveBeenCalledWith("12345", expect.any(Object)); + expect(mockBarcodeInstance.render).toHaveBeenCalled(); + }); + + it("supports EAN-2 addon format", () => { + const mockBarcodeInstance = { + EAN13: jest.fn().mockReturnThis(), + blank: jest.fn().mockReturnThis(), + EAN2: jest.fn().mockReturnThis(), + render: jest.fn() + }; + + mockJsBarcode.mockReturnValue(mockBarcodeInstance); + + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN13" as CustomCodeFormatEnum, + addonValue: { value: "12", status: "available" } as any, + addonFormat: "EAN2" as any, + codeValue: { value: "1234567890128", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalled(); + expect(mockBarcodeInstance.EAN13).toHaveBeenCalled(); + expect(mockBarcodeInstance.EAN2).toHaveBeenCalledWith("12", expect.any(Object)); + }); + + it("does not apply addon when addonFormat is None", () => { + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN13" as CustomCodeFormatEnum, + addonValue: { value: "12345", status: "available" } as any, + addonFormat: "None" as any, + codeValue: { value: "1234567890128", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith(expect.any(Object), "1234567890128", expect.any(Object)); }); - const props = { - ...defaultProps, - codeFormat: "CODE128" as const, - codeValue: { - value: "INVALID", - status: "available" - } as any - }; + it("applies custom addon spacing", () => { + const mockBarcodeInstance = { + EAN13: jest.fn().mockReturnThis(), + blank: jest.fn().mockReturnThis(), + EAN5: jest.fn().mockReturnThis(), + render: jest.fn() + }; + + mockJsBarcode.mockReturnValue(mockBarcodeInstance); - render(); + const props = createBarcodeProps({ + codeFormat: "Custom" as CodeFormatEnum, + customCodeFormat: "EAN13" as CustomCodeFormatEnum, + addonValue: { value: "12345", status: "available" } as any, + addonFormat: "EAN5" as any, + addonSpacing: 40, + codeValue: { value: "1234567890128", status: "available" } as any + }); - expect(consoleSpy).toHaveBeenCalledWith("Error generating barcode:", expect.any(Error)); - consoleSpy.mockRestore(); + render(); + + expect(mockBarcodeInstance.blank).toHaveBeenCalledWith(40); + }); }); - it("applies correct CSS class and tabIndex", () => { - const props = { - ...defaultProps, - class: "mx-barcode-generator custom-class", - tabIndex: 5, - codeValue: { - value: "CSS Test", - status: "available" - } as any - }; - - const { container } = render(); - - const widget = container.firstChild as HTMLElement; - expect(widget).toHaveClass("barcode-generator"); - expect(widget).toHaveAttribute("tabIndex", "5"); + // ============= Error Handling Tests ============= + describe("error handling", () => { + it("renders error message when JsBarcode throws", () => { + mockJsBarcode.mockImplementation(() => { + throw new Error("Invalid barcode value"); + }); + + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "INVALID", status: "available" } as any + }); + + render(); + + expect(screen.getByText(/Unable to generate barcode/)).toBeInTheDocument(); + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); + + it("renders alert role for error message", () => { + mockJsBarcode.mockImplementation(() => { + throw new Error("Format error"); + }); + + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "TEST", status: "available" } as any + }); + + render(); + + const alert = screen.getByRole("alert"); + expect(alert).toBeInTheDocument(); + expect(alert).toHaveClass("alert-danger"); + }); + + it("clears error when valid barcode value is provided after error", () => { + mockJsBarcode.mockImplementation(() => { + throw new Error("Initial error"); + }); + + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "BAD", status: "available" } as any + }); + + const { unmount } = render(); + + expect(screen.getByText(/Unable to generate barcode/)).toBeInTheDocument(); + + // Clean up first render to avoid duplicate DOM + unmount(); + + // Mock now succeeds + mockJsBarcode.mockReset(); + + const goodProps = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "GOOD", status: "available" } as any + }); + + render(); + + expect(screen.queryByText(/Unable to generate barcode/)).not.toBeInTheDocument(); + }); }); - it("uses fallback values when props are missing", () => { - const props = { - ...defaultProps, - codeFormat: "CODE128" as const, - codeValue: { - value: "DEFAULT_TEST", - status: "available" - } as any - }; - - // Component uses nullish coalescing to provide defaults - render(); - - expect(mockJsBarcode).toHaveBeenCalledWith(expect.any(Object), "DEFAULT_TEST", { - format: "CODE128", - width: 2, // from defaultProps - height: 200, // from defaultProps - margin: 4, // from defaultProps - displayValue: false, - ean128: false, - flat: false, - lastChar: "", - mod43: false + // ============= Accessibility Tests ============= + describe("accessibility", () => { + it("renders QR code title as semantic element when provided", () => { + const props = createBarcodeProps({ + qrTitle: "Invoice QR Code", + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const title = screen.getByText("Invoice QR Code"); + expect(title).toBeInTheDocument(); + expect(title.tagName).toBe("H3"); + }); + + it("does not render title when qrTitle is empty", () => { + const props = createBarcodeProps({ + qrTitle: "", + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + expect(screen.queryByRole("heading")).not.toBeInTheDocument(); + }); + + it("download button has proper semantics", () => { + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Download Barcode" } as any, + downloadButtonAriaLabel: { + status: "available" as const, + value: "Download current barcode as PNG" + } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const button = screen.getByRole("button"); + expect(button).toHaveAttribute("aria-label", "Download current barcode as PNG"); + expect(button).toHaveTextContent("Download Barcode"); + }); + + it("download button is keyboard accessible", async () => { + const mockDownloadCode = downloadCode as jest.Mock; + + const props = createBarcodeProps({ + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Download" } as any, + codeValue: { value: "test", status: "available" } as any + }); + + render(); + + const button = screen.getByRole("button"); + const user = userEvent.setup(); + + await user.click(button); + + expect(mockDownloadCode).toHaveBeenCalled(); + }); + + it("error messages have alert role for screen readers", () => { + mockJsBarcode.mockImplementation(() => { + throw new Error("Test error"); + }); + + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + codeValue: { value: "TEST", status: "available" } as any + }); + + render(); + + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); + + it("barcode widget container is focusable when tabIndex is set", () => { + const props = createBarcodeProps({ + tabIndex: 0, + codeValue: { value: "test", status: "available" } as any + }); + + const { container } = render(); + const widget = container.firstChild as HTMLElement; + + expect(widget).toHaveAttribute("tabIndex", "0"); }); }); - it("supports EAN addon functionality", () => { - const mockBarcodeInstance = { - EAN13: jest.fn().mockReturnThis(), - blank: jest.fn().mockReturnThis(), - EAN5: jest.fn().mockReturnThis(), - render: jest.fn() - }; - - mockJsBarcode.mockReturnValue(mockBarcodeInstance); - - const props = { - ...defaultProps, - codeFormat: "Custom" as CodeFormatEnum, - customCodeFormat: "EAN13" as any, - addonValue: { - value: "12345", - status: "available" - } as any, - addonFormat: "EAN5" as any, - addonSpacing: 25, - codeValue: { - value: "1234567890128", - status: "available" - } as any - }; - - render(); - - expect(mockJsBarcode).toHaveBeenCalled(); - expect(mockBarcodeInstance.EAN13).toHaveBeenCalledWith("1234567890128", expect.any(Object)); - expect(mockBarcodeInstance.blank).toHaveBeenCalledWith(25); - expect(mockBarcodeInstance.EAN5).toHaveBeenCalledWith("12345", expect.any(Object)); - expect(mockBarcodeInstance.render).toHaveBeenCalled(); + // ============= Integration Tests ============= + describe("integration scenarios", () => { + it("renders QR code with download, title, and image overlay", () => { + const props = createBarcodeProps({ + allowDownload: true, + qrTitle: "Secure QR", + qrImage: true, + qrImageSrc: createMockWebImage("available"), + downloadButtonCaption: { status: "available" as const, value: "Save QR" } as any, + codeValue: { value: "secure-data", status: "available" } as any + }); + + render(); + + expect(screen.getByText("Secure QR")).toBeInTheDocument(); + expect(screen.getByText("Save QR")).toBeInTheDocument(); + expect(screen.getByTestId("qr-code")).toBeInTheDocument(); + }); + + it("renders barcode with all advanced options enabled", () => { + const mockBarcodeInstance = { + render: jest.fn() + }; + mockJsBarcode.mockReturnValue(mockBarcodeInstance); + + const props = createBarcodeProps({ + codeFormat: "CODE128" as CodeFormatEnum, + displayValue: true, + showAsCard: true, + enableEan128: true, + enableFlat: true, + enableMod43: true, + allowDownload: true, + downloadButtonCaption: { status: "available" as const, value: "Export" } as any, + codeWidth: 3, + codeHeight: 250, + codeMargin: 5, + lastChar: "Z", + codeValue: { value: "FULL_TEST", status: "available" } as any + }); + + render(); + + expect(mockJsBarcode).toHaveBeenCalledWith( + expect.any(Object), + "FULL_TEST", + expect.objectContaining({ + displayValue: true, + ean128: true, + flat: true, + mod43: true, + width: 3, + height: 250, + margin: 5, + lastChar: "Z" + }) + ); + expect(screen.getByText("Export")).toBeInTheDocument(); + }); }); }); diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodePreview.assets.ts b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodePreview.assets.ts new file mode 100644 index 0000000000..e62f30435a --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodePreview.assets.ts @@ -0,0 +1,80 @@ +// Import all barcode SVG files +import code128Svg from "./barcodes/code128.svg"; +import ean13Svg from "./barcodes/ean13.svg"; +import ean13Ean2Svg from "./barcodes/ean13-ean2.svg"; +import ean13Ean5Svg from "./barcodes/ean13-ean5.svg"; +import ean13FlatSvg from "./barcodes/ean13-flat.svg"; +import ean8Svg from "./barcodes/ean8.svg"; +import ean8Ean2Svg from "./barcodes/ean8-ean2.svg"; +import ean8Ean5Svg from "./barcodes/ean8-ean5.svg"; +import ean8FlatSvg from "./barcodes/ean8-flat.svg"; +import upcSvg from "./barcodes/upc.svg"; +import upcEan2Svg from "./barcodes/upc-ean2.svg"; +import upcEan5Svg from "./barcodes/upc-ean5.svg"; +import code39Svg from "./barcodes/code39.svg"; +import itf14Svg from "./barcodes/itf14.svg"; +import msiSvg from "./barcodes/msi.svg"; +import pharmacodeSvg from "./barcodes/pharmacode.svg"; +import codabarSvg from "./barcodes/codabar.svg"; +import code93Svg from "./barcodes/code93.svg"; + +type BarcodeImageVariants = { + default: string; + flat?: string; + EAN2?: string; + EAN5?: string; +}; + +const barcodeImageMap: Record = { + CODE128: { default: code128Svg }, + EAN13: { + default: ean13Svg, + EAN2: ean13Ean2Svg, + EAN5: ean13Ean5Svg, + flat: ean13FlatSvg + }, + EAN8: { + default: ean8Svg, + EAN2: ean8Ean2Svg, + EAN5: ean8Ean5Svg, + flat: ean8FlatSvg + }, + UPC: { + default: upcSvg, + EAN2: upcEan2Svg, + EAN5: upcEan5Svg + }, + CODE39: { default: code39Svg }, + ITF14: { default: itf14Svg }, + MSI: { default: msiSvg }, + pharmacode: { default: pharmacodeSvg }, + codabar: { default: codabarSvg }, + CODE93: { default: code93Svg } +}; + +export function getBarcodeImageUrl( + codeFormat: string, + customCodeFormat: string, + addonFormat: string, + enableFlat: boolean +): string | null { + const format = codeFormat === "Custom" ? customCodeFormat : codeFormat; + const formatMap = barcodeImageMap[format]; + + if (!formatMap) return null; + + if (enableFlat && (format === "EAN13" || format === "EAN8")) { + return formatMap.flat || formatMap.default; + } + + if (addonFormat && addonFormat !== "None" && (format === "EAN13" || format === "EAN8" || format === "UPC")) { + if (addonFormat === "EAN2") { + return formatMap.EAN2 || formatMap.default; + } + if (addonFormat === "EAN5") { + return formatMap.EAN5 || formatMap.default; + } + } + + return formatMap.default || null; +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/codabar.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/codabar.svg new file mode 100644 index 0000000000..558073304b --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/codabar.svg @@ -0,0 +1 @@ +1234 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code128.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code128.svg new file mode 100644 index 0000000000..54399b37d1 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code128.svg @@ -0,0 +1 @@ +5901234123457 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code39.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code39.svg new file mode 100644 index 0000000000..1f8ed586ca --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code39.svg @@ -0,0 +1 @@ +HELLO-WORLD \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code93.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code93.svg new file mode 100644 index 0000000000..5ac6720dc9 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/code93.svg @@ -0,0 +1 @@ +CODE93EXAMPLE \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean2.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean2.svg new file mode 100644 index 0000000000..f722a5c843 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean2.svg @@ -0,0 +1 @@ +590123412345742 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean5.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean5.svg new file mode 100644 index 0000000000..c039ce9106 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-ean5.svg @@ -0,0 +1 @@ +590123412345751234 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-flat.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-flat.svg new file mode 100644 index 0000000000..4c753de60c --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13-flat.svg @@ -0,0 +1 @@ +5901234123457 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13.svg new file mode 100644 index 0000000000..54399b37d1 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean13.svg @@ -0,0 +1 @@ +5901234123457 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean2.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean2.svg new file mode 100644 index 0000000000..928a6435e7 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean2.svg @@ -0,0 +1 @@ +9638507442 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean5.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean5.svg new file mode 100644 index 0000000000..216984f0cf --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-ean5.svg @@ -0,0 +1 @@ +9638507451234 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-flat.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-flat.svg new file mode 100644 index 0000000000..1f031583a0 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8-flat.svg @@ -0,0 +1 @@ +96385074 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8.svg new file mode 100644 index 0000000000..a91fd9f834 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/ean8.svg @@ -0,0 +1 @@ +96385074 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/itf14.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/itf14.svg new file mode 100644 index 0000000000..3d328376ae --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/itf14.svg @@ -0,0 +1 @@ +04006381333931 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/msi.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/msi.svg new file mode 100644 index 0000000000..f79b391a1a --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/msi.svg @@ -0,0 +1 @@ +1234567890 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/pharmacode.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/pharmacode.svg new file mode 100644 index 0000000000..f8ee5a5acb --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/pharmacode.svg @@ -0,0 +1 @@ +123456 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean2.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean2.svg new file mode 100644 index 0000000000..64e2fcc764 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean2.svg @@ -0,0 +1 @@ +12345678901242 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean5.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean5.svg new file mode 100644 index 0000000000..9c243f4d2e --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc-ean5.svg @@ -0,0 +1 @@ +12345678901251234 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc.svg new file mode 100644 index 0000000000..3e081cb948 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/barcodes/upc.svg @@ -0,0 +1 @@ +123456789012 \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/assets/structurePreview.svg b/packages/pluggableWidgets/barcode-generator-web/src/assets/structurePreview.svg new file mode 100644 index 0000000000..7e8f05641f --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/assets/structurePreview.svg @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx index ed7b714f25..a9bb5ff9da 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx @@ -1,22 +1,43 @@ import { useRenderBarcode } from "../hooks/useRenderBarcode"; -import { useDownloadBarcode } from "../hooks/useDownloadBarcode"; -import { useBarcodeConfig } from "../config/BarcodeContext"; +import { downloadCode } from "../utils/download-code"; +import { BarcodeTypeConfig } from "../config/Barcode.config"; +import { DownloadButton } from "./DownloadButton"; -import { Fragment } from "react"; +import { ReactElement } from "react"; -export const BarcodeRenderer = () => { - const ref = useRenderBarcode(); - const { allowDownload, downloadAriaLabel } = useBarcodeConfig(); - const { downloadBarcode } = useDownloadBarcode({ ref }); +interface BarcodeRendererProps { + config: BarcodeTypeConfig; +} + +export function BarcodeRenderer({ config }: BarcodeRendererProps): ReactElement { + const { ref, error } = useRenderBarcode(config); + const { downloadButton } = config; + const buttonPosition = downloadButton?.buttonPosition ?? "bottom"; + + if (error) { + return ( +
+
+ Unable to generate barcode. Please check the barcode value and format + configuration. +
+
+ ); + } + + const button = downloadButton && ( + downloadCode(ref, config.type, downloadButton.fileName)} + ariaLabel={downloadButton.label} + caption={downloadButton.caption} + /> + ); return ( - +
+ {buttonPosition === "top" && button} - {allowDownload && ( - - )} - + {buttonPosition === "bottom" && button} +
); -}; +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/DownloadButton.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/DownloadButton.tsx new file mode 100644 index 0000000000..fdfd79a018 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/DownloadButton.tsx @@ -0,0 +1,16 @@ +import { ReactElement } from "react"; +import { DownloadIcon } from "./icons/DownloadIcon"; + +interface DownloadButtonProps { + onClick: () => void; + ariaLabel?: string; + caption?: string; +} + +export function DownloadButton({ onClick, ariaLabel, caption }: DownloadButtonProps): ReactElement { + return ( + + ); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx index 879e0f8462..55999d731f 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -1,61 +1,41 @@ import { QRCodeSVG } from "qrcode.react"; -import { Fragment, useRef } from "react"; -import { useDownloadQrCode } from "../hooks/useDownloadQRCode"; -import { useBarcodeConfig } from "../config/BarcodeContext"; +import { ReactElement, useRef } from "react"; +import { downloadCode } from "../utils/download-code"; +import { DownloadButton } from "./DownloadButton"; +import { QRCodeTypeConfig } from "../config/Barcode.config"; -export const QRCodeRenderer = () => { +interface QRCodeRendererProps { + config: QRCodeTypeConfig; +} + +export function QRCodeRenderer({ config }: QRCodeRendererProps): ReactElement { const ref = useRef(null); - const { downloadQrCode } = useDownloadQrCode({ ref }); - const { - value, - allowDownload, - qrSize: size, - qrMargin: margin, - qrTitle: title, - qrLevel: level, - qrImageSrc: imageSrc, - qrImageX: imageX, - qrImageY: imageY, - qrImageHeight: imageHeight, - qrImageWidth: imageWidth, - qrImageOpacity: imageOpacity, - qrImageExcavate: imageExcavate, - downloadAriaLabel: downloadAriaLabel - } = useBarcodeConfig(); - const imageSettings = imageSrc - ? { - src: imageSrc, - x: imageX, - y: imageY, - height: imageHeight, - width: imageWidth, - opacity: imageOpacity, - excavate: imageExcavate - } - : undefined; + const { codeValue, downloadButton, size, margin, title, level, image } = config; + const buttonPosition = downloadButton?.buttonPosition ?? "bottom"; + + const button = downloadButton && ( + downloadCode(ref, config.type, downloadButton.fileName)} + ariaLabel={downloadButton.label} + caption={downloadButton.caption} + /> + ); return ( - +
+ {title &&

{title}

} + {buttonPosition === "top" && button} - {allowDownload && ( - - )} - + {buttonPosition === "bottom" && button} +
); -}; +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/icons/DownloadIcon.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/icons/DownloadIcon.tsx new file mode 100644 index 0000000000..f2acb48537 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/icons/DownloadIcon.tsx @@ -0,0 +1,21 @@ +import { ReactElement } from "react"; + +export function DownloadIcon(): ReactElement { + return ( + <> + + + ); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx new file mode 100644 index 0000000000..354900bc7c --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/BarcodePreview.tsx @@ -0,0 +1,40 @@ +import { ReactElement } from "react"; +import { BarcodeGeneratorPreviewProps } from "../../../typings/BarcodeGeneratorProps"; +import { useBarcodePreviewSvg } from "../../hooks/useBarcodePreviewSvg"; + +interface BarcodePreviewProps extends BarcodeGeneratorPreviewProps { + downloadButton: ReactElement | null; +} + +export function BarcodePreview(props: BarcodePreviewProps): ReactElement { + const { downloadButton, ...restProps } = props; + const codeHeight = restProps.codeHeight ?? 200; + const displayHeight = Math.min(codeHeight, 200); // Clamped to 200px for preview + + const { imageUrl, displayUrl } = useBarcodePreviewSvg({ + codeFormat: restProps.codeFormat, + customCodeFormat: restProps.customCodeFormat, + addonFormat: restProps.addonFormat, + enableFlat: restProps.enableFlat === true, + displayValue: restProps.displayValue + }); + + return ( +
+ {restProps.buttonPosition === "top" && downloadButton} + {imageUrl ? ( + Barcode preview + ) : ( +
+ Barcode format not supported +
+ )} + {restProps.buttonPosition === "bottom" && downloadButton} +
+ ); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx new file mode 100644 index 0000000000..4a2720e41c --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/preview/QRCodePreview.tsx @@ -0,0 +1,69 @@ +import { type CSSProperties, ReactElement, useState } from "react"; +import { BarcodeGeneratorPreviewProps } from "../../../typings/BarcodeGeneratorProps"; +import BarcodePreviewSVG from "../../assets/BarcodeGeneratorPreview.svg"; +import { resolveQRImageSrc } from "../../utils/qrcode-preview-utils"; + +interface QRCodePreviewProps extends BarcodeGeneratorPreviewProps { + downloadButton: ReactElement | null; +} + +export function QRCodePreview(props: QRCodePreviewProps): ReactElement { + const { downloadButton, ...restProps } = props; + const doc = decodeURI(BarcodePreviewSVG); + const qrSize = restProps.qrSize ?? 128; + // Note: qrMargin is in module units (QR grid cells), not pixels + // The QRCodeSVG component handles margin internally within the specified size + const displaySize = Math.min(qrSize, 200); // Clamped to 200px for preview + const qrOverlayWidth = restProps.qrOverlayWidth ?? 32; + const qrOverlayHeight = restProps.qrOverlayHeight ?? 32; + const qrOverlayOpacity = restProps.qrOverlayOpacity ?? 1; + const qrOverlayX = restProps.qrOverlayX ?? 0; + const qrOverlayY = restProps.qrOverlayY ?? 0; + + const [imageSrcError, setImageSrcError] = useState(false); + + const imageBaseStyle: CSSProperties = restProps.qrOverlayCenter + ? { + left: "50%", + top: "50%", + transform: "translate(-50%, -50%)", + width: qrOverlayWidth, + height: qrOverlayHeight + } + : { + left: qrOverlayX, + top: qrOverlayY, + width: qrOverlayWidth, + height: qrOverlayHeight + }; + + return ( +
+ {restProps.qrTitle &&

{restProps.qrTitle}

} + {restProps.buttonPosition === "top" && downloadButton} + + {restProps.qrOverlay && ( + setImageSrcError(true)} + style={{ + ...imageBaseStyle, + opacity: qrOverlayOpacity, + ...(restProps.qrOverlayExcavate && { + backgroundColor: "#ffffff", + outline: "3px solid #ffffff" + }) + }} + /> + )} + {restProps.buttonPosition === "bottom" && downloadButton} +
+ ); +} 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 bc93f8e0b7..1d4d15fb03 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts @@ -1,17 +1,22 @@ -import { BarcodeGeneratorContainerProps } from "../../typings/BarcodeGeneratorProps"; +import { BarcodeGeneratorContainerProps, QrLevelEnum } from "../../typings/BarcodeGeneratorProps"; -/** Configuration for static values that don't change at runtime. */ -export interface BarcodeConfig { - // Basic barcode properties - value: string; +interface DownloadButtonConfig { + caption?: string; + label?: string; + fileName: string; + buttonPosition: "top" | "bottom"; +} + +/** Configuration for barcode (non-QR) rendering */ +export interface BarcodeTypeConfig { + type: "barcode"; + codeValue: string; width: number; height: number; format: string; - isQRCode: boolean; margin: number; displayValue: boolean; - allowDownload: boolean; - downloadAriaLabel?: string; + downloadButton?: DownloadButtonConfig; // Advanced barcode options enableEan128: boolean; @@ -21,38 +26,76 @@ export interface BarcodeConfig { addonValue: string; addonFormat: string; addonSpacing: number; +} - // QR Code properties - qrSize: number; - qrMargin: number; - qrTitle: string; - qrLevel: string; - qrImageSrc: string; - qrImageX: number | undefined; - qrImageY: number | undefined; - qrImageHeight: number; - qrImageWidth: number; - qrImageOpacity: number; - qrImageExcavate: boolean; +/** Configuration for QR code rendering */ +export interface QRCodeTypeConfig { + type: "qrcode"; + codeValue: string; + size: number; + margin: number; + title: string; + level: QrLevelEnum; + downloadButton?: DownloadButtonConfig; + image?: { + src: string; + x: number | undefined; + y: number | undefined; + height: number; + width: number; + opacity: number; + excavate: boolean; + }; } +export type BarcodeConfig = BarcodeTypeConfig | QRCodeTypeConfig; + export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeConfig { - const value = props.codeValue?.status === "available" ? (props.codeValue.value ?? "") : ""; - const format = - props.codeFormat === "Custom" ? (props.customCodeFormat ?? "CODE128") : (props.codeFormat ?? "CODE128"); - const isQRCode = format === "QRCode"; + const codeValue = props.codeValue?.value ?? ""; + const format = props.codeFormat === "Custom" ? props.customCodeFormat : props.codeFormat; + + const downloadButtonConfig = props.allowDownload + ? { + caption: props.downloadButtonCaption?.value, + label: props.downloadButtonAriaLabel?.value, + fileName: generateFileName(props.downloadFileName?.value, format, codeValue), + buttonPosition: props.buttonPosition ?? "bottom" + } + : undefined; + + if (format === "QRCode") { + return { + type: "qrcode", + codeValue, + size: props.qrSize ?? 128, + margin: props.qrMargin ?? 2, + title: props.qrTitle ?? "", + level: props.qrLevel ?? "L", + downloadButton: downloadButtonConfig, + image: + props.qrOverlaySrc?.status === "available" + ? { + src: props.qrOverlaySrc.value.uri, + x: props.qrOverlayX === 0 ? undefined : props.qrOverlayX, + y: props.qrOverlayY === 0 ? undefined : props.qrOverlayY, + height: props.qrOverlayHeight ?? 24, + width: props.qrOverlayWidth ?? 24, + opacity: props.qrOverlayOpacity?.toNumber() ?? 1, + excavate: props.qrOverlayExcavate ?? true + } + : undefined + }; + } - return Object.freeze({ - // Basic barcode properties - value, + return { + type: "barcode", + codeValue, width: props.codeWidth ?? 128, height: props.codeHeight ?? 128, format, - isQRCode, margin: props.codeMargin ?? 2, displayValue: props.displayValue ?? false, - allowDownload: props.allowDownload ?? false, - downloadAriaLabel: props.downloadAriaLabel, + downloadButton: downloadButtonConfig, // Advanced barcode options enableEan128: props.enableEan128 ?? false, @@ -61,20 +104,38 @@ export function barcodeConfig(props: BarcodeGeneratorContainerProps): BarcodeCon enableMod43: props.enableMod43 ?? false, addonValue: props.addonValue?.status === "available" ? (props.addonValue.value ?? "") : "", addonFormat: props.addonFormat, - addonSpacing: props.addonSpacing ?? 20, + addonSpacing: props.addonSpacing ?? 20 + }; +} + +function generateFileName(customFileName: string | undefined, format: string, codeValue: string): string { + // Use custom filename if provided + if (customFileName && customFileName.trim()) { + return customFileName.trim().endsWith(".png") ? customFileName.trim() : `${customFileName.trim()}.png`; + } + + // Auto-generate filename with format and hash + const hash = hashCode(codeValue); + if (format === "QRCode") { + return `qrcode_${hash}.png`; + } + return `barcode_${format}_${hash}.png`; +} + +function hashCode(s: string): string { + if (!s) { + return "empty"; + } + + let hash = 0; + for (let i = 0; i < s.length; i++) { + const char = s.charCodeAt(i); + // eslint-disable-next-line no-bitwise + hash = (hash << 5) - hash + char; + // eslint-disable-next-line no-bitwise + hash = hash & hash; // Convert to 32-bit integer + } - // QR Code properties - qrSize: props.qrSize ?? 128, - qrMargin: props.qrMargin ?? 2, - qrTitle: props.qrTitle ?? "", - qrLevel: props.qrLevel ?? "L", - qrImageSrc: - props.qrImageSrc?.status === "available" && props.qrImageSrc.value ? props.qrImageSrc.value.uri : "", - qrImageX: props.qrImageX === 0 ? undefined : props.qrImageX, - qrImageY: props.qrImageY === 0 ? undefined : props.qrImageY, - qrImageHeight: props.qrImageHeight ?? 24, - qrImageWidth: props.qrImageWidth ?? 24, - qrImageOpacity: props.qrImageOpacity?.toNumber() ?? 1, - qrImageExcavate: props.qrImageExcavate ?? true - }); + // Convert to base36 and take first 10 characters + return Math.abs(hash).toString(36).substring(0, 10); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/BarcodeContext.tsx b/packages/pluggableWidgets/barcode-generator-web/src/config/BarcodeContext.tsx deleted file mode 100644 index a84fe138b4..0000000000 --- a/packages/pluggableWidgets/barcode-generator-web/src/config/BarcodeContext.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { createContext, ReactNode, useContext } from "react"; -import { BarcodeConfig } from "./Barcode.config"; - -const BarcodeContext = createContext(null); - -interface BarcodeContextProviderProps { - config: BarcodeConfig; - children: ReactNode; -} - -export function BarcodeContextProvider({ config, children }: BarcodeContextProviderProps): ReactNode { - return {children}; -} - -export function useBarcodeConfig(): BarcodeConfig { - const config = useContext(BarcodeContext); - if (!config) { - throw new Error("useBarcodeConfig must be used within a BarcodeConfigProvider"); - } - return config; -} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useBarcodePreviewSvg.ts b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useBarcodePreviewSvg.ts new file mode 100644 index 0000000000..fcee148482 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useBarcodePreviewSvg.ts @@ -0,0 +1,93 @@ +import { useEffect, useMemo, useState } from "react"; +import { getBarcodeImageUrl } from "../assets/barcodePreview.assets"; + +type UseBarcodePreviewSvgOptions = { + codeFormat: string; + customCodeFormat: string; + addonFormat: string; + enableFlat: boolean; + displayValue?: boolean; +}; + +type UseBarcodePreviewSvgResult = { + imageUrl: string | null; + displayUrl: string | null; +}; + +export function useBarcodePreviewSvg(options: UseBarcodePreviewSvgOptions): UseBarcodePreviewSvgResult { + const imageUrl = useMemo( + () => getBarcodeImageUrl(options.codeFormat, options.customCodeFormat, options.addonFormat, options.enableFlat), + [options.codeFormat, options.customCodeFormat, options.addonFormat, options.enableFlat] + ); + + const [modifiedSvgUrl, setModifiedSvgUrl] = useState(null); + + useEffect(() => { + let active = true; + + if (!imageUrl) { + setModifiedSvgUrl(null); + return () => { + active = false; + }; + } + + if (options.displayValue === true) { + setModifiedSvgUrl(null); + return () => { + active = false; + }; + } + + fetch(imageUrl) + .then(response => response.text()) + .then(svgText => { + if (!active) return; + const modifiedSvg = conditionallyModifySVG(svgText, false); + setModifiedSvgUrl(svgToDataUri(modifiedSvg)); + }) + .catch(() => { + if (active) { + setModifiedSvgUrl(null); + } + }); + + return () => { + active = false; + }; + }, [imageUrl, options.displayValue]); + + return { + imageUrl, + displayUrl: modifiedSvgUrl ?? imageUrl + }; +} + +function conditionallyModifySVG(svgString: string, showText: boolean): string { + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(svgString, "image/svg+xml"); + + if (doc.getElementsByTagName("parsererror").length > 0) { + return svgString; + } + + const textElements = doc.querySelectorAll("text"); + textElements.forEach(text => { + text.style.display = showText ? "block" : "none"; + }); + + return new XMLSerializer().serializeToString(doc); + } catch { + return svgString; + } +} + +function svgToDataUri(svgString: string): string { + try { + const encodedSvg = encodeURIComponent(svgString); + return `data:image/svg+xml;charset=UTF-8,${encodedSvg}`; + } catch { + return ""; + } +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadBarcode.ts b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadBarcode.ts deleted file mode 100644 index aeeca7e261..0000000000 --- a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadBarcode.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { RefObject, useCallback } from "react"; -import { downloadBlob, FILENAMES, prepareSvgForDownload } from "../utils/download-utils"; - -interface UseDownloadBarcodeParams { - ref: RefObject; -} -interface UseDownloadBarcodeReturn { - downloadBarcode: () => Promise; -} - -export function useDownloadBarcode({ ref }: UseDownloadBarcodeParams): UseDownloadBarcodeReturn { - const downloadBarcode = useCallback(async () => { - const svgElement = ref.current; - if (!svgElement) { - console.error("SVG element not found for download"); - return; - } - - try { - const clonedSvg = prepareSvgForDownload(svgElement); - const serializer = new XMLSerializer(); - const svgString = serializer.serializeToString(clonedSvg); - - // Create download blob and trigger download - const blobOptions = { - type: "image/svg+xml;charset=utf-8", - lastModified: Date.now() - }; - const blob = new Blob([svgString], blobOptions); - const filename = FILENAMES.Barcode; - downloadBlob(blob, filename); - } catch (error) { - console.error("Error downloading barcode:", error); - } - }, [ref]); - - return { downloadBarcode }; -} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadQRCode.ts b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadQRCode.ts deleted file mode 100644 index 47be31d8c4..0000000000 --- a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadQRCode.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { RefObject, useCallback } from "react"; -import { downloadBlob, FILENAMES, prepareSvgForDownload, processQRImages } from "../utils/download-utils"; - -interface UseDownloadParams { - ref: RefObject; -} -interface UseDownloadReturn { - downloadQrCode: () => Promise; -} - -export function useDownloadQrCode({ ref }: UseDownloadParams): UseDownloadReturn { - const downloadQrCode = useCallback(async () => { - const svgElement = ref.current; - if (!svgElement) { - console.error("SVG element not found for download"); - return; - } - - try { - const clonedSvg = prepareSvgForDownload(svgElement); - - // Process overlay images for QR codes - await processQRImages(clonedSvg); - - const serializer = new XMLSerializer(); - const svgString = serializer.serializeToString(clonedSvg); - - // Create download blob and trigger download - const blobOptions = { - type: "image/svg+xml;charset=utf-8", - lastModified: Date.now() - }; - const blob = new Blob([svgString], blobOptions); - const filename = FILENAMES.QRCode; - downloadBlob(blob, filename); - } catch (error) { - console.error("Error downloading SVG:", error); - } - }, [ref]); - - return { downloadQrCode }; -} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts index 5ea15389a2..cefb142de3 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts @@ -1,12 +1,16 @@ -import { useBarcodeConfig } from "../config/BarcodeContext"; -import { RefObject, useEffect, useRef } from "react"; +import { BarcodeTypeConfig } from "../config/Barcode.config"; +import { RefObject, useEffect, useRef, useState } from "react"; import { type BarcodeRenderOptions, renderBarcode } from "../utils/barcodeRenderer-utils"; +import { validateAddonValue, validateBarcodeValue } from "../config/validation"; -export const useRenderBarcode = (): RefObject => { +export const useRenderBarcode = ( + config: BarcodeTypeConfig +): { ref: RefObject; error: boolean } => { const ref = useRef(null); + const [error, setError] = useState(false); const { - value, + codeValue: value, width, height, format, @@ -19,10 +23,43 @@ export const useRenderBarcode = (): RefObject => { enableMod43, addonFormat, addonSpacing - } = useBarcodeConfig(); + } = config; useEffect(() => { if (ref && typeof ref !== "function" && ref.current && value) { + // Reset error state at the start of each render attempt + setError(false); + + // Validate barcode value at runtime + const validationResult = validateBarcodeValue(format, value); + if (!validationResult.valid) { + const errorMsg = validationResult.message || "Invalid barcode value"; + // Log detailed error for developers + console.error( + `[Barcode Generator] Validation failed for format "${format}":`, + errorMsg, + `\nProvided value: "${value}"` + ); + setError(true); + return; + } + + // Validate addon if present + if (addonValue && addonFormat && addonFormat !== "None") { + const addonResult = validateAddonValue(addonFormat, addonValue); + if (!addonResult.valid) { + const errorMsg = addonResult.message || "Invalid addon value"; + // Log detailed error for developers + console.error( + `[Barcode Generator] Addon validation failed for format "${addonFormat}":`, + errorMsg, + `\nProvided addon value: "${addonValue}"` + ); + setError(true); + return; + } + } + try { const renderOptions: BarcodeRenderOptions = { value, @@ -41,11 +78,38 @@ export const useRenderBarcode = (): RefObject => { }; renderBarcode(ref, renderOptions); + setError(false); // Clear any previous errors } catch (error) { - console.error("Error generating barcode:", error); + const errorMsg = error instanceof Error ? error.message : "Error generating barcode"; + // Log detailed error for developers + console.error( + `[Barcode Generator] Rendering failed:`, + errorMsg, + `\nFormat: "${format}"`, + `\nValue: "${value}"`, + error + ); + setError(true); } + } else if (!value) { + // Clear error if value becomes empty + setError(false); } - }, [value, addonValue]); + }, [ + value, + format, + width, + height, + margin, + displayValue, + enableEan128, + enableFlat, + lastChar, + enableMod43, + addonValue, + addonFormat, + addonSpacing + ]); - return ref; + return { ref, error }; }; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss index e219c30909..431a27f89e 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss +++ b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGenerator.scss @@ -2,5 +2,99 @@ $widget-prefix: "barcode-generator"; .#{$widget-prefix} { - display: inline-block; + display: block; + width: 100%; + border-radius: var(--card-border-radius); + + &--as-card { + background-color: var(--card-bg); + border: var(--card-border); + padding: var(--spacing-medium); + } +} + +.qrcode-renderer, +.barcode-renderer { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + + > svg { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + } + + .qrcode-renderer-title { + font-weight: var(--font-weight-normal); + font-size: var(--font-size-small); + color: var(--gray-darker); + margin: 0; + } +} + +// Download button styled like mx-link +.barcode-generator-download-button { + display: inline-flex; + align-items: center; + gap: 4px; + background: none; + border: none; + padding: 0; + margin: 0; + font-family: inherit; + font-size: inherit; + color: var(--brand-primary, #0595db); + cursor: pointer; + text-decoration: none; + transition: + color 0.15s ease-in-out, + text-decoration 0.15s ease-in-out; + + &:hover { + color: var(--brand-primary-darker, #0470a6); + text-decoration: underline; + } + + &:focus { + outline: 2px solid var(--brand-primary, #0595db); + outline-offset: 2px; + border-radius: 2px; + } + + &:active { + color: var(--brand-primary-darkest, #03405a); + } + + &:disabled { + color: var(--gray-light, #ced0d3); + cursor: not-allowed; + text-decoration: none; + } +} + +// Preview graphics for barcode and QR code +.qrcode-preview-image { + max-width: 100%; + width: auto; + height: auto; + display: block; + object-fit: contain; +} + +.barcode-preview-image { + max-width: 100%; + width: 100%; + height: auto; + display: block; + object-fit: contain; +} + +// Overlay image for QR codes (positioned absolutely) +.qrcode-preview-overlay { + position: absolute; + object-fit: contain; } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGeneratorPreview.scss b/packages/pluggableWidgets/barcode-generator-web/src/ui/BarcodeGeneratorPreview.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts index 97802f0428..1830366147 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts @@ -72,8 +72,8 @@ export const createBarcodeWithAddon = ( // Add spacing BarcodeService.blank(addonSpacing); - // Add addon dynamically - BarcodeService[addonFormat](addonValue, { width: 1 }); + // Add addon dynamically with same displayValue setting + BarcodeService[addonFormat](addonValue, { width: 1, displayValue: options.displayValue }); BarcodeService.render(); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/download-code.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/download-code.ts new file mode 100644 index 0000000000..2f6926e2bf --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/download-code.ts @@ -0,0 +1,32 @@ +import { RefObject } from "react"; +import { convertSvgToPng, downloadBlob, prepareSvgForDownload, processQRImages } from "./download-utils"; +import { BarcodeConfig } from "../config/Barcode.config"; + +export async function downloadCode( + ref: RefObject, + type: BarcodeConfig["type"], + fileName: string +): Promise { + try { + const svgElement = ref.current; + if (!svgElement) { + console.error("SVG element not found for download"); + return; + } + + const clonedSvg = prepareSvgForDownload(svgElement); + + // Process overlay images for QR codes + if (type === "qrcode") { + await processQRImages(clonedSvg); + } + + // Convert SVG to PNG with 2x scale for better quality + const pngBlob = await convertSvgToPng(clonedSvg, 2); + + // Trigger download + downloadBlob(pngBlob, fileName); + } catch (error) { + console.error(`Error downloading ${type}:`, error); + } +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/download-utils.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/download-utils.ts index 2ee913a689..4f27ade4a4 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/utils/download-utils.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/download-utils.ts @@ -5,11 +5,6 @@ const NAMESPACES = { XLINK: "http://www.w3.org/1999/xlink" } as const; -const FILENAMES = { - QRCode: "qrcode.svg", - Barcode: "barcode.svg" -} as const; - // Prepare SVG for download by setting namespaces export const prepareSvgForDownload = (svgElement: SVGSVGElement): SVGSVGElement => { const clonedSvg = svgElement.cloneNode(true) as SVGSVGElement; @@ -68,4 +63,57 @@ export const downloadBlob = (blob: Blob, filename: string): void => { URL.revokeObjectURL(url); }; -export { FILENAMES }; +export const convertSvgToPng = async (svgElement: SVGSVGElement, scale = 2): Promise => { + return new Promise((resolve, reject) => { + const serializer = new XMLSerializer(); + const svgString = serializer.serializeToString(svgElement); + const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" }); + const url = URL.createObjectURL(svgBlob); + + const img = new Image(); + img.onload = () => { + try { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + if (!ctx) { + throw new Error("Failed to get canvas context"); + } + + // Set canvas dimensions with scale for better quality + canvas.width = img.width * scale; + canvas.height = img.height * scale; + + // Fill white background (important for transparency) + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw the image scaled + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + canvas.toBlob( + blob => { + URL.revokeObjectURL(url); + if (blob) { + resolve(blob); + } else { + reject(new Error("Failed to create PNG blob")); + } + }, + "image/png", + 1.0 + ); + } catch (error) { + URL.revokeObjectURL(url); + reject(error); + } + }; + + img.onerror = () => { + URL.revokeObjectURL(url); + reject(new Error("Failed to load SVG image")); + }; + + img.src = url; + }); +}; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/qrcode-preview-utils.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/qrcode-preview-utils.ts new file mode 100644 index 0000000000..2cf1de02cb --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/qrcode-preview-utils.ts @@ -0,0 +1,26 @@ +export const QR_IMAGE_PLACEHOLDER = + "data:image/svg+xml;utf8," + + "" + + "" + + "" + + ""; + +// Resolve the actual image URL from user config +export function resolveQRImageSrc(qrImageSrc: any, imageSrcError: boolean): string { + if (!qrImageSrc) { + return QR_IMAGE_PLACEHOLDER; + } + + if (imageSrcError) { + return QR_IMAGE_PLACEHOLDER; + } + + // Static image URL + if (qrImageSrc.type === "static") { + return qrImageSrc.imageUrl; + } + + // Dynamic image (from data entity) - not directly resolvable in preview + // Fall back to placeholder + return QR_IMAGE_PLACEHOLDER; +} diff --git a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts index 8776621ad5..ca0ee33637 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -4,11 +4,13 @@ * @author Mendix Widgets Framework Team */ import { CSSProperties } from "react"; -import { DynamicValue, EditableValue, WebImage } from "mendix"; +import { DynamicValue, WebImage } from "mendix"; import { Big } from "big.js"; export type CodeFormatEnum = "CODE128" | "QRCode" | "Custom"; +export type ButtonPositionEnum = "top" | "bottom"; + export type CustomCodeFormatEnum = "CODE128" | "EAN13" | "EAN8" | "UPC" | "CODE39" | "ITF14" | "MSI" | "pharmacode" | "codabar" | "CODE93"; export type AddonFormatEnum = "None" | "EAN5" | "EAN2"; @@ -20,19 +22,23 @@ export interface BarcodeGeneratorContainerProps { class: string; style?: CSSProperties; tabIndex?: number; - codeValue: EditableValue; + codeValue: DynamicValue; codeFormat: CodeFormatEnum; allowDownload: boolean; - downloadAriaLabel: string; + downloadButtonCaption?: DynamicValue; + downloadButtonAriaLabel?: DynamicValue; + downloadFileName?: DynamicValue; + buttonPosition: ButtonPositionEnum; customCodeFormat: CustomCodeFormatEnum; enableEan128: boolean; enableFlat: boolean; lastChar: string; enableMod43: boolean; addonFormat: AddonFormatEnum; - addonValue: EditableValue; + addonValue: DynamicValue; addonSpacing: number; displayValue: boolean; + showAsCard: boolean; codeWidth: number; codeHeight: number; codeMargin: number; @@ -40,15 +46,15 @@ export interface BarcodeGeneratorContainerProps { qrMargin: number; qrTitle: string; qrLevel: QrLevelEnum; - qrImage: boolean; - qrImageSrc: DynamicValue; - qrImageCenter: boolean; - qrImageX: number; - qrImageY: number; - qrImageHeight: number; - qrImageWidth: number; - qrImageOpacity: Big; - qrImageExcavate: boolean; + qrOverlay: boolean; + qrOverlaySrc: DynamicValue; + qrOverlayCenter: boolean; + qrOverlayX: number; + qrOverlayY: number; + qrOverlayHeight: number; + qrOverlayWidth: number; + qrOverlayOpacity: Big; + qrOverlayExcavate: boolean; } export interface BarcodeGeneratorPreviewProps { @@ -65,7 +71,10 @@ export interface BarcodeGeneratorPreviewProps { codeValue: string; codeFormat: CodeFormatEnum; allowDownload: boolean; - downloadAriaLabel: string; + downloadButtonCaption: string; + downloadButtonAriaLabel: string; + downloadFileName: string; + buttonPosition: ButtonPositionEnum; customCodeFormat: CustomCodeFormatEnum; enableEan128: boolean; enableFlat: boolean; @@ -75,6 +84,7 @@ export interface BarcodeGeneratorPreviewProps { addonValue: string; addonSpacing: number | null; displayValue: boolean; + showAsCard: boolean; codeWidth: number | null; codeHeight: number | null; codeMargin: number | null; @@ -82,13 +92,13 @@ export interface BarcodeGeneratorPreviewProps { qrMargin: number | null; qrTitle: string; qrLevel: QrLevelEnum; - qrImage: boolean; - qrImageSrc: { type: "static"; imageUrl: string; } | { type: "dynamic"; entity: string; } | null; - qrImageCenter: boolean; - qrImageX: number | null; - qrImageY: number | null; - qrImageHeight: number | null; - qrImageWidth: number | null; - qrImageOpacity: number | null; - qrImageExcavate: boolean; + qrOverlay: boolean; + qrOverlaySrc: { type: "static"; imageUrl: string; } | { type: "dynamic"; entity: string; } | null; + qrOverlayCenter: boolean; + qrOverlayX: number | null; + qrOverlayY: number | null; + qrOverlayHeight: number | null; + qrOverlayWidth: number | null; + qrOverlayOpacity: number | null; + qrOverlayExcavate: boolean; }