@openlettermarketing/olc-react-sdk 2.1.5 → 2.1.6-beta.1

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.
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ declare const WarningIcon: (props: any) => React.JSX.Element;
3
+ export default WarningIcon;
@@ -17,6 +17,7 @@ interface DialogProps {
17
17
  isGallery?: boolean;
18
18
  currentTheme?: string | null | undefined;
19
19
  isQRCode?: boolean;
20
+ hideButtons?: boolean;
20
21
  }
21
22
  declare const DialogV2: React.FC<DialogProps>;
22
23
  export default DialogV2;
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { InvalidField } from '../../../utils/template-builder';
3
+ import './styles.scss';
4
+ interface FieldValidationModalProps {
5
+ open: boolean;
6
+ invalidFields: InvalidField[];
7
+ currentTheme?: string | null | undefined;
8
+ handleClose: () => void;
9
+ }
10
+ declare const FieldValidationModal: React.FC<FieldValidationModalProps>;
11
+ 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: "Please fix the following field errors";
142
+ readonly DESCRIPTION: (fieldNames: string) => string;
143
+ readonly INVALID_FORMAT: "is incorrect";
144
+ readonly NOT_UPPERCASE: "is incorrect";
145
+ };
139
146
  };
140
147
  readonly QR_CODE_MODAL: {
141
148
  readonly CREATE_TITLE: "Create New QR Code";
@@ -72,3 +72,19 @@ 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[];
@@ -1 +1 @@
1
- export const SDK_VERSION: "2.1.5";
1
+ export const SDK_VERSION: "2.1.6-beta.1";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openlettermarketing/olc-react-sdk",
3
3
  "private": false,
4
- "version": "2.1.5",
4
+ "version": "2.1.6-beta.1",
5
5
  "type": "module",
6
6
  "description": "Simplify template builder integration for any product.",
7
7
  "main": "build/index.js",
@@ -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;
@@ -407,7 +407,9 @@ const CreateTemplateV2: React.FC<CreateTemplateV2Props> = ({
407
407
  <div className='size-selection-container'>
408
408
  <Typography style={descriptionStyles}>Design Format</Typography>
409
409
  <div className='radio-buttons-container'>
410
- {product?.size?.map((type: any) => (
410
+ {product?.size
411
+ ?.filter((type: any) => !restrictedSet.has(String(type.id)))
412
+ .map((type: any) => (
411
413
  <label key={type.id} className={`radio-button-wrapper ${selectedRPLType === type.id ? 'selected' : ''}`}>
412
414
  <input type="radio" name="postcardSize" value={type.id} checked={selectedRPLType === type.id} onChange={() => handleRealPennedLetter(type, product)} className="radio-input" />
413
415
  <span className="radio-button"></span>
@@ -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}
@@ -223,6 +223,10 @@
223
223
  gap: 12px;
224
224
  margin-top: 16px;
225
225
 
226
+ &:empty {
227
+ margin-top: 0;
228
+ }
229
+
226
230
  button{
227
231
  min-height: 50px;
228
232
  font-size: 16px;
@@ -625,11 +625,11 @@ const CustomQRCode = {
625
625
  const handleEditQRCode = async (qrCode: any) => {
626
626
  setIsEditing(true);
627
627
  setSelectedQR(qrCode);
628
- dispatch(setQrUrl(qrCode.url));
629
- dispatch(setUtmSource(qrCode.utmSource || 'direct mail'));
630
- dispatch(setUtmMedium(qrCode.utmMedium || 'QR Code'));
631
- dispatch(setUtmCampaignName(qrCode.utm_campaign_name || ''));
632
- dispatch(setCustomUtms(qrCode.custom_utms || {}));
628
+ dispatch(setQrUrl(qrCode.url ?? ''));
629
+ dispatch(setUtmSource(qrCode.utm_source ?? ''));
630
+ dispatch(setUtmMedium(qrCode.utm_medium ?? ''));
631
+ dispatch(setUtmCampaignName(qrCode.utm_campaign_name ?? ''));
632
+ dispatch(setCustomUtms(qrCode.custom_utms ?? {}));
633
633
 
634
634
  handleDialogChange('qr-modal');
635
635
  setIsActionsOpen(null);
@@ -678,8 +678,8 @@ const CustomQRCode = {
678
678
  name: utmCampaignName,
679
679
  url: url,
680
680
  qrImagePath: uploadedFile,
681
- utm_source: utmSource,
682
- utm_medium: utmMedium,
681
+ utm_source: utmSource || null,
682
+ utm_medium: utmMedium || null,
683
683
  utm_campaign_name: utmCampaignName,
684
684
  custom_utms: customUtms,
685
685
  },
@@ -0,0 +1,73 @@
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
+ // Types
14
+ import { InvalidField } from '../../../utils/template-builder';
15
+
16
+ // Styles
17
+ import './styles.scss';
18
+
19
+ interface FieldValidationModalProps {
20
+ open: boolean;
21
+ invalidFields: InvalidField[];
22
+ currentTheme?: string | null | undefined;
23
+ handleClose: () => void;
24
+ }
25
+
26
+ const modalStyles = {
27
+ maxWidth: '600px',
28
+ minHeight: '300px',
29
+ };
30
+
31
+ const modalStylesV2 = {
32
+ maxWidth: '600px',
33
+ minHeight: '300px',
34
+ padding: '40px',
35
+ };
36
+
37
+ const FieldValidationModal: React.FC<FieldValidationModalProps> = ({
38
+ open,
39
+ invalidFields,
40
+ currentTheme,
41
+ handleClose,
42
+ }) => {
43
+ // Build the description with all field names (reversed so UI order matches template order)
44
+ const fieldNames = [...invalidFields].reverse().map(item => `"${item.field}"`).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
+ handleClose={handleClose}
53
+ title={MESSAGES.TEMPLATE.FIELD_VALIDATION.TITLE}
54
+ subHeading=""
55
+ description={description}
56
+ currentTheme="v2"
57
+ isGallery={false}
58
+ hideButtons={true}
59
+ />
60
+ ) : (
61
+ <Dialog
62
+ icon={<Warning fill="var(--primary-color)" />}
63
+ customStyles={modalStyles}
64
+ open={open}
65
+ handleClose={handleClose}
66
+ title={MESSAGES.TEMPLATE.FIELD_VALIDATION.TITLE}
67
+ subHeading=""
68
+ description={description}
69
+ />
70
+ );
71
+ };
72
+
73
+ 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
+ InvalidField,
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<InvalidField[]>([]);
145
149
 
146
150
  const { id } = useParams<{ id: string }>();
147
151
 
@@ -447,6 +451,19 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
447
451
  return;
448
452
  }
449
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
+
450
467
  const hasEmoji = validateEmoji(jsonData.pages);
451
468
 
452
469
  if (hasEmoji) {
@@ -576,6 +593,24 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
576
593
  model: string = '',
577
594
  loading: string | null = null
578
595
  ) => {
596
+ // If trying to open save modal, validate fields first
597
+ if (model === 'save' && templateType === 'json') {
598
+ const jsonData = store.toJSON();
599
+ const fieldValidationResult = validateTemplateFields(jsonData.pages);
600
+
601
+ if (fieldValidationResult.length > 0) {
602
+ // Show validation modal instead of save modal
603
+ setInvalidFields(fieldValidationResult);
604
+ setIsShowModel({
605
+ open: true,
606
+ model: 'field-validation',
607
+ loading: false
608
+ });
609
+ return; // Don't open save modal
610
+ }
611
+ }
612
+
613
+ // If validation passed or not save modal, proceed normally
579
614
  setIsShowModel((prev) => ({
580
615
  ...prev,
581
616
  open: !prev.open,
@@ -913,6 +948,15 @@ const TopNavigation: React.FC<TopNavigationProps> = ({
913
948
  currentTheme={currentTheme}
914
949
  />
915
950
  )}
951
+ {/* Field Validation Modal */}
952
+ {isShowModel.open && isShowModel.model === 'field-validation' && (
953
+ <FieldValidationModal
954
+ open={isShowModel.open}
955
+ invalidFields={invalidFields}
956
+ currentTheme={currentTheme}
957
+ handleClose={() => handleChangeModel()}
958
+ />
959
+ )}
916
960
  {/* Duplicate Template Modal */}
917
961
  <DuplicateTemplateModal
918
962
  open={isShowModel.open && isShowModel.model === 'duplicate'}
@@ -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: "Please fix the following field errors",
149
+ DESCRIPTION: (fieldNames: string) => `Following fields ${fieldNames} are incorrect to proceed further. You need to fix those`,
150
+ INVALID_FORMAT: "is incorrect",
151
+ NOT_UPPERCASE: "is incorrect",
152
+ },
146
153
  },
147
154
  QR_CODE_MODAL: {
148
155
  CREATE_TITLE: "Create New QR Code",
@@ -325,4 +325,67 @@ 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
+ // Find all potential field tokens/fields in the text.
355
+ // A tokentokens/fields is any sequence that contains at least one brace character.
356
+ const tokens = text.split(/\s+/);
357
+
358
+ tokens.forEach((token: string) => {
359
+ if (!token) return;
360
+
361
+ // Only process tokens that contain at least one brace
362
+ if (!token.includes('{') && !token.includes('}')) return;
363
+
364
+ // Valid format: must be EXACTLY {{UPPERCASE_CONTENT}} with no extra chars
365
+ const isValidFormat = /^\{\{[A-Z0-9._]+\}\}$/.test(token);
366
+
367
+ if (!isValidFormat) {
368
+ const content = token.replace(/^\{+|\}+$/g, '');
369
+ const isDoubleBrace = /^\{\{.+\}\}$/.test(token);
370
+ const hasOnlyValidChars = /^[A-Za-z0-9._]+$/.test(content);
371
+
372
+ if (isDoubleBrace && hasOnlyValidChars && content !== content.toUpperCase()) {
373
+ // Double brace but lowercase content
374
+ invalidFields.push({ field: token, issue: 'NOT_UPPERCASE', pageIndex });
375
+ } else {
376
+ // Any other invalid format
377
+ invalidFields.push({ field: token, issue: 'INVALID_FORMAT', pageIndex });
378
+ }
379
+ }
380
+ });
381
+ }
382
+ });
383
+ });
384
+
385
+ // Remove duplicates
386
+ const uniqueFields = invalidFields.filter((item: InvalidField, index: number, self: InvalidField[]) =>
387
+ index === self.findIndex((t: InvalidField) => t.field === item.field && t.issue === item.issue)
388
+ );
389
+
390
+ return uniqueFields;
391
+ };
package/version.js CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.1.5';
1
+ export const SDK_VERSION = '2.1.6-beta.1';