Skip to content

Commit 1d0fa4e

Browse files
authored
[WC-3181] Barcode Generator: Complete app (#2059)
2 parents fac03d1 + 296c529 commit 1d0fa4e

51 files changed

Lines changed: 2333 additions & 638 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/pluggableWidgets/barcode-generator-web/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88

99
### Added
1010

11+
- Error handling for incompatible barcode types
12+
- Enhanced preview for all barcode types
1113
- Comprehensive configuration and styling settings for various barcode types
1214
- Download functionality for barcodes
1315

packages/pluggableWidgets/barcode-generator-web/src/BarcodeGenerator.editorConfig.ts

Lines changed: 138 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { StructurePreviewProps } from "@mendix/widget-plugin-platform/preview/structure-preview-api";
12
import { hidePropertiesIn, hidePropertyIn, Properties } from "@mendix/pluggable-widgets-tools";
2-
import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps";
3+
import { BarcodeGeneratorPreviewProps, CodeFormatEnum, CustomCodeFormatEnum } from "../typings/BarcodeGeneratorProps";
34
import { validateAddonValue, validateBarcodeValue } from "./config/validation";
45

56
export type Problem = {
@@ -15,49 +16,64 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope
1516
if (values.codeFormat === "QRCode") {
1617
hidePropertiesIn(defaultProperties, values, ["codeWidth", "codeHeight", "displayValue", "codeMargin"]);
1718
} else {
18-
hidePropertiesIn(defaultProperties, values, ["qrImage", "qrSize", "qrMargin", "qrLevel", "qrTitle"]);
19+
hidePropertiesIn(defaultProperties, values, [
20+
"qrOverlay",
21+
"qrSize",
22+
"qrMargin",
23+
"qrLevel",
24+
"qrTitle",
25+
"showTitle"
26+
]);
1927
}
2028

21-
if (values.codeFormat !== "QRCode" || !values.qrImage) {
29+
if (values.codeFormat !== "QRCode" || !values.qrOverlay) {
2230
hidePropertiesIn(defaultProperties, values, [
23-
"qrImageSrc",
24-
"qrImageCenter",
25-
"qrImageWidth",
26-
"qrImageHeight",
27-
"qrImageX",
28-
"qrImageY",
29-
"qrImageOpacity",
30-
"qrImageExcavate"
31+
"qrOverlaySrc",
32+
"qrOverlayCenter",
33+
"qrOverlayWidth",
34+
"qrOverlayHeight",
35+
"qrOverlayX",
36+
"qrOverlayY",
37+
"qrOverlayOpacity",
38+
"qrOverlayExcavate"
3139
]);
3240
}
3341

34-
if (values.codeFormat !== "CODE128" && values.customCodeFormat !== "CODE128") {
42+
if (values.codeFormat === "QRCode" || (values.codeFormat !== "CODE128" && values.customCodeFormat !== "CODE128")) {
3543
hidePropertyIn(defaultProperties, values, "enableEan128");
3644
}
3745

46+
// enableFlat is only supported for EAN-13 and EAN-8, and NOT when addons are enabled
3847
if (
39-
values.codeFormat === "QRCode" ||
40-
values.codeFormat === "CODE128" ||
41-
(values.codeFormat === "Custom" &&
42-
values.customCodeFormat !== "EAN13" &&
43-
values.customCodeFormat !== "EAN8" &&
44-
values.customCodeFormat !== "UPC")
48+
!(
49+
values.codeFormat === "Custom" &&
50+
(values.customCodeFormat === "EAN13" || values.customCodeFormat === "EAN8") &&
51+
values.addonFormat === "None"
52+
)
4553
) {
4654
hidePropertyIn(defaultProperties, values, "enableFlat");
4755
}
4856

57+
// lastChar is only supported for EAN-13, and NOT when flat is enabled or addons are present
4958
if (
50-
values.codeFormat === "QRCode" ||
51-
values.codeFormat === "CODE128" ||
52-
(values.codeFormat === "Custom" && values.customCodeFormat !== "EAN13")
59+
!(
60+
values.codeFormat === "Custom" &&
61+
values.customCodeFormat === "EAN13" &&
62+
!values.enableFlat &&
63+
values.addonFormat === "None"
64+
)
5365
) {
5466
hidePropertyIn(defaultProperties, values, "lastChar");
5567
}
5668

69+
// EAN addons are only supported for EAN-13, EAN-8, and UPC
5770
if (
5871
values.codeFormat === "QRCode" ||
5972
values.codeFormat === "CODE128" ||
60-
(values.codeFormat === "Custom" && values.customCodeFormat !== "EAN13" && values.customCodeFormat !== "EAN8")
73+
(values.codeFormat === "Custom" &&
74+
values.customCodeFormat !== "EAN13" &&
75+
values.customCodeFormat !== "EAN8" &&
76+
values.customCodeFormat !== "UPC")
6177
) {
6278
hidePropertiesIn(defaultProperties, values, ["addonFormat", "addonValue", "addonSpacing"]);
6379
}
@@ -77,17 +93,32 @@ export function getProperties(values: BarcodeGeneratorPreviewProps, defaultPrope
7793
hidePropertyIn(defaultProperties, values, "enableMod43");
7894
}
7995

80-
if (values.qrImageCenter) {
81-
hidePropertiesIn(defaultProperties, values, ["qrImageX", "qrImageY"]);
96+
if (values.qrOverlayCenter) {
97+
hidePropertiesIn(defaultProperties, values, ["qrOverlayX", "qrOverlayY"]);
8298
}
8399

84100
if (values.codeFormat !== "Custom") {
85101
hidePropertiesIn(defaultProperties, values, ["customCodeFormat"]);
86102
}
87103

104+
if (!values.allowDownload) {
105+
hidePropertiesIn(defaultProperties, values, [
106+
"downloadButtonCaption",
107+
"downloadButtonAriaLabel",
108+
"downloadFileName",
109+
"buttonPosition"
110+
]);
111+
}
112+
88113
return defaultProperties;
89114
}
90115

116+
export function getPreview(_: StructurePreviewProps, _isDarkMode: boolean): StructurePreviewProps | null {
117+
// Return null to use the widget icon (BarcodeGenerator.icon.png or BarcodeGenerator.icon.dark.png)
118+
// based on the user's theme settings
119+
return null;
120+
}
121+
91122
export function check(_values: BarcodeGeneratorPreviewProps): Problem[] {
92123
const errors: Problem[] = [];
93124

@@ -120,36 +151,103 @@ export function check(_values: BarcodeGeneratorPreviewProps): Problem[] {
120151
return errors.concat(valueProblems);
121152
}
122153

123-
function getActiveFormat(values: BarcodeGeneratorPreviewProps): string {
154+
function getActiveFormat(values: BarcodeGeneratorPreviewProps): CodeFormatEnum | CustomCodeFormatEnum {
124155
if (values.codeFormat === "Custom") {
125156
return values.customCodeFormat || "CODE128";
126157
}
127158

128159
return values.codeFormat;
129160
}
130161

162+
function stripQuotes(value: string): string {
163+
// Remove leading/trailing quotes and whitespace from expression values
164+
let trimmed = value.trim();
165+
// Match and remove surrounding quotes (single or double)
166+
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
167+
trimmed = trimmed.slice(1, -1);
168+
}
169+
return trimmed;
170+
}
171+
172+
function isDynamicExpression(value: string): boolean {
173+
// Check if the value is a dynamic expression (attribute binding, variable, etc.)
174+
// Dynamic expressions start with $ or contain / paths or are empty
175+
return !value || value.startsWith("$") || value.includes("/");
176+
}
177+
178+
function getFormatHint(format: string): string {
179+
const hints: Record<string, string> = {
180+
EAN13: "EAN-13 requires 12 or 13 numeric digits",
181+
EAN8: "EAN-8 requires 7 or 8 numeric digits",
182+
UPC: "UPC requires 11 or 12 numeric digits",
183+
ITF14: "ITF-14 requires exactly 14 numeric digits",
184+
CODE39: "CODE39: uppercase A-Z, digits, space and - . $ / + % (max 43 chars)",
185+
CODE128: "CODE128: alphanumeric, no control characters (max 80 chars)",
186+
CODE93: "CODE93: alphanumeric, no control characters (max 47 chars)",
187+
MSI: "MSI: numeric only (max 30 digits)",
188+
pharmacode: "Pharmacode: numeric only (max 7 digits)",
189+
codabar: "Codabar: digits, A-D start/stop, and - $ : / . + (max 20 chars)",
190+
QRCode: "QR Code: any text (max 1200 chars recommended)"
191+
};
192+
return hints[format] || "";
193+
}
194+
131195
function validateCodeValues(values: BarcodeGeneratorPreviewProps): Problem[] {
132196
const problems: Problem[] = [];
133-
const val = values.codeValue ?? "";
134-
const addon = values.addonValue ?? "";
197+
const rawVal = values.codeValue ?? "";
198+
const rawAddon = values.addonValue ?? "";
135199
const format = getActiveFormat(values);
136200

137-
// Only validate static (design-time) values — if empty, skip (user may bind dynamically)
138-
if (!val) {
139-
// still validate addon if present
140-
} else {
141-
const result = validateBarcodeValue(format, val);
142-
if (!result.valid) {
143-
const msg = result.message || "Invalid barcode value for selected format.";
144-
problems.push({ property: "codeValue", severity: "warning", message: msg });
201+
// Add informational hint for dynamic expressions
202+
if (isDynamicExpression(rawVal) && rawVal) {
203+
const hint = getFormatHint(format);
204+
if (hint) {
205+
problems.push({
206+
property: "codeValue",
207+
severity: "warning",
208+
message: `Dynamic value provided. Ensure runtime value matches format: ${hint}`
209+
});
210+
}
211+
}
212+
213+
// Only validate static literal values, skip dynamic expressions (attribute bindings, variables, etc.)
214+
if (!isDynamicExpression(rawVal)) {
215+
const val = stripQuotes(rawVal);
216+
if (val) {
217+
const result = validateBarcodeValue(format, val);
218+
if (!result.valid) {
219+
const msg = result.message || "Invalid barcode value for selected format.";
220+
problems.push({ property: "codeValue", severity: "error", message: msg });
221+
}
145222
}
146223
}
147224

148-
// Validate addon value if visible
149-
const addonResult = validateAddonValue(values.addonFormat, addon);
150-
if (!addonResult.valid) {
151-
const msg = addonResult.message || "Invalid addon value.";
152-
problems.push({ property: "addonValue", severity: "warning", message: msg });
225+
// Validate addon value if visible and format is selected
226+
if (values.addonFormat !== "None") {
227+
// Add informational hint for dynamic addon expressions
228+
if (isDynamicExpression(rawAddon) && rawAddon) {
229+
const addonHint =
230+
values.addonFormat === "EAN5"
231+
? "EAN-5 addon requires exactly 5 numeric digits"
232+
: "EAN-2 addon requires exactly 2 numeric digits";
233+
problems.push({
234+
property: "addonValue",
235+
severity: "warning",
236+
message: `Dynamic addon value provided. Ensure runtime value matches format: ${addonHint}`
237+
});
238+
}
239+
240+
// Validate static addon values
241+
if (!isDynamicExpression(rawAddon)) {
242+
const addon = stripQuotes(rawAddon);
243+
if (addon) {
244+
const addonResult = validateAddonValue(values.addonFormat, addon);
245+
if (!addonResult.valid) {
246+
const msg = addonResult.message || "Invalid addon value.";
247+
problems.push({ property: "addonValue", severity: "error", message: msg });
248+
}
249+
}
250+
}
153251
}
154252

155253
return problems;
Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,46 @@
1+
import classNames from "classnames";
12
import { ReactElement } from "react";
3+
import { parseStyle } from "@mendix/widget-plugin-platform/preview/parse-style";
24
import { BarcodeGeneratorPreviewProps } from "../typings/BarcodeGeneratorProps";
3-
import BarcodePreviewSVG from "./assets/BarcodeGeneratorPreview.svg";
5+
import { DownloadIcon } from "./components/icons/DownloadIcon";
6+
import { BarcodePreview } from "./components/preview/BarcodePreview";
7+
import { QRCodePreview } from "./components/preview/QRCodePreview";
48

5-
export function preview(_props: BarcodeGeneratorPreviewProps): ReactElement {
6-
const doc = decodeURI(BarcodePreviewSVG);
9+
const defaultDownloadCaption = "Download";
10+
11+
function PreviewDownloadButton(props: BarcodeGeneratorPreviewProps): ReactElement | null {
12+
if (!props.allowDownload) {
13+
return null;
14+
}
15+
16+
return (
17+
<a className="mx-link" role="button" aria-label={props.downloadButtonAriaLabel || undefined} tabIndex={0}>
18+
<DownloadIcon /> {props.downloadButtonCaption || defaultDownloadCaption}
19+
</a>
20+
);
21+
}
22+
23+
export function preview(props: BarcodeGeneratorPreviewProps): ReactElement {
24+
const styles = parseStyle(props.style);
25+
const isQrCode = props.codeFormat === "QRCode";
26+
const downloadButton = <PreviewDownloadButton {...props} />;
727

828
return (
9-
<div className="barcode-generator-widget-preview">
10-
<img src={doc} alt="" />
29+
<div
30+
className={classNames(props.class, props.className, "barcode-generator", {
31+
"barcode-generator--as-card": props.showAsCard
32+
})}
33+
style={styles}
34+
>
35+
{isQrCode ? (
36+
<QRCodePreview {...props} downloadButton={downloadButton} />
37+
) : (
38+
<BarcodePreview {...props} downloadButton={downloadButton} />
39+
)}
1140
</div>
1241
);
1342
}
43+
44+
export function getPreviewCss(): string {
45+
return require("./ui/BarcodeGenerator.scss");
46+
}
4.84 KB
Loading
5.16 KB
Loading
14.2 KB
Loading
14.9 KB
Loading
Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,28 @@
1+
import classNames from "classnames";
12
import { ReactElement } from "react";
23
import { BarcodeGeneratorContainerProps } from "../typings/BarcodeGeneratorProps";
34
import { barcodeConfig } from "./config/Barcode.config";
4-
import { BarcodeContextProvider, useBarcodeConfig } from "./config/BarcodeContext";
55
import { QRCodeRenderer } from "./components/QRCode";
66
import { BarcodeRenderer } from "./components/Barcode";
77

88
import "./ui/BarcodeGenerator.scss";
99

10-
function BarcodeContainer({ tabIndex }: { tabIndex?: number }): ReactElement {
11-
const config = useBarcodeConfig();
12-
13-
return (
14-
<div className="barcode-generator" tabIndex={tabIndex}>
15-
{config.isQRCode ? <QRCodeRenderer /> : <BarcodeRenderer />}
16-
</div>
17-
);
18-
}
19-
2010
export default function BarcodeGenerator(props: BarcodeGeneratorContainerProps): ReactElement {
2111
const config = barcodeConfig(props);
2212

23-
if (!config.value) {
24-
return <span>No barcode value provided</span>;
13+
if (!config.codeValue) {
14+
return <span>{props.emptyMessage?.value || "No barcode value provided"}</span>;
2515
}
2616

2717
return (
28-
<BarcodeContextProvider config={config}>
29-
<BarcodeContainer tabIndex={props.tabIndex} />
30-
</BarcodeContextProvider>
18+
<div
19+
className={classNames(props.class, "barcode-generator", {
20+
"barcode-generator--as-card": props.showAsCard
21+
})}
22+
tabIndex={props.tabIndex}
23+
style={props.style}
24+
>
25+
{config.type === "qrcode" ? <QRCodeRenderer config={config} /> : <BarcodeRenderer config={config} />}
26+
</div>
3127
);
3228
}

0 commit comments

Comments
 (0)