Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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";
import structurePreviewSvg from "./assets/structurePreview.svg";

export type Problem = {
property?: string; // key of the property, at which the problem exists
Expand Down Expand Up @@ -31,33 +33,41 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope
]);
}

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"]);
}
Expand Down Expand Up @@ -88,6 +98,15 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope
return defaultProperties;
}

export function getPreview(_: StructurePreviewProps): StructurePreviewProps | null {
return {
type: "Image",
document: decodeURIComponent(structurePreviewSvg.replace("data:image/svg+xml,", "")),
height: 275,
width: 275
};
}

export function check(_values: BarcodeGeneratorPreviewProps): Problem[] {
const errors: Problem[] = [];

Expand Down Expand Up @@ -128,28 +147,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<string, string> = {
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
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 (
<a className="mx-link" role="button" aria-label={props.downloadButtonAriaLabel || undefined} tabIndex={0}>
<DownloadIcon /> {props.downloadButtonCaption || defaultDownloadCaption}
</a>
);
}

export function preview(props: BarcodeGeneratorPreviewProps): ReactElement {
const styles = parseStyle(props.style);
const isQrCode = props.codeFormat === "QRCode";
const downloadButton = <PreviewDownloadButton {...props} />;

return (
<div className="barcode-generator-widget-preview">
<img src={doc} alt="" />
<div
className={classNames(
props.class,
props.className,
"barcode-generator",
{ "barcode-generator--as-card": props.showAsCard },
"barcode-generator-widget-preview"
)}
style={styles}
>
{isQrCode ? (
<QRCodePreview {...props} downloadButton={downloadButton} />
) : (
<BarcodePreview {...props} downloadButton={downloadButton} />
)}
</div>
);
}

export function getPreviewCss(): string {
return require("./ui/BarcodeGeneratorPreview.scss");
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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 (
<div className="barcode-generator" tabIndex={tabIndex}>
{config.isQRCode ? <QRCodeRenderer /> : <BarcodeRenderer />}
</div>
);
}

export default function BarcodeGenerator(props: BarcodeGeneratorContainerProps): ReactElement {
const config = barcodeConfig(props);

if (!config.value) {
if (!config.codeValue) {
return <span>No barcode value provided</span>;
}

return (
<BarcodeContextProvider config={config}>
<BarcodeContainer tabIndex={props.tabIndex} />
</BarcodeContextProvider>
<div
className={classNames(props.class, "barcode-generator", {
"barcode-generator--as-card": props.showAsCard
})}
tabIndex={props.tabIndex}
style={props.style}
>
{config.type === "qrcode" ? <QRCodeRenderer config={config} /> : <BarcodeRenderer config={config} />}
</div>
);
}
Loading
Loading