@openlettermarketing/olc-react-sdk 2.1.6-beta.2 → 2.1.6-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +77 -77
- package/build/index.js.map +1 -1
- package/build/types/src/assets/images/modal-icons/warning.d.ts +3 -0
- package/build/types/src/components/GenericUIBlocks/Dialog/V2/index.d.ts +1 -0
- package/build/types/src/components/TopNavigation/FieldValidationModal/index.d.ts +12 -0
- package/build/types/src/utils/message.d.ts +7 -0
- package/build/types/src/utils/template-builder.d.ts +26 -0
- package/build/types/src/utils/templateIdentifierArea/triFold.d.ts +0 -2
- package/build/types/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/assets/images/modal-icons/warning.tsx +15 -0
- package/src/components/GenericUIBlocks/Dialog/V2/index.tsx +4 -2
- package/src/components/GenericUIBlocks/Dialog/V2/styles.scss +4 -0
- package/src/components/TopNavigation/FieldValidationModal/index.tsx +82 -0
- package/src/components/TopNavigation/FieldValidationModal/styles.scss +39 -0
- package/src/components/TopNavigation/index.tsx +66 -0
- package/src/utils/message.ts +8 -1
- package/src/utils/products.ts +1 -1
- package/src/utils/template-builder.ts +105 -3
- package/src/utils/templateIdentifierArea/triFold.ts +8 -26
- package/src/utils/templateRestrictedArea/triFold.ts +6 -8
- package/src/utils/templateSafetyBorders/triFold.ts +20 -27
- package/version.js +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './styles.scss';
|
|
3
|
+
interface FieldValidationModalProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
invalidFields: string[];
|
|
6
|
+
currentTheme?: string | null | undefined;
|
|
7
|
+
loading?: boolean;
|
|
8
|
+
handleClose: () => void;
|
|
9
|
+
handleContinue: () => void;
|
|
10
|
+
}
|
|
11
|
+
declare const FieldValidationModal: React.FC<FieldValidationModalProps>;
|
|
12
|
+
export default FieldValidationModal;
|
|
@@ -136,6 +136,13 @@ export declare const MESSAGES: {
|
|
|
136
136
|
readonly DOWNLOAD_ENVELOPE_BUTTON: "Download Envelope Proof";
|
|
137
137
|
readonly CANCEL_BUTTON: "Cancel";
|
|
138
138
|
readonly SUBMIT_BUTTON: "Save";
|
|
139
|
+
readonly FIELD_VALIDATION: {
|
|
140
|
+
readonly TITLE: "Invalid Fields";
|
|
141
|
+
readonly HEADING: "The following fields are not recognized";
|
|
142
|
+
readonly DESCRIPTION: (fieldNames: string) => string;
|
|
143
|
+
readonly CONTINUE_BUTTON: "Continue Saving";
|
|
144
|
+
readonly CANCEL_BUTTON: "Cancel";
|
|
145
|
+
};
|
|
139
146
|
};
|
|
140
147
|
readonly QR_CODE_MODAL: {
|
|
141
148
|
readonly CREATE_TITLE: "Create New QR Code";
|
|
@@ -72,3 +72,29 @@ export declare const extractCustomRistrictedBoxColor: (jsonData: any) => null;
|
|
|
72
72
|
export declare const validateGSV: (pages: any) => boolean;
|
|
73
73
|
export declare const validateEmoji: (pages: any) => boolean;
|
|
74
74
|
export declare const isValidQR: (pages: any) => boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Interface for invalid field validation results
|
|
77
|
+
*/
|
|
78
|
+
export interface InvalidField {
|
|
79
|
+
field: string;
|
|
80
|
+
issue: 'INVALID_FORMAT' | 'NOT_UPPERCASE';
|
|
81
|
+
pageIndex: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validates template fields for correct format and capitalization
|
|
85
|
+
* Returns array of invalid fields found in the template
|
|
86
|
+
*
|
|
87
|
+
* @param pages - Template pages array from store.toJSON()
|
|
88
|
+
* @returns Array of objects containing invalid fields and their issues
|
|
89
|
+
*/
|
|
90
|
+
export declare const validateTemplateFields: (pages: any) => InvalidField[];
|
|
91
|
+
/**
|
|
92
|
+
* Validates that all dynamic field tokens used in the template exist in the allowed keys list.
|
|
93
|
+
* Scans the entire serialized template JSON for {{...}} patterns and returns any tokens
|
|
94
|
+
* that are not present in the provided allowedKeys list.
|
|
95
|
+
*
|
|
96
|
+
* @param templateJSON - Full template JSON object from store.toJSON()
|
|
97
|
+
* @param allowedKeys - Array of allowed {{...}} token strings (predefined + custom fields)
|
|
98
|
+
* @returns Array of unrecognized token strings; empty array means all fields are valid
|
|
99
|
+
*/
|
|
100
|
+
export declare const validateAllowedTemplateFields: (templateJSON: any, allowedKeys: string[]) => string[];
|
package/build/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION: "2.1.6-beta.
|
|
1
|
+
export const SDK_VERSION: "2.1.6-beta.3";
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
const WarningIcon = (props: any) => {
|
|
4
|
+
const { fill = 'var(--primary-color)' } = props;
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<svg width="34" height="34" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
8
|
+
<path d="M24 4L4 44H44L24 4Z" stroke={fill} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
9
|
+
<path d="M24 18V28" stroke={fill} strokeWidth="2" strokeLinecap="round"/>
|
|
10
|
+
<circle cx="24" cy="36" r="2" fill={fill}/>
|
|
11
|
+
</svg>
|
|
12
|
+
);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default WarningIcon;
|
|
@@ -27,6 +27,7 @@ interface DialogProps {
|
|
|
27
27
|
isGallery?: boolean;
|
|
28
28
|
currentTheme?: string | null | undefined;
|
|
29
29
|
isQRCode?: boolean;
|
|
30
|
+
hideButtons?: boolean;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const buttonStyles: CSSProperties = {
|
|
@@ -80,7 +81,8 @@ const DialogV2: React.FC<DialogProps> = ({
|
|
|
80
81
|
children = [],
|
|
81
82
|
isGallery = false,
|
|
82
83
|
currentTheme = "default",
|
|
83
|
-
isQRCode = false
|
|
84
|
+
isQRCode = false,
|
|
85
|
+
hideButtons = false
|
|
84
86
|
}) => {
|
|
85
87
|
const contentAdjust = submitText.length > 6 ? "fit-content" : "100px";
|
|
86
88
|
|
|
@@ -204,7 +206,7 @@ const DialogV2: React.FC<DialogProps> = ({
|
|
|
204
206
|
)}
|
|
205
207
|
{children}
|
|
206
208
|
<div className="confirm-modal-footer">
|
|
207
|
-
{!isGallery &&
|
|
209
|
+
{!isGallery && !hideButtons &&
|
|
208
210
|
<>
|
|
209
211
|
<button onClick={onSubmit} disabled={loading}>
|
|
210
212
|
{loading ? <CircularProgress style={progressStyles} /> : submitText}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
// Utils
|
|
4
|
+
import { MESSAGES } from '../../../utils/message';
|
|
5
|
+
|
|
6
|
+
// Components
|
|
7
|
+
import Dialog from '../../GenericUIBlocks/Dialog';
|
|
8
|
+
import DialogV2 from '../../GenericUIBlocks/Dialog/V2';
|
|
9
|
+
|
|
10
|
+
// Icons
|
|
11
|
+
import Warning from '../../../assets/images/modal-icons/warning';
|
|
12
|
+
|
|
13
|
+
// Styles
|
|
14
|
+
import './styles.scss';
|
|
15
|
+
|
|
16
|
+
interface FieldValidationModalProps {
|
|
17
|
+
open: boolean;
|
|
18
|
+
invalidFields: string[];
|
|
19
|
+
currentTheme?: string | null | undefined;
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
handleClose: () => void;
|
|
22
|
+
handleContinue: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const modalStyles = {
|
|
26
|
+
maxWidth: '600px',
|
|
27
|
+
minHeight: '300px',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const modalStylesV2 = {
|
|
31
|
+
maxWidth: '600px',
|
|
32
|
+
minHeight: '300px',
|
|
33
|
+
padding: '40px',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const FieldValidationModal: React.FC<FieldValidationModalProps> = ({
|
|
37
|
+
open,
|
|
38
|
+
invalidFields,
|
|
39
|
+
currentTheme,
|
|
40
|
+
loading = false,
|
|
41
|
+
handleClose,
|
|
42
|
+
handleContinue,
|
|
43
|
+
}) => {
|
|
44
|
+
const fieldNames = [...invalidFields].map(item => `"${item}"`).join(', ');
|
|
45
|
+
const description = MESSAGES.TEMPLATE.FIELD_VALIDATION.DESCRIPTION(fieldNames);
|
|
46
|
+
|
|
47
|
+
return currentTheme === 'v2' ? (
|
|
48
|
+
<DialogV2
|
|
49
|
+
icon={<Warning fill="var(--primary-color)" />}
|
|
50
|
+
customStyles={modalStylesV2}
|
|
51
|
+
open={open}
|
|
52
|
+
loading={loading}
|
|
53
|
+
handleClose={handleClose}
|
|
54
|
+
title={MESSAGES.TEMPLATE.FIELD_VALIDATION.TITLE}
|
|
55
|
+
subHeading=""
|
|
56
|
+
description={description}
|
|
57
|
+
currentTheme="v2"
|
|
58
|
+
isGallery={false}
|
|
59
|
+
onSubmit={handleContinue}
|
|
60
|
+
submitText={MESSAGES.TEMPLATE.FIELD_VALIDATION.CONTINUE_BUTTON}
|
|
61
|
+
onCancel={handleClose}
|
|
62
|
+
cancelText={MESSAGES.TEMPLATE.FIELD_VALIDATION.CANCEL_BUTTON}
|
|
63
|
+
/>
|
|
64
|
+
) : (
|
|
65
|
+
<Dialog
|
|
66
|
+
icon={<Warning fill="var(--primary-color)" />}
|
|
67
|
+
customStyles={modalStyles}
|
|
68
|
+
open={open}
|
|
69
|
+
loading={loading}
|
|
70
|
+
handleClose={handleClose}
|
|
71
|
+
title={MESSAGES.TEMPLATE.FIELD_VALIDATION.TITLE}
|
|
72
|
+
subHeading=""
|
|
73
|
+
description={description}
|
|
74
|
+
onSubmit={handleContinue}
|
|
75
|
+
submitText={MESSAGES.TEMPLATE.FIELD_VALIDATION.CONTINUE_BUTTON}
|
|
76
|
+
onCancel={handleClose}
|
|
77
|
+
cancelText={MESSAGES.TEMPLATE.FIELD_VALIDATION.CANCEL_BUTTON}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default FieldValidationModal;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.field-validation-content {
|
|
2
|
+
margin-top: 20px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.field-validation-list {
|
|
6
|
+
max-height: 300px;
|
|
7
|
+
overflow-y: auto;
|
|
8
|
+
padding: 10px;
|
|
9
|
+
background-color: #f9f9f9;
|
|
10
|
+
border-radius: 4px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.field-validation-item {
|
|
14
|
+
margin-bottom: 15px;
|
|
15
|
+
padding: 12px;
|
|
16
|
+
background-color: #fff;
|
|
17
|
+
border-left: 3px solid #ff9800;
|
|
18
|
+
border-radius: 4px;
|
|
19
|
+
|
|
20
|
+
.field-name {
|
|
21
|
+
font-weight: 600;
|
|
22
|
+
color: #333;
|
|
23
|
+
margin-bottom: 5px;
|
|
24
|
+
font-family: 'Courier New', monospace;
|
|
25
|
+
font-size: 14px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.field-error {
|
|
29
|
+
color: #666;
|
|
30
|
+
font-size: 13px;
|
|
31
|
+
margin-bottom: 4px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.field-location {
|
|
35
|
+
color: #999;
|
|
36
|
+
font-size: 12px;
|
|
37
|
+
font-style: italic;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -20,6 +20,7 @@ import { searchAndAdvanceChange } from '../../redux/actions/templateActions';
|
|
|
20
20
|
import SaveTemplateModel from './SaveTemplateModel';
|
|
21
21
|
import ConfirmNavigateDialog from './ConfirmNavigateDialog';
|
|
22
22
|
import EditTemplateNameModel from './EditTemplateNameModel';
|
|
23
|
+
import FieldValidationModal from './FieldValidationModal';
|
|
23
24
|
|
|
24
25
|
// Utils
|
|
25
26
|
import {
|
|
@@ -31,6 +32,8 @@ import {
|
|
|
31
32
|
removeBracketsFromRPL,
|
|
32
33
|
validateEmoji,
|
|
33
34
|
validateGSV,
|
|
35
|
+
validateTemplateFields,
|
|
36
|
+
validateAllowedTemplateFields,
|
|
34
37
|
} from '../../utils/template-builder';
|
|
35
38
|
import { addSafetyBordersToNonWindowProfessioanl } from '../../utils/templateSafetyBorders/professional';
|
|
36
39
|
import { addIdentifierAreaToProfessionalNonWindow } from '../../utils/templateIdentifierArea/professional';
|
|
@@ -142,6 +145,7 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
|
|
|
142
145
|
const [downloadingProof, setDownloaingProof] = useState<boolean>(false);
|
|
143
146
|
const [downloadingEnvelope, setDownloaingEnvelope] = useState<boolean>(false);
|
|
144
147
|
const [templateTitle, setTemplateTitle] = useState('');
|
|
148
|
+
const [invalidFields, setInvalidFields] = useState<string[]>([]);
|
|
145
149
|
|
|
146
150
|
const { id } = useParams<{ id: string }>();
|
|
147
151
|
|
|
@@ -572,10 +576,61 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
|
|
|
572
576
|
}
|
|
573
577
|
};
|
|
574
578
|
|
|
579
|
+
const handleContinueAnyway = () => {
|
|
580
|
+
setIsShowModel((prev) => ({ ...prev, loading: true }));
|
|
581
|
+
handleSave();
|
|
582
|
+
};
|
|
583
|
+
|
|
575
584
|
const handleChangeModel = (
|
|
576
585
|
model: string = '',
|
|
577
586
|
loading: string | null = null
|
|
578
587
|
) => {
|
|
588
|
+
// When opening the save modal, validate all {{...}} tokens against the allowed list
|
|
589
|
+
// and also catch any malformed/incomplete brace patterns (e.g. {{C.ZIP_COD without closing }})
|
|
590
|
+
if (model === 'save' && templateType === 'json') {
|
|
591
|
+
const tokenPattern = /\{\{[^{}]+\}\}/g;
|
|
592
|
+
|
|
593
|
+
// Flatten v2 custom field sections (each section has a .fields array)
|
|
594
|
+
const flattenedCustomFieldsV2 = (customFieldsV2 as any[]).length > 0
|
|
595
|
+
? (customFieldsV2 as any[]).flatMap((section: { fields: any }) => section.fields)
|
|
596
|
+
: [];
|
|
597
|
+
|
|
598
|
+
const allAllowedFields = [
|
|
599
|
+
...defaultFields,
|
|
600
|
+
...customFields, // v1 custom fields from API
|
|
601
|
+
...flattenedCustomFieldsV2, // v2 custom fields from API (flattened)
|
|
602
|
+
...Object.values(dynamicFields),
|
|
603
|
+
...defaultSenderFields,
|
|
604
|
+
...defaultPropertyFields,
|
|
605
|
+
...defaultMiscFields,
|
|
606
|
+
];
|
|
607
|
+
const allowedKeys = Array.from(new Set(
|
|
608
|
+
allAllowedFields.flatMap((field: any) =>
|
|
609
|
+
(field?.key || '').match(tokenPattern) || []
|
|
610
|
+
)
|
|
611
|
+
));
|
|
612
|
+
|
|
613
|
+
const jsonData = store.toJSON();
|
|
614
|
+
|
|
615
|
+
// Catch complete tokens not in the allowed list
|
|
616
|
+
const unrecognizedFields = validateAllowedTemplateFields(jsonData, allowedKeys);
|
|
617
|
+
|
|
618
|
+
// Catch malformed/incomplete patterns (e.g. {{C.ZIP_COD with no closing }})
|
|
619
|
+
const formatErrors = validateTemplateFields(jsonData.pages).map((f) => f.field);
|
|
620
|
+
|
|
621
|
+
const allInvalid = Array.from(new Set([...unrecognizedFields, ...formatErrors]));
|
|
622
|
+
|
|
623
|
+
if (allInvalid.length > 0) {
|
|
624
|
+
setInvalidFields(allInvalid);
|
|
625
|
+
setIsShowModel({
|
|
626
|
+
open: true,
|
|
627
|
+
model: 'field-validation',
|
|
628
|
+
loading: false,
|
|
629
|
+
});
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
579
634
|
setIsShowModel((prev) => ({
|
|
580
635
|
...prev,
|
|
581
636
|
open: !prev.open,
|
|
@@ -913,6 +968,17 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
|
|
|
913
968
|
currentTheme={currentTheme}
|
|
914
969
|
/>
|
|
915
970
|
)}
|
|
971
|
+
{/* Field Validation Modal */}
|
|
972
|
+
{isShowModel.open && isShowModel.model === 'field-validation' && (
|
|
973
|
+
<FieldValidationModal
|
|
974
|
+
open={isShowModel.open}
|
|
975
|
+
invalidFields={invalidFields}
|
|
976
|
+
loading={isShowModel.loading}
|
|
977
|
+
currentTheme={currentTheme}
|
|
978
|
+
handleClose={() => handleChangeModel()}
|
|
979
|
+
handleContinue={handleContinueAnyway}
|
|
980
|
+
/>
|
|
981
|
+
)}
|
|
916
982
|
{/* Duplicate Template Modal */}
|
|
917
983
|
<DuplicateTemplateModal
|
|
918
984
|
open={isShowModel.open && isShowModel.model === 'duplicate'}
|
package/src/utils/message.ts
CHANGED
|
@@ -142,7 +142,14 @@ export const MESSAGES = {
|
|
|
142
142
|
DOWNLOAD_PROOF_BUTTON: "Download Mailer Proof",
|
|
143
143
|
DOWNLOAD_ENVELOPE_BUTTON: "Download Envelope Proof",
|
|
144
144
|
CANCEL_BUTTON: "Cancel",
|
|
145
|
-
SUBMIT_BUTTON: "Save"
|
|
145
|
+
SUBMIT_BUTTON: "Save",
|
|
146
|
+
FIELD_VALIDATION: {
|
|
147
|
+
TITLE: "Invalid Fields",
|
|
148
|
+
HEADING: "The following fields are not recognized",
|
|
149
|
+
DESCRIPTION: (fieldNames: string) => `The following field(s) ${fieldNames} are not recognized.`,
|
|
150
|
+
CONTINUE_BUTTON: "Continue Saving",
|
|
151
|
+
CANCEL_BUTTON: "Cancel",
|
|
152
|
+
},
|
|
146
153
|
},
|
|
147
154
|
QR_CODE_MODAL: {
|
|
148
155
|
CREATE_TITLE: "Create New QR Code",
|
package/src/utils/products.ts
CHANGED
|
@@ -84,10 +84,10 @@ export const selfMailerProduct = {
|
|
|
84
84
|
],
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
|
-
id: "11", defaultSize: "
|
|
87
|
+
id: "11", defaultSize: "12x9", title: "Self Mailer", productType: "Tri-Fold Self-Mailers", hasEnvelope: false, paper: '80# text', label: '8.5x11',
|
|
88
88
|
finish: 'Glass Coated', size: [{
|
|
89
89
|
id: "11",
|
|
90
|
-
size: "
|
|
90
|
+
size: "12x9",
|
|
91
91
|
label: "8.5x11"
|
|
92
92
|
}]
|
|
93
93
|
},
|
|
@@ -325,4 +325,106 @@ export const isValidQR = (pages: any) => {
|
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
return true;
|
|
328
|
-
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Interface for invalid field validation results
|
|
332
|
+
*/
|
|
333
|
+
export interface InvalidField {
|
|
334
|
+
field: string;
|
|
335
|
+
issue: 'INVALID_FORMAT' | 'NOT_UPPERCASE';
|
|
336
|
+
pageIndex: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Validates template fields for correct format and capitalization
|
|
341
|
+
* Returns array of invalid fields found in the template
|
|
342
|
+
*
|
|
343
|
+
* @param pages - Template pages array from store.toJSON()
|
|
344
|
+
* @returns Array of objects containing invalid fields and their issues
|
|
345
|
+
*/
|
|
346
|
+
export const validateTemplateFields = (pages: any): InvalidField[] => {
|
|
347
|
+
const invalidFields: InvalidField[] = [];
|
|
348
|
+
|
|
349
|
+
pages.forEach((page: any, pageIndex: number) => {
|
|
350
|
+
page.children?.forEach((child: any) => {
|
|
351
|
+
if (child.type === 'text' && child.text) {
|
|
352
|
+
const text = child.text;
|
|
353
|
+
|
|
354
|
+
// ── Step 1 - Find ALL complete brace expressions: anything from the first { to the last } on the same "group", including spaces inside.
|
|
355
|
+
const completePattern = /\{+[^{}]*\}+/g;
|
|
356
|
+
let match: RegExpExecArray | null;
|
|
357
|
+
|
|
358
|
+
// Track which character ranges are already covered so Steps 2 & 3 - don't double-report sub-strings of a complete match.
|
|
359
|
+
const coveredRanges: Array<[number, number]> = [];
|
|
360
|
+
|
|
361
|
+
const completeExec = new RegExp(completePattern.source, 'g');
|
|
362
|
+
while ((match = completeExec.exec(text)) !== null) {
|
|
363
|
+
coveredRanges.push([match.index, match.index + match[0].length]);
|
|
364
|
+
const token = match[0];
|
|
365
|
+
// Valid: EXACTLY {{UPPERCASE_CONTENT}} — no spaces, no lowercase
|
|
366
|
+
const isValidFormat = /^\{\{[A-Z0-9._]+\}\}$/.test(token);
|
|
367
|
+
|
|
368
|
+
if (!isValidFormat) {
|
|
369
|
+
const content = token.replace(/^\{+|\}+$/g, '');
|
|
370
|
+
const isDoubleBrace = /^\{\{[\s\S]+\}\}$/.test(token);
|
|
371
|
+
const hasOnlyValidChars = /^[A-Za-z0-9._]+$/.test(content);
|
|
372
|
+
if (isDoubleBrace && hasOnlyValidChars && content !== content.toUpperCase()) {
|
|
373
|
+
// e.g. {{c.last_name}} — correct braces but lowercase content
|
|
374
|
+
invalidFields.push({ field: token, issue: 'NOT_UPPERCASE', pageIndex });
|
|
375
|
+
} else {
|
|
376
|
+
invalidFields.push({ field: token, issue: 'INVALID_FORMAT', pageIndex });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Helper: returns true if the character at `idx` is inside a covered range
|
|
382
|
+
const isCovered = (idx: number): boolean =>
|
|
383
|
+
coveredRanges.some(([s, e]) => idx >= s && idx < e);
|
|
384
|
+
|
|
385
|
+
// ── Step 2 ─ Find incomplete patterns that have opening brace(s) but NO closing brace.e.g. "{C.LAST_NAME" or "{{C.LAST_NAME" at end of text.
|
|
386
|
+
const missingClosingExec = /\{+[^{}]*/g;
|
|
387
|
+
while ((match = missingClosingExec.exec(text)) !== null) {
|
|
388
|
+
if (!isCovered(match.index) && match[0].trim().length > 0) {
|
|
389
|
+
invalidFields.push({ field: match[0], issue: 'INVALID_FORMAT', pageIndex });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── Step 3 ─ Find incomplete patterns that have closing brace(s) but NO opening brace. e.g. "C.LAST_NAME}" or "C.LAST_NAME}}".
|
|
394
|
+
const missingOpeningExec = /(?<!\{)[A-Z0-9][A-Z0-9._\s]*\}+/g;
|
|
395
|
+
while ((match = missingOpeningExec.exec(text)) !== null) {
|
|
396
|
+
if (!isCovered(match.index) && match[0].trim().length > 0) {
|
|
397
|
+
invalidFields.push({ field: match[0].trim(), issue: 'INVALID_FORMAT', pageIndex });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Remove duplicates
|
|
405
|
+
const uniqueFields = invalidFields.filter((item: InvalidField, index: number, self: InvalidField[]) =>
|
|
406
|
+
index === self.findIndex((t: InvalidField) => t.field === item.field && t.issue === item.issue)
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
return uniqueFields;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Validates that all dynamic field tokens used in the template exist in the allowed keys list.
|
|
414
|
+
* Scans the entire serialized template JSON for {{...}} patterns and returns any tokens
|
|
415
|
+
* that are not present in the provided allowedKeys list.
|
|
416
|
+
*
|
|
417
|
+
* @param templateJSON - Full template JSON object from store.toJSON()
|
|
418
|
+
* @param allowedKeys - Array of allowed {{...}} token strings (predefined + custom fields)
|
|
419
|
+
* @returns Array of unrecognized token strings; empty array means all fields are valid
|
|
420
|
+
*/
|
|
421
|
+
export const validateAllowedTemplateFields = (
|
|
422
|
+
templateJSON: any,
|
|
423
|
+
allowedKeys: string[]
|
|
424
|
+
): string[] => {
|
|
425
|
+
const templateStr = JSON.stringify(templateJSON);
|
|
426
|
+
const pattern = /\{\{[^{}]+\}\}/g;
|
|
427
|
+
const matches = templateStr.match(pattern) || [];
|
|
428
|
+
const unique = Array.from(new Set(matches));
|
|
429
|
+
return unique.filter(token => !allowedKeys.includes(token));
|
|
430
|
+
};
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { DPI } from "../constants";
|
|
2
|
-
|
|
3
1
|
// Define the type for the element being added
|
|
4
2
|
interface Element {
|
|
5
3
|
id: string;
|
|
@@ -45,8 +43,6 @@ interface Page {
|
|
|
45
43
|
|
|
46
44
|
// Define the type for the store
|
|
47
45
|
interface Store {
|
|
48
|
-
width: number;
|
|
49
|
-
height: number;
|
|
50
46
|
pages: Page[];
|
|
51
47
|
history: {
|
|
52
48
|
clear: () => void;
|
|
@@ -94,30 +90,16 @@ const createTextElement = (
|
|
|
94
90
|
|
|
95
91
|
export const addIdentifierAreaToTriFold = (store: Store): void => {
|
|
96
92
|
const page = store.pages[0];
|
|
97
|
-
const foldSectionHeight = store.height / 3;
|
|
98
|
-
const restrictedAreaHeight = 2.375 * DPI;
|
|
99
|
-
const restrictedAreaY = store.height - restrictedAreaHeight - 0.125 * DPI - foldSectionHeight;
|
|
100
|
-
const identifierY = restrictedAreaY + restrictedAreaHeight - 12;
|
|
101
|
-
|
|
102
|
-
// Keep the original right-side alignment behavior, but make it responsive to page width.
|
|
103
|
-
const contPlaceholderX = store.width - 185;
|
|
104
|
-
const contIdX = store.width - 177;
|
|
105
|
-
const sequencePlaceholderX = store.width - 149;
|
|
106
|
-
const sequenceX = store.width - 140;
|
|
107
|
-
const templatePlaceholderX = store.width - 114;
|
|
108
|
-
const templateIdX = store.width - 101.4;
|
|
109
|
-
const orderPlaceholderX = store.width - 72.4;
|
|
110
|
-
const orderIdX = store.width - 61.4;
|
|
111
93
|
|
|
112
94
|
const elements: Element[] = [
|
|
113
|
-
createTextElement("cont-id-placeholder", "C:",
|
|
114
|
-
createTextElement("contId", "0000001",
|
|
115
|
-
createTextElement("sequence-id-placeholder", "| S:",
|
|
116
|
-
createTextElement("sequence", "0000001",
|
|
117
|
-
createTextElement("template-id-placeholder", "| T:",
|
|
118
|
-
createTextElement("templateId", "0001034",
|
|
119
|
-
createTextElement("order-id-placeholder", "| O:",
|
|
120
|
-
createTextElement("orderId", "0000127",
|
|
95
|
+
createTextElement("cont-id-placeholder", "C:", 679, 739, 11),
|
|
96
|
+
createTextElement("contId", "0000001", 687, 739, 30),
|
|
97
|
+
createTextElement("sequence-id-placeholder", "| S:", 715, 739, 12),
|
|
98
|
+
createTextElement("sequence", "0000001", 724, 739, 29),
|
|
99
|
+
createTextElement("template-id-placeholder", "| T:", 750, 739, 14),
|
|
100
|
+
createTextElement("templateId", "0001034", 762.6, 739, 29),
|
|
101
|
+
createTextElement("order-id-placeholder", "| O:", 791.6, 739, 11),
|
|
102
|
+
createTextElement("orderId", "0000127", 802.6, 739, 29),
|
|
121
103
|
];
|
|
122
104
|
|
|
123
105
|
elements.forEach(element => page.addElement(element));
|
|
@@ -85,8 +85,6 @@ interface Store {
|
|
|
85
85
|
|
|
86
86
|
export const addRestrictedAreaToTriFold = (store: Store, size: [number, number], barcodeSrc: string): void => {
|
|
87
87
|
const page = store.pages[0];
|
|
88
|
-
const foldSectionHeight = store.height / 3;
|
|
89
|
-
const indiciaLeftShift = 8;
|
|
90
88
|
|
|
91
89
|
const elements: Element[] = [
|
|
92
90
|
{
|
|
@@ -100,7 +98,7 @@ export const addRestrictedAreaToTriFold = (store: Store, size: [number, number],
|
|
|
100
98
|
alwaysOnTop: true,
|
|
101
99
|
showInExport: true,
|
|
102
100
|
x: store.width - size[0] * DPI - 0.15 * DPI,
|
|
103
|
-
y: store.height - size[1] * DPI - 0.125 * DPI -
|
|
101
|
+
y: store.height - size[1] * DPI - 0.125 * DPI - 4 * DPI,
|
|
104
102
|
width: size[0] * DPI,
|
|
105
103
|
height: size[1] * DPI,
|
|
106
104
|
rotation: 0,
|
|
@@ -139,7 +137,7 @@ export const addRestrictedAreaToTriFold = (store: Store, size: [number, number],
|
|
|
139
137
|
alwaysOnTop: true,
|
|
140
138
|
showInExport: true,
|
|
141
139
|
x: store.width - size[0] * DPI - 0.15 * DPI + 5,
|
|
142
|
-
y: store.height - size[1] * DPI - 0.125 * DPI + 20 -
|
|
140
|
+
y: store.height - size[1] * DPI - 0.125 * DPI + 20 - 4 * DPI,
|
|
143
141
|
width: 240,
|
|
144
142
|
height: 20,
|
|
145
143
|
rotation: 0,
|
|
@@ -190,8 +188,8 @@ export const addRestrictedAreaToTriFold = (store: Store, size: [number, number],
|
|
|
190
188
|
removable: false,
|
|
191
189
|
alwaysOnTop: true,
|
|
192
190
|
showInExport: true,
|
|
193
|
-
x: store.width - 111 - 0.15 * DPI
|
|
194
|
-
y: store.height - size[1] * DPI + 10 -
|
|
191
|
+
x: store.width - 111 - 0.15 * DPI,
|
|
192
|
+
y: store.height - size[1] * DPI + 10 - 4 * DPI,
|
|
195
193
|
width: 111,
|
|
196
194
|
height: 40,
|
|
197
195
|
rotation: 0,
|
|
@@ -242,7 +240,7 @@ export const addRestrictedAreaToTriFold = (store: Store, size: [number, number],
|
|
|
242
240
|
removable: false,
|
|
243
241
|
alwaysOnTop: true,
|
|
244
242
|
showInExport: true,
|
|
245
|
-
y: store.height - 28 - size[1] * DPI + 100 -
|
|
243
|
+
y: store.height - 28 - size[1] * DPI + 100 - 4 * DPI,
|
|
246
244
|
x: store.width - size[0] * DPI - 0.15 * DPI + 5,
|
|
247
245
|
width: size[0] * DPI - 23,
|
|
248
246
|
height: 15,
|
|
@@ -284,7 +282,7 @@ export const addRestrictedAreaToTriFold = (store: Store, size: [number, number],
|
|
|
284
282
|
alwaysOnTop: true,
|
|
285
283
|
showInExport: true,
|
|
286
284
|
x: store.width - size[0] * DPI - 0.15 * DPI + 5,
|
|
287
|
-
y: store.height - size[1] * DPI - 0.125 * DPI + 120 -
|
|
285
|
+
y: store.height - size[1] * DPI - 0.125 * DPI + 120 - 4 * DPI,
|
|
288
286
|
width: 240,
|
|
289
287
|
height: 20,
|
|
290
288
|
rotation: 0,
|