diff --git a/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md b/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md index 861d88dfaf..21b358e32a 100644 --- a/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md +++ b/packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Comprehensive configuration and styling settings for various barcode types +- Download functionality for barcodes + ## [1.0.0] - 2025-10-09 ### Added diff --git a/packages/pluggableWidgets/barcode-generator-web/package.json b/packages/pluggableWidgets/barcode-generator-web/package.json index f11aecb8cb..bf1e3a0227 100644 --- a/packages/pluggableWidgets/barcode-generator-web/package.json +++ b/packages/pluggableWidgets/barcode-generator-web/package.json @@ -56,6 +56,7 @@ "@mendix/widget-plugin-component-kit": "workspace:*", "@mendix/widget-plugin-platform": "workspace:*", "@mendix/widget-plugin-test-utils": "workspace:*", - "cross-env": "^7.0.3" + "cross-env": "^7.0.3", + "eslint": "^9.37.0" } } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts index 077e6c5783..1dfe503b00 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts @@ -1,4 +1,4 @@ -import { hidePropertiesIn, Properties } from "@mendix/pluggable-widgets-tools"; +import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools"; import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps"; export type Problem = { @@ -12,14 +12,78 @@ export type Problem = { export function getProperties(values: BarcodeGeneratorPreviewProps, defaultProperties: Properties): Properties { if (values.codeFormat === "QRCode") { - hidePropertiesIn(defaultProperties, values, ["codeWidth", "codeHeight", "displayValue"]); + hidePropertiesIn(defaultProperties, values, ["codeWidth", "codeHeight", "displayValue", "codeMargin"]); } else { - hidePropertiesIn(defaultProperties, values, ["qrSize"]); + hidePropertiesIn(defaultProperties, values, ["qrImage", "qrSize", "qrMargin", "qrLevel", "qrTitle"]); + } + + if (values.codeFormat !== "QRCode" || !values.qrImage) { + hidePropertiesIn(defaultProperties, values, [ + "qrImageSrc", + "qrImageCenter", + "qrImageWidth", + "qrImageHeight", + "qrImageX", + "qrImageY", + "qrImageOpacity", + "qrImageExcavate" + ]); + } + + if (values.codeFormat !== "CODE128" && values.customCodeFormat !== "CODE128") { + hidePropertyIn(defaultProperties, values, "enableEan128"); + } + + if ( + values.codeFormat === "QRCode" || + values.codeFormat === "CODE128" || + (values.codeFormat === "Custom" && + values.customCodeFormat !== "EAN13" && + values.customCodeFormat !== "EAN8" && + values.customCodeFormat !== "UPC") + ) { + hidePropertyIn(defaultProperties, values, "enableFlat"); + } + + if ( + values.codeFormat === "QRCode" || + values.codeFormat === "CODE128" || + (values.codeFormat === "Custom" && values.customCodeFormat !== "EAN13") + ) { + hidePropertyIn(defaultProperties, values, "lastChar"); + } + + if ( + values.codeFormat === "QRCode" || + values.codeFormat === "CODE128" || + (values.codeFormat === "Custom" && values.customCodeFormat !== "EAN13" && values.customCodeFormat !== "EAN8") + ) { + hidePropertiesIn(defaultProperties, values, ["addonFormat", "addonValue", "addonSpacing"]); + } + if ( + values.codeFormat === "QRCode" || + values.codeFormat === "CODE128" || + (values.codeFormat === "Custom" && values.addonFormat !== "EAN5" && values.addonFormat !== "EAN2") + ) { + hidePropertiesIn(defaultProperties, values, ["addonValue", "addonSpacing"]); + } + + if ( + values.codeFormat === "QRCode" || + values.codeFormat === "CODE128" || + (values.codeFormat === "Custom" && values.customCodeFormat !== "CODE39") + ) { + hidePropertyIn(defaultProperties, values, "enableMod43"); + } + + if (values.qrImageCenter) { + hidePropertiesIn(defaultProperties, values, ["qrImageX", "qrImageY"]); } if (values.codeFormat !== "Custom") { hidePropertiesIn(defaultProperties, values, ["customCodeFormat"]); } + return defaultProperties; } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx index a6e3c41aae..444dc6df37 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.tsx @@ -1,53 +1,32 @@ -import JsBarcode from "jsbarcode"; -import { QRCodeSVG } from "qrcode.react"; -import { ReactElement, useEffect, useRef } from "react"; +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"; -export default function BarcodeGenerator({ - codeValue, - codeWidth, - codeHeight, - codeFormat, - codeMargin, - displayValue, - qrSize, - tabIndex -}: BarcodeGeneratorContainerProps): ReactElement { - const svgRef = useRef(null); +function BarcodeContainer({ tabIndex }: { tabIndex?: number }): ReactElement { + const config = useBarcodeConfig(); - const value = codeValue?.status === "available" ? codeValue.value : ""; - const width = codeWidth ?? 128; - const height = codeHeight ?? 128; - const format = codeFormat ?? "CODE128"; - const margin = codeMargin ?? 2; - const showValue = displayValue ?? false; - const size = qrSize ?? 128; + return ( +
+ {config.isQRCode ? : } +
+ ); +} - useEffect(() => { - if (format !== "QRCode" && svgRef.current && value) { - try { - JsBarcode(svgRef.current, value, { - format, - width, - height, - margin, - displayValue: showValue - }); - } catch (error) { - console.error("Error generating barcode:", error); - } - } - }, [value, width, height, format, margin, showValue]); +export default function BarcodeGenerator(props: BarcodeGeneratorContainerProps): ReactElement { + const config = barcodeConfig(props); - if (!value) { + if (!config.value) { return No barcode value provided; } return ( -
- {format === "QRCode" ? : } -
+ + + ); } diff --git a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml index d4ea655914..279ea9732d 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml +++ b/packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.xml @@ -21,9 +21,67 @@ Barcode QR Code - Custom Format + Custom + + Custom Format + Choose between barcode types format + + CODE128 + EAN-13 + EAN-8 + UPC + CODE39 + ITF-14 + MSI + Pharmacode + Codabar + CODE93 + + + + EAN-128 + Enable encoding CODE128 as GS1-128/EAN-128 + + + Flat + Enable flat barcode, skip guard bars + + + Last character + Character after the barcode + + + Mod43 + For code 39 if used with modulo 43 check digit + + + Allow download + Adds a download button + + + + + Addon format + Choose between EAN-5 or EAN-2 addon format + + None + EAN-5 + 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) + @@ -32,41 +90,75 @@ Bar width - Width of the barcode bars + Width of a single bar Code height + Height of the barcode + + + Margin size In pixels QR Size The size of the QR box - + Margin size - In pixels + + + + Title + Used for accessibility reasons + + + Level + The Error Correction Level to use + + L + M + Q + H + + + + 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 - - - Barcode Format - Choose between barcode types format - - CODE128 - EAN-13 - EAN-8 - EAN-5 - EAN-2 - UPC - CODE39 - ITF-14 - MSI - Pharmacode - Codabar - CODE93 - - - 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 ff4c4e1464..5f68f79909 100644 --- a/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx +++ b/packages/pluggableWidgets/barcode-generator-web/src/__tests__/BarcodeGenerator.spec.tsx @@ -25,12 +25,32 @@ describe("BarcodeGenerator", () => { class: "mx-barcode-generator", tabIndex: -1, codeFormat: "QRCode" as CodeFormatEnum, + customCodeFormat: "CODE128" as CustomCodeFormatEnum, + enableEan128: false, + enableFlat: false, + lastChar: "", + enableMod43: false, + allowDownload: false, displayValue: false, codeWidth: 2, codeHeight: 200, - qrSize: 128, codeMargin: 4, - customCodeFormat: "CODE128" as CustomCodeFormatEnum, + 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() }; @@ -87,7 +107,7 @@ describe("BarcodeGenerator", () => { it("renders CODE128 barcode when format is not QR", () => { const props = { ...defaultProps, - codeFormat: "CODE128" as const, + codeFormat: "CODE128" as CodeFormatEnum, codeValue: { value: "123456789", status: "available" @@ -108,7 +128,11 @@ describe("BarcodeGenerator", () => { width: 2, height: 200, margin: 4, - displayValue: false + displayValue: false, + ean128: false, + flat: false, + lastChar: "", + mod43: false } ); }); @@ -207,7 +231,46 @@ describe("BarcodeGenerator", () => { width: 2, // from defaultProps height: 200, // from defaultProps margin: 4, // from defaultProps - displayValue: false + displayValue: false, + ean128: false, + flat: false, + lastChar: "", + mod43: false }); }); + + 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(); + }); }); diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx new file mode 100644 index 0000000000..3f25d8cfac --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/Barcode.tsx @@ -0,0 +1,22 @@ +import { useRenderBarcode } from "../hooks/useRenderBarcode"; +import { useDownloadBarcode } from "../hooks/useDownloadBarcode"; +import { useBarcodeConfig } from "../config/BarcodeContext"; + +import { Fragment } from "react"; + +export const BarcodeRenderer = () => { + const ref = useRenderBarcode(); + const { allowDownload } = useBarcodeConfig(); + const { downloadBarcode } = useDownloadBarcode({ ref }); + + return ( + + + {allowDownload && ( + + )} + + ); +}; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx new file mode 100644 index 0000000000..b7642ca5ea --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/components/QRCode.tsx @@ -0,0 +1,55 @@ +import { QRCodeSVG } from "qrcode.react"; +import { Fragment, useRef } from "react"; +import { useDownloadQrCode } from "../hooks/useDownloadQRCode"; +import { useBarcodeConfig } from "../config/BarcodeContext"; + +export const QRCodeRenderer = () => { + 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 + } = useBarcodeConfig(); + const imageSettings = imageSrc + ? { + src: imageSrc, + x: imageX, + y: imageY, + height: imageHeight, + width: imageWidth, + opacity: imageOpacity, + excavate: imageExcavate + } + : undefined; + + return ( + + + {allowDownload && ( + + )} + + ); +}; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts new file mode 100644 index 0000000000..8c0149fc52 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/Barcode.config.ts @@ -0,0 +1,78 @@ +import { BarcodeGeneratorContainerProps } from "../../typings/BarcodeGeneratorProps"; + +/** Configuration for static values that don't change at runtime. */ +export interface BarcodeConfig { + // Basic barcode properties + value: string; + width: number; + height: number; + format: string; + isQRCode: boolean; + margin: number; + displayValue: boolean; + allowDownload: boolean; + + // Advanced barcode options + enableEan128: boolean; + enableFlat: boolean; + lastChar: string; + enableMod43: boolean; + 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; +} + +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, + width: props.codeWidth ?? 128, + height: props.codeHeight ?? 128, + format, + isQRCode, + margin: props.codeMargin ?? 2, + displayValue: props.displayValue ?? false, + allowDownload: props.allowDownload ?? false, + + // Advanced barcode options + enableEan128: props.enableEan128 ?? false, + enableFlat: props.enableFlat ?? false, + lastChar: props.lastChar ?? "", + 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 + }); +} diff --git a/packages/pluggableWidgets/barcode-generator-web/src/config/BarcodeContext.tsx b/packages/pluggableWidgets/barcode-generator-web/src/config/BarcodeContext.tsx new file mode 100644 index 0000000000..a84fe138b4 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/config/BarcodeContext.tsx @@ -0,0 +1,21 @@ +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/useDownloadBarcode.ts b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadBarcode.ts new file mode 100644 index 0000000000..aeeca7e261 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadBarcode.ts @@ -0,0 +1,38 @@ +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 new file mode 100644 index 0000000000..47be31d8c4 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useDownloadQRCode.ts @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000000..5ea15389a2 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/hooks/useRenderBarcode.ts @@ -0,0 +1,51 @@ +import { useBarcodeConfig } from "../config/BarcodeContext"; +import { RefObject, useEffect, useRef } from "react"; +import { type BarcodeRenderOptions, renderBarcode } from "../utils/barcodeRenderer-utils"; + +export const useRenderBarcode = (): RefObject => { + const ref = useRef(null); + + const { + value, + width, + height, + format, + margin, + displayValue, + addonValue, + enableEan128, + enableFlat, + lastChar, + enableMod43, + addonFormat, + addonSpacing + } = useBarcodeConfig(); + + useEffect(() => { + if (ref && typeof ref !== "function" && ref.current && value) { + try { + const renderOptions: BarcodeRenderOptions = { + value, + format, + width, + height, + margin, + displayValue, + ean128: enableEan128, + flat: enableFlat, + lastChar, + mod43: enableMod43, + addonValue, + addonFormat, + addonSpacing + }; + + renderBarcode(ref, renderOptions); + } catch (error) { + console.error("Error generating barcode:", error); + } + } + }, [value, addonValue]); + + return ref; +}; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts new file mode 100644 index 0000000000..97802f0428 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/barcodeRenderer-utils.ts @@ -0,0 +1,119 @@ +import JsBarcode from "jsbarcode"; +import { type ForwardedRef } from "react"; + +interface BarcodeMethodOptions { + width?: number; + height?: number; + margin?: number; + displayValue?: boolean; +} + +interface BarcodeService { + EAN13: (value: string, options: BarcodeMethodOptions) => BarcodeService; + EAN8: (value: string, options: BarcodeMethodOptions) => BarcodeService; + EAN5: (value: string, options: BarcodeMethodOptions) => BarcodeService; + EAN2: (value: string, options: BarcodeMethodOptions) => BarcodeService; + blank: (spacing: number) => BarcodeService; + render: () => void; + [key: string]: any; +} + +export interface BarcodeOptions { + format: string; + width: number; + height: number; + margin: number; + displayValue: boolean; + ean128?: boolean; + flat?: boolean; + lastChar?: string; + mod43?: boolean; +} + +export interface BarcodeRenderOptions { + value: string; + format: string; + width: number; + height: number; + margin: number; + displayValue: boolean; + ean128?: boolean; + flat?: boolean; + lastChar?: string; + mod43?: boolean; + addonValue?: string; + addonFormat?: string; + addonSpacing?: number; +} + +/** + * Creates a barcode with an addon (EAN2 or EAN5) + */ +export const createBarcodeWithAddon = ( + ref: ForwardedRef, + value: string, + mainFormat: string, + addonValue: string, + addonFormat: string, + options: BarcodeOptions, + addonSpacing: number +): void => { + if (ref && typeof ref !== "function" && ref.current) { + const BarcodeService = JsBarcode(ref.current) as BarcodeService; + + // Generate main barcode dynamically + BarcodeService[mainFormat](value, { + width: options.width, + height: options.height, + margin: options.margin, + displayValue: options.displayValue + }); + + // Add spacing + BarcodeService.blank(addonSpacing); + + // Add addon dynamically + BarcodeService[addonFormat](addonValue, { width: 1 }); + + BarcodeService.render(); + } +}; + +/** + * Creates a standard barcode without addons + */ +export const createStandardBarcode = ( + ref: ForwardedRef, + value: string, + options: BarcodeOptions +): void => { + if (ref && typeof ref !== "function" && ref.current) { + JsBarcode(ref.current, value, options); + } +}; + +/** + * Renders a barcode with optional addon support + */ +export const renderBarcode = (ref: ForwardedRef, renderOptions: BarcodeRenderOptions): void => { + const { value, format, addonValue, addonFormat, addonSpacing = 20, ...barcodeOptions } = renderOptions; + + const options: BarcodeOptions = { + format, + ...barcodeOptions + }; + + switch (addonFormat) { + case "EAN5": + createBarcodeWithAddon(ref, value, format, addonValue!, addonFormat, options, addonSpacing); + break; + + case "EAN2": + createBarcodeWithAddon(ref, value, format, addonValue!, addonFormat, options, addonSpacing); + break; + + default: + createStandardBarcode(ref, value, options); + break; + } +}; diff --git a/packages/pluggableWidgets/barcode-generator-web/src/utils/download-utils.ts b/packages/pluggableWidgets/barcode-generator-web/src/utils/download-utils.ts new file mode 100644 index 0000000000..2ee913a689 --- /dev/null +++ b/packages/pluggableWidgets/barcode-generator-web/src/utils/download-utils.ts @@ -0,0 +1,71 @@ +// SVG download and processing utilities + +const NAMESPACES = { + SVG: "http://www.w3.org/2000/svg", + 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; + clonedSvg.setAttribute("xmlns", NAMESPACES.SVG); + clonedSvg.setAttribute("xmlns:xlink", NAMESPACES.XLINK); + return clonedSvg; +}; + +export const convertImageToBase64 = async (url: string): Promise => { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch image: ${response.statusText}`); + } + const blob = await response.blob(); + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(new Error("Failed to convert image to base64")); + reader.readAsDataURL(blob); + }); + } catch (error) { + console.warn("Failed to convert image to base64:", error); + return url; // Return original URL as fallback + } +}; + +// Check if URL is external (http/https) +export const isExternalUrl = (url: string): boolean => { + return url.startsWith("http://") || url.startsWith("https://"); +}; + +// Convert overlay images to base64 for QR codes +export const processQRImages = async (clonedSvg: SVGSVGElement): Promise => { + const imageElement = clonedSvg.querySelector("image"); + if (imageElement) { + const hrefValue = imageElement.getAttribute("href") || imageElement.getAttribute("xlink:href"); + if (hrefValue && isExternalUrl(hrefValue)) { + const base64 = await convertImageToBase64(hrefValue); + // Use modern href attribute and remove any existing xlink:href + imageElement.setAttribute("href", base64); + imageElement.removeAttribute("xlink:href"); + } + } +}; + +export const downloadBlob = (blob: Blob, filename: string): void => { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +}; + +export { FILENAMES }; diff --git a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts index ff747ea4cf..6c0b1aecad 100644 --- a/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts +++ b/packages/pluggableWidgets/barcode-generator-web/typings/BarcodeGeneratorProps.d.ts @@ -4,11 +4,16 @@ * @author Mendix Widgets Framework Team */ import { CSSProperties } from "react"; -import { EditableValue } from "mendix"; +import { DynamicValue, EditableValue, WebImage } from "mendix"; +import { Big } from "big.js"; export type CodeFormatEnum = "CODE128" | "QRCode" | "Custom"; -export type CustomCodeFormatEnum = "CODE128" | "EAN13" | "EAN8" | "EAN5" | "EAN2" | "UPC" | "CODE39" | "ITF14" | "MSI" | "pharmacode" | "codabar" | "CODE93"; +export type CustomCodeFormatEnum = "CODE128" | "EAN13" | "EAN8" | "UPC" | "CODE39" | "ITF14" | "MSI" | "pharmacode" | "codabar" | "CODE93"; + +export type AddonFormatEnum = "None" | "EAN5" | "EAN2"; + +export type QrLevelEnum = "L" | "M" | "Q" | "H"; export interface BarcodeGeneratorContainerProps { name: string; @@ -17,12 +22,32 @@ export interface BarcodeGeneratorContainerProps { tabIndex?: number; codeValue: EditableValue; codeFormat: CodeFormatEnum; + customCodeFormat: CustomCodeFormatEnum; + enableEan128: boolean; + enableFlat: boolean; + lastChar: string; + enableMod43: boolean; + allowDownload: boolean; + addonFormat: AddonFormatEnum; + addonValue: EditableValue; + addonSpacing: number; displayValue: boolean; codeWidth: number; codeHeight: number; - qrSize: number; codeMargin: number; - customCodeFormat: CustomCodeFormatEnum; + qrSize: number; + qrMargin: number; + qrTitle: string; + qrLevel: QrLevelEnum; + qrImage: boolean; + qrImageSrc: DynamicValue; + qrImageCenter: boolean; + qrImageX: number; + qrImageY: number; + qrImageHeight: number; + qrImageWidth: number; + qrImageOpacity: Big; + qrImageExcavate: boolean; } export interface BarcodeGeneratorPreviewProps { @@ -38,10 +63,30 @@ export interface BarcodeGeneratorPreviewProps { translate: (text: string) => string; codeValue: string; codeFormat: CodeFormatEnum; + customCodeFormat: CustomCodeFormatEnum; + enableEan128: boolean; + enableFlat: boolean; + lastChar: string; + enableMod43: boolean; + allowDownload: boolean; + addonFormat: AddonFormatEnum; + addonValue: string; + addonSpacing: number | null; displayValue: boolean; codeWidth: number | null; codeHeight: number | null; - qrSize: number | null; codeMargin: number | null; - customCodeFormat: CustomCodeFormatEnum; + qrSize: number | null; + 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; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7774b684ae..11b30e7f82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -632,9 +632,15 @@ importers: '@mendix/widget-plugin-test-utils': specifier: workspace:* version: link:../../shared/widget-plugin-test-utils + '@types/react': + specifier: '>=18.2.36' + version: 19.2.2 cross-env: specifier: ^7.0.3 version: 7.0.3 + eslint: + specifier: ^9.37.0 + version: 9.37.0(jiti@2.6.1) packages/pluggableWidgets/barcode-scanner-web: dependencies: @@ -12483,7 +12489,7 @@ snapshots: identity-obj-proxy: 3.0.0 jasmine: 3.99.0 jasmine-core: 3.99.1 - jest: 29.7.0(@types/node@22.14.1) + jest: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) jest-environment-jsdom: 29.7.0 jest-jasmine2: 29.7.0 jest-junit: 13.2.0 @@ -17026,18 +17032,6 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.14.1): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest@29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)): dependencies: '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) @@ -19904,7 +19898,7 @@ snapshots: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.14.1) + jest: 29.7.0(@types/node@22.14.1)(ts-node@10.9.2(@swc/core@1.13.5)(@types/node@22.14.1)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6