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..5633fe1b52 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,17 +86,32 @@ 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") { hidePropertiesIn(defaultProperties, values, ["customCodeFormat"]); } + if (!values.allowDownload) { + hidePropertiesIn(defaultProperties, values, [ + "downloadButtonCaption", + "downloadButtonAriaLabel", + "downloadFileName", + "buttonPosition" + ]); + } + 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 +152,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..ef1a43a000 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) { - return No barcode value provided; + if (!config.codeValue) { + return {props.emptyMessage?.value || "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..676cc4bef5 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 @@ -24,13 +22,45 @@ Custom + + Empty message + + + No barcode value provided + Geen barcodewaarde opgegeven + + 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 +88,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,31 +109,44 @@ EAN-2 - + Addon value Value for the addon barcode (5 digits for EAN-5, 2 digits for EAN-2) - - - + Addon spacing Space between main barcode and addon (in pixels) + + + Log Level + Choose the log level for in the case of failure for generating the barcode. Info will display generic error message on the UI and Debug will gives detailed information on the developer console. + + None + Info + Debug + + + 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 +154,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 +174,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..94816d2045 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx @@ -1,22 +1,45 @@ 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 ( +
+ {config.logLevel !== "None" && ( +
+ 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..5aecb12775 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,27 @@ -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"; +} + +type codeType = "barcode" | "qrcode"; + +export interface CodeBaseTypeConfig extends Pick { + type: T; + codeValue: string; + margin: number; + downloadButton?: DownloadButtonConfig; +} + +/** Configuration for barcode (non-QR) rendering */ +export interface BarcodeTypeConfig extends CodeBaseTypeConfig<"barcode"> { width: number; height: number; format: string; - isQRCode: boolean; - margin: number; displayValue: boolean; - allowDownload: boolean; - downloadAriaLabel?: string; // Advanced barcode options enableEan128: boolean; @@ -21,38 +31,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 extends CodeBaseTypeConfig<"qrcode"> { + size: number; + title: string; + level: QrLevelEnum; + 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"; - - return Object.freeze({ - // Basic barcode properties - value, + 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; + + const baseConfig: CodeBaseTypeConfig = { + type: format === "QRCode" ? "qrcode" : "barcode", + codeValue, + margin: props.codeMargin ?? 2, + logLevel: props.logLevel, + downloadButton: downloadButtonConfig + }; + + if (format === "QRCode") { + return { + ...baseConfig, + type: "qrcode", + size: props.qrSize ?? 128, + title: props.qrTitle ?? "", + level: props.qrLevel ?? "L", + 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 { + ...baseConfig, + type: "barcode", 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, // Advanced barcode options enableEan128: props.enableEan128 ?? false, @@ -61,20 +109,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, - - // 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 - }); + 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 + } + + // 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..0ef6797816 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts @@ -1,12 +1,23 @@ -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"; +import { LogLevelEnum } from "../../typings/BarcodeGeneratorProps"; -export const useRenderBarcode = (): RefObject => { +function printError(message: string, logLevel: LogLevelEnum) { + if (logLevel === "Debug") { + console.error(`[Barcode Generator] ${message}`); + } +} + +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 +30,42 @@ 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 + + printError( + `Validation failed for format "${format}": ${errorMsg} \nProvided value: "${value}"`, + config.logLevel + ); + 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 + printError( + `Addon validation failed for format "${addonFormat}": ${errorMsg} \nProvided addon value: "${addonValue}"`, + config.logLevel + ); + setError(true); + return; + } + } + try { const renderOptions: BarcodeRenderOptions = { value, @@ -41,11 +84,32 @@ 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 + printError(`Rendering failed: ${errorMsg} \nFormat: "${format}" \nValue: "${value}"`, config.logLevel); + 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..941382fc36 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -4,15 +4,19 @@ * @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"; +export type LogLevelEnum = "None" | "Info" | "Debug"; + export type QrLevelEnum = "L" | "M" | "Q" | "H"; export interface BarcodeGeneratorContainerProps { @@ -20,19 +24,25 @@ export interface BarcodeGeneratorContainerProps { class: string; style?: CSSProperties; tabIndex?: number; - codeValue: EditableValue; + codeValue: DynamicValue; codeFormat: CodeFormatEnum; + emptyMessage?: DynamicValue; 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; + logLevel: LogLevelEnum; displayValue: boolean; + showAsCard: boolean; codeWidth: number; codeHeight: number; codeMargin: number; @@ -40,15 +50,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 { @@ -64,8 +74,12 @@ export interface BarcodeGeneratorPreviewProps { translate: (text: string) => string; codeValue: string; codeFormat: CodeFormatEnum; + emptyMessage: string; allowDownload: boolean; - downloadAriaLabel: string; + downloadButtonCaption: string; + downloadButtonAriaLabel: string; + downloadFileName: string; + buttonPosition: ButtonPositionEnum; customCodeFormat: CustomCodeFormatEnum; enableEan128: boolean; enableFlat: boolean; @@ -74,7 +88,9 @@ export interface BarcodeGeneratorPreviewProps { addonFormat: AddonFormatEnum; addonValue: string; addonSpacing: number | null; + logLevel: LogLevelEnum; displayValue: boolean; + showAsCard: boolean; codeWidth: number | null; codeHeight: number | null; codeMargin: number | null; @@ -82,13 +98,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; }