@openlettermarketing/olc-react-sdk 2.1.6-beta.1 → 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 +76 -76
- package/build/index.js.map +1 -1
- package/build/types/src/components/TopNavigation/FieldValidationModal/index.d.ts +3 -2
- package/build/types/src/utils/message.d.ts +3 -3
- package/build/types/src/utils/template-builder.d.ts +10 -0
- package/build/types/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/TopNavigation/FieldValidationModal/index.tsx +16 -7
- package/src/components/TopNavigation/index.tsx +47 -25
- package/src/utils/message.ts +4 -4
- package/src/utils/template-builder.ts +57 -18
- package/version.js +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { InvalidField } from '../../../utils/template-builder';
|
|
3
2
|
import './styles.scss';
|
|
4
3
|
interface FieldValidationModalProps {
|
|
5
4
|
open: boolean;
|
|
6
|
-
invalidFields:
|
|
5
|
+
invalidFields: string[];
|
|
7
6
|
currentTheme?: string | null | undefined;
|
|
7
|
+
loading?: boolean;
|
|
8
8
|
handleClose: () => void;
|
|
9
|
+
handleContinue: () => void;
|
|
9
10
|
}
|
|
10
11
|
declare const FieldValidationModal: React.FC<FieldValidationModalProps>;
|
|
11
12
|
export default FieldValidationModal;
|
|
@@ -138,10 +138,10 @@ export declare const MESSAGES: {
|
|
|
138
138
|
readonly SUBMIT_BUTTON: "Save";
|
|
139
139
|
readonly FIELD_VALIDATION: {
|
|
140
140
|
readonly TITLE: "Invalid Fields";
|
|
141
|
-
readonly HEADING: "
|
|
141
|
+
readonly HEADING: "The following fields are not recognized";
|
|
142
142
|
readonly DESCRIPTION: (fieldNames: string) => string;
|
|
143
|
-
readonly
|
|
144
|
-
readonly
|
|
143
|
+
readonly CONTINUE_BUTTON: "Continue Saving";
|
|
144
|
+
readonly CANCEL_BUTTON: "Cancel";
|
|
145
145
|
};
|
|
146
146
|
};
|
|
147
147
|
readonly QR_CODE_MODAL: {
|
|
@@ -88,3 +88,13 @@ export interface InvalidField {
|
|
|
88
88
|
* @returns Array of objects containing invalid fields and their issues
|
|
89
89
|
*/
|
|
90
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
|
@@ -10,17 +10,16 @@ import DialogV2 from '../../GenericUIBlocks/Dialog/V2';
|
|
|
10
10
|
// Icons
|
|
11
11
|
import Warning from '../../../assets/images/modal-icons/warning';
|
|
12
12
|
|
|
13
|
-
// Types
|
|
14
|
-
import { InvalidField } from '../../../utils/template-builder';
|
|
15
|
-
|
|
16
13
|
// Styles
|
|
17
14
|
import './styles.scss';
|
|
18
15
|
|
|
19
16
|
interface FieldValidationModalProps {
|
|
20
17
|
open: boolean;
|
|
21
|
-
invalidFields:
|
|
18
|
+
invalidFields: string[];
|
|
22
19
|
currentTheme?: string | null | undefined;
|
|
20
|
+
loading?: boolean;
|
|
23
21
|
handleClose: () => void;
|
|
22
|
+
handleContinue: () => void;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
const modalStyles = {
|
|
@@ -38,10 +37,11 @@ const FieldValidationModal: React.FC<FieldValidationModalProps> = ({
|
|
|
38
37
|
open,
|
|
39
38
|
invalidFields,
|
|
40
39
|
currentTheme,
|
|
40
|
+
loading = false,
|
|
41
41
|
handleClose,
|
|
42
|
+
handleContinue,
|
|
42
43
|
}) => {
|
|
43
|
-
|
|
44
|
-
const fieldNames = [...invalidFields].reverse().map(item => `"${item.field}"`).join(', ');
|
|
44
|
+
const fieldNames = [...invalidFields].map(item => `"${item}"`).join(', ');
|
|
45
45
|
const description = MESSAGES.TEMPLATE.FIELD_VALIDATION.DESCRIPTION(fieldNames);
|
|
46
46
|
|
|
47
47
|
return currentTheme === 'v2' ? (
|
|
@@ -49,23 +49,32 @@ const FieldValidationModal: React.FC<FieldValidationModalProps> = ({
|
|
|
49
49
|
icon={<Warning fill="var(--primary-color)" />}
|
|
50
50
|
customStyles={modalStylesV2}
|
|
51
51
|
open={open}
|
|
52
|
+
loading={loading}
|
|
52
53
|
handleClose={handleClose}
|
|
53
54
|
title={MESSAGES.TEMPLATE.FIELD_VALIDATION.TITLE}
|
|
54
55
|
subHeading=""
|
|
55
56
|
description={description}
|
|
56
57
|
currentTheme="v2"
|
|
57
58
|
isGallery={false}
|
|
58
|
-
|
|
59
|
+
onSubmit={handleContinue}
|
|
60
|
+
submitText={MESSAGES.TEMPLATE.FIELD_VALIDATION.CONTINUE_BUTTON}
|
|
61
|
+
onCancel={handleClose}
|
|
62
|
+
cancelText={MESSAGES.TEMPLATE.FIELD_VALIDATION.CANCEL_BUTTON}
|
|
59
63
|
/>
|
|
60
64
|
) : (
|
|
61
65
|
<Dialog
|
|
62
66
|
icon={<Warning fill="var(--primary-color)" />}
|
|
63
67
|
customStyles={modalStyles}
|
|
64
68
|
open={open}
|
|
69
|
+
loading={loading}
|
|
65
70
|
handleClose={handleClose}
|
|
66
71
|
title={MESSAGES.TEMPLATE.FIELD_VALIDATION.TITLE}
|
|
67
72
|
subHeading=""
|
|
68
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}
|
|
69
78
|
/>
|
|
70
79
|
);
|
|
71
80
|
};
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
validateEmoji,
|
|
34
34
|
validateGSV,
|
|
35
35
|
validateTemplateFields,
|
|
36
|
-
|
|
36
|
+
validateAllowedTemplateFields,
|
|
37
37
|
} from '../../utils/template-builder';
|
|
38
38
|
import { addSafetyBordersToNonWindowProfessioanl } from '../../utils/templateSafetyBorders/professional';
|
|
39
39
|
import { addIdentifierAreaToProfessionalNonWindow } from '../../utils/templateIdentifierArea/professional';
|
|
@@ -145,7 +145,7 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
|
|
|
145
145
|
const [downloadingProof, setDownloaingProof] = useState<boolean>(false);
|
|
146
146
|
const [downloadingEnvelope, setDownloaingEnvelope] = useState<boolean>(false);
|
|
147
147
|
const [templateTitle, setTemplateTitle] = useState('');
|
|
148
|
-
const [invalidFields, setInvalidFields] = useState<
|
|
148
|
+
const [invalidFields, setInvalidFields] = useState<string[]>([]);
|
|
149
149
|
|
|
150
150
|
const { id } = useParams<{ id: string }>();
|
|
151
151
|
|
|
@@ -451,19 +451,6 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
|
|
|
451
451
|
return;
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
// Validate fields FIRST - before other validations
|
|
455
|
-
const fieldValidationResult = validateTemplateFields(jsonData.pages);
|
|
456
|
-
|
|
457
|
-
if (fieldValidationResult.length > 0) {
|
|
458
|
-
setInvalidFields(fieldValidationResult);
|
|
459
|
-
setIsShowModel((prev) => ({
|
|
460
|
-
...prev,
|
|
461
|
-
open: true,
|
|
462
|
-
model: 'field-validation'
|
|
463
|
-
}));
|
|
464
|
-
return; // Stop save process
|
|
465
|
-
}
|
|
466
|
-
|
|
467
454
|
const hasEmoji = validateEmoji(jsonData.pages);
|
|
468
455
|
|
|
469
456
|
if (hasEmoji) {
|
|
@@ -589,28 +576,61 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
|
|
|
589
576
|
}
|
|
590
577
|
};
|
|
591
578
|
|
|
579
|
+
const handleContinueAnyway = () => {
|
|
580
|
+
setIsShowModel((prev) => ({ ...prev, loading: true }));
|
|
581
|
+
handleSave();
|
|
582
|
+
};
|
|
583
|
+
|
|
592
584
|
const handleChangeModel = (
|
|
593
585
|
model: string = '',
|
|
594
586
|
loading: string | null = null
|
|
595
587
|
) => {
|
|
596
|
-
//
|
|
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 }})
|
|
597
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
|
+
|
|
598
613
|
const jsonData = store.toJSON();
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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);
|
|
604
625
|
setIsShowModel({
|
|
605
626
|
open: true,
|
|
606
627
|
model: 'field-validation',
|
|
607
|
-
loading: false
|
|
628
|
+
loading: false,
|
|
608
629
|
});
|
|
609
|
-
return;
|
|
630
|
+
return;
|
|
610
631
|
}
|
|
611
632
|
}
|
|
612
|
-
|
|
613
|
-
// If validation passed or not save modal, proceed normally
|
|
633
|
+
|
|
614
634
|
setIsShowModel((prev) => ({
|
|
615
635
|
...prev,
|
|
616
636
|
open: !prev.open,
|
|
@@ -953,8 +973,10 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
|
|
|
953
973
|
<FieldValidationModal
|
|
954
974
|
open={isShowModel.open}
|
|
955
975
|
invalidFields={invalidFields}
|
|
976
|
+
loading={isShowModel.loading}
|
|
956
977
|
currentTheme={currentTheme}
|
|
957
978
|
handleClose={() => handleChangeModel()}
|
|
979
|
+
handleContinue={handleContinueAnyway}
|
|
958
980
|
/>
|
|
959
981
|
)}
|
|
960
982
|
{/* Duplicate Template Modal */}
|
package/src/utils/message.ts
CHANGED
|
@@ -145,10 +145,10 @@ export const MESSAGES = {
|
|
|
145
145
|
SUBMIT_BUTTON: "Save",
|
|
146
146
|
FIELD_VALIDATION: {
|
|
147
147
|
TITLE: "Invalid Fields",
|
|
148
|
-
HEADING: "
|
|
149
|
-
DESCRIPTION: (fieldNames: string) => `
|
|
150
|
-
|
|
151
|
-
|
|
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
152
|
},
|
|
153
153
|
},
|
|
154
154
|
QR_CODE_MODAL: {
|
|
@@ -350,34 +350,53 @@ export const validateTemplateFields = (pages: any): InvalidField[] => {
|
|
|
350
350
|
page.children?.forEach((child: any) => {
|
|
351
351
|
if (child.type === 'text' && child.text) {
|
|
352
352
|
const text = child.text;
|
|
353
|
-
|
|
354
|
-
// Find
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
365
366
|
const isValidFormat = /^\{\{[A-Z0-9._]+\}\}$/.test(token);
|
|
366
|
-
|
|
367
|
+
|
|
367
368
|
if (!isValidFormat) {
|
|
368
369
|
const content = token.replace(/^\{+|\}+$/g, '');
|
|
369
|
-
const isDoubleBrace = /^\{\{
|
|
370
|
+
const isDoubleBrace = /^\{\{[\s\S]+\}\}$/.test(token);
|
|
370
371
|
const hasOnlyValidChars = /^[A-Za-z0-9._]+$/.test(content);
|
|
371
|
-
|
|
372
372
|
if (isDoubleBrace && hasOnlyValidChars && content !== content.toUpperCase()) {
|
|
373
|
-
//
|
|
373
|
+
// e.g. {{c.last_name}} — correct braces but lowercase content
|
|
374
374
|
invalidFields.push({ field: token, issue: 'NOT_UPPERCASE', pageIndex });
|
|
375
375
|
} else {
|
|
376
|
-
// Any other invalid format
|
|
377
376
|
invalidFields.push({ field: token, issue: 'INVALID_FORMAT', pageIndex });
|
|
378
377
|
}
|
|
379
378
|
}
|
|
380
|
-
}
|
|
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
|
+
}
|
|
381
400
|
}
|
|
382
401
|
});
|
|
383
402
|
});
|
|
@@ -389,3 +408,23 @@ export const validateTemplateFields = (pages: any): InvalidField[] => {
|
|
|
389
408
|
|
|
390
409
|
return uniqueFields;
|
|
391
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
|
+
};
|
package/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '2.1.6-beta.
|
|
1
|
+
export const SDK_VERSION = '2.1.6-beta.3';
|