@capillarytech/creatives-library 8.0.222 → 8.0.223-alpha.0

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/config/app.js CHANGED
@@ -21,7 +21,6 @@ const config = {
21
21
  },
22
22
  development: {
23
23
  api_endpoint: 'https://crm-nightly-new.cc.capillarytech.com/arya/api/v1/creatives',
24
- // api_endpoint: 'http://localhost:2022/arya/api/v1/creatives',
25
24
  campaigns_api_endpoint: 'https://crm-nightly-new.cc.capillarytech.com/iris/v2/campaigns',
26
25
  campaigns_api_org_endpoint: 'https://crm-nightly-new.cc.capillarytech.com/iris/v2/org/campaign',
27
26
  auth_endpoint: 'https://crm-nightly-new.cc.capillarytech.com/arya/api/v1/auth',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.222",
4
+ "version": "8.0.223-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/services/api.js CHANGED
@@ -264,7 +264,6 @@ export const getUserData = () => {
264
264
 
265
265
  export const createTemplate = ({template}) => {
266
266
  const url = `${API_ENDPOINT}/templates/SMS`;
267
- console.log("creating template",template);
268
267
  return request(url, getAPICallObject('POST', template));
269
268
  };
270
269
 
@@ -347,7 +346,6 @@ export const getAllTemplates = async ({channel, queryParams = {}}) => {
347
346
 
348
347
  export const deleteTemplate = ({channel, id}) => {
349
348
  const url = `${API_ENDPOINT}/templates/${id}/${channel}`;
350
- console.log("deleting template", url);
351
349
  return request(url, getAPICallObject('DELETE'));
352
350
  //return API.deleteResource(url);
353
351
  };
@@ -19,6 +19,7 @@ const SendTestMessage = ({
19
19
  formData,
20
20
  isSendingTestMessage,
21
21
  formatMessage,
22
+ isContentValid = true,
22
23
  }) => (
23
24
  <CapStepsAccordian
24
25
  showNumberSteps={false}
@@ -43,7 +44,11 @@ const SendTestMessage = ({
43
44
  multiple
44
45
  placeholder={formatMessage(messages.testCustomersPlaceholder)}
45
46
  />
46
- <CapButton onClick={handleSendTestMessage} disabled={isEmpty(selectedTestEntities) || (isEmpty(formData['template-subject']) && isEmpty(formData[0]?.['template-subject'])) || isSendingTestMessage}>
47
+ <CapButton
48
+ onClick={handleSendTestMessage}
49
+ disabled={isEmpty(selectedTestEntities) || (isEmpty(formData['template-subject']) && isEmpty(formData[0]?.['template-subject'])) || isSendingTestMessage || !isContentValid}
50
+ title={!isContentValid ? formatMessage(messages.contentInvalid) : ''}
51
+ >
47
52
  <FormattedMessage {...messages.sendTestButton} />
48
53
  </CapButton>
49
54
  </CapRow>),
@@ -63,6 +68,7 @@ SendTestMessage.propTypes = {
63
68
  formData: PropTypes.object.isRequired,
64
69
  isSendingTestMessage: PropTypes.bool.isRequired,
65
70
  formatMessage: PropTypes.func.isRequired,
71
+ isContentValid: PropTypes.bool,
66
72
  };
67
73
 
68
74
  export default SendTestMessage;
@@ -52,6 +52,7 @@ import {
52
52
  INITIAL_PAYLOAD, EMAIL, TEST, DESKTOP, ACTIVE, MOBILE,
53
53
  } from './constants';
54
54
  import { GLOBAL_CONVERT_OPTIONS } from '../FormBuilder/constants';
55
+ import { validateIfTagClosed } from '../../utils/tagValidations';
55
56
 
56
57
  const TestAndPreviewSlidebox = (props) => {
57
58
  const {
@@ -103,10 +104,147 @@ const TestAndPreviewSlidebox = (props) => {
103
104
  const [selectedTestEntities, setSelectedTestEntities] = useState([]);
104
105
  const [beeContent, setBeeContent] = useState(''); // Track BEE editor content separately
105
106
  const previousBeeContentRef = useRef(''); // Track previous BEE content to prevent unnecessary updates
107
+ const [isContentValid, setIsContentValid] = useState(true); // Track if content tags are valid
106
108
 
107
109
  const isUpdatePreviewDisabled = useMemo(() => (
108
- requiredTags.some((tag) => !customValues[tag.fullPath])
109
- ), [requiredTags, customValues]);
110
+ requiredTags.some((tag) => !customValues[tag.fullPath]) || !isContentValid
111
+ ), [requiredTags, customValues, isContentValid]);
112
+
113
+ // Function to validate tags in content
114
+ const validateContentTags = (content) => {
115
+ if (!content) return true;
116
+
117
+ try {
118
+ // Convert HTML to text (same as what's used for tag extraction)
119
+ // This ensures we validate the same content that will be used for tag extraction
120
+ const textContent = convert(content, GLOBAL_CONVERT_OPTIONS);
121
+
122
+ // Check if there are any braces in the content
123
+ const hasBraces = textContent.includes('{') || textContent.includes('}');
124
+
125
+ // If no braces exist, content is valid (no tag validation needed)
126
+ if (!hasBraces) {
127
+ return true;
128
+ }
129
+
130
+ // First check if tags are properly closed using the utility function
131
+ // This validates that all opening braces have corresponding closing braces
132
+ if (!validateIfTagClosed(textContent)) {
133
+ return false;
134
+ }
135
+
136
+ // Now validate tag format: tags must be in format {{tag_name}}
137
+ // Find all valid tag patterns {{tag_name}}
138
+ const tagPattern = /{{[^}]*}}/g;
139
+ const matches = textContent.match(tagPattern) || [];
140
+
141
+ // Remove all valid {{tag}} patterns from content to check for invalid braces
142
+ let contentWithoutValidTags = textContent;
143
+ matches.forEach((match) => {
144
+ contentWithoutValidTags = contentWithoutValidTags.replace(match, '');
145
+ });
146
+
147
+ // Check if there are any remaining braces (single braces or unclosed braces)
148
+ // These would be invalid patterns like {tag}, {first, first}, etc.
149
+ if (contentWithoutValidTags.includes('{') || contentWithoutValidTags.includes('}')) {
150
+ return false;
151
+ }
152
+
153
+ // Check each tag for valid format
154
+ for (const match of matches) {
155
+ // Valid tag format: {{tag_name}} - must start with {{ and end with }}
156
+ if (!match.startsWith('{{') || !match.endsWith('}}')) {
157
+ return false;
158
+ }
159
+
160
+ // Extract tag name (content between {{ and }})
161
+ const tagName = match.slice(2, -2).trim();
162
+
163
+ // Tag name should not be empty
164
+ if (!tagName) {
165
+ return false;
166
+ }
167
+
168
+ // Check for invalid patterns in tag name
169
+ // Invalid patterns: "first or first", "first and first", etc.
170
+ const invalidPatterns = [
171
+ /\s+or\s+/i, // " or " as separate word (e.g., "first or first")
172
+ /\s+and\s+/i, // " and " as separate word
173
+ ];
174
+
175
+ for (const pattern of invalidPatterns) {
176
+ if (pattern.test(tagName)) {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ // Check for unclosed single braces in tag name (e.g., {{first{name}})
182
+ const singleOpenBraces = (tagName.match(/{/g) || []).length;
183
+ const singleCloseBraces = (tagName.match(/}/g) || []).length;
184
+ if (singleOpenBraces !== singleCloseBraces) {
185
+ return false;
186
+ }
187
+ }
188
+
189
+ return true;
190
+ } catch (error) {
191
+ // If conversion fails, fall back to validating the original content
192
+ console.warn('Error converting content for validation:', error);
193
+ const hasBraces = content.includes('{') || content.includes('}');
194
+ if (!hasBraces) {
195
+ return true;
196
+ }
197
+
198
+ // Apply same validation to original content
199
+ if (!validateIfTagClosed(content)) {
200
+ return false;
201
+ }
202
+
203
+ const tagPattern = /{{[^}]*}}/g;
204
+ const matches = content.match(tagPattern) || [];
205
+
206
+ // Remove all valid {{tag}} patterns from content to check for invalid braces
207
+ let contentWithoutValidTags = content;
208
+ matches.forEach((match) => {
209
+ contentWithoutValidTags = contentWithoutValidTags.replace(match, '');
210
+ });
211
+
212
+ // Check if there are any remaining braces (single braces or unclosed braces)
213
+ if (contentWithoutValidTags.includes('{') || contentWithoutValidTags.includes('}')) {
214
+ return false;
215
+ }
216
+
217
+ for (const match of matches) {
218
+ if (!match.startsWith('{{') || !match.endsWith('}}')) {
219
+ return false;
220
+ }
221
+
222
+ const tagName = match.slice(2, -2).trim();
223
+ if (!tagName) {
224
+ return false;
225
+ }
226
+
227
+ const invalidPatterns = [
228
+ /\s+or\s+/i,
229
+ /\s+and\s+/i,
230
+ ];
231
+
232
+ for (const pattern of invalidPatterns) {
233
+ if (pattern.test(tagName)) {
234
+ return false;
235
+ }
236
+ }
237
+
238
+ const singleOpenBraces = (tagName.match(/{/g) || []).length;
239
+ const singleCloseBraces = (tagName.match(/}/g) || []).length;
240
+ if (singleOpenBraces !== singleCloseBraces) {
241
+ return false;
242
+ }
243
+ }
244
+
245
+ return true;
246
+ }
247
+ };
110
248
 
111
249
  // Function to resolve tags in text with custom values
112
250
  const resolveTagsInText = (text, tagValues) => {
@@ -153,6 +291,10 @@ const TestAndPreviewSlidebox = (props) => {
153
291
  if (existingContent && existingContent.trim() !== '') {
154
292
  // We already have content, update local state only if it's different
155
293
  if (existingContent !== previousBeeContentRef.current) {
294
+ // Validate content tags for BEE editor
295
+ const isValid = validateContentTags(existingContent);
296
+ setIsContentValid(isValid);
297
+
156
298
  previousBeeContentRef.current = existingContent;
157
299
  setBeeContent(existingContent);
158
300
  setPreviewDataHtml({
@@ -186,6 +328,10 @@ const TestAndPreviewSlidebox = (props) => {
186
328
  }
187
329
 
188
330
  if (htmlFile) {
331
+ // Validate content tags
332
+ const isValid = validateContentTags(htmlFile);
333
+ setIsContentValid(isValid);
334
+
189
335
  // Update our states
190
336
  previousBeeContentRef.current = htmlFile;
191
337
  setBeeContent(htmlFile);
@@ -194,9 +340,16 @@ const TestAndPreviewSlidebox = (props) => {
194
340
  resolvedTitle: formData['template-subject'] || ''
195
341
  });
196
342
 
197
- // Always extract tags when content changes
198
- const payloadContent = convert(htmlFile, GLOBAL_CONVERT_OPTIONS);
199
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
343
+ // Only extract tags if content is valid
344
+ if (isValid) {
345
+ const payloadContent = convert(htmlFile, GLOBAL_CONVERT_OPTIONS);
346
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
347
+ } else {
348
+ // Show error notification for invalid content
349
+ CapNotification.error({
350
+ message: formatMessage(messages.contentInvalid),
351
+ });
352
+ }
200
353
  }
201
354
 
202
355
  // Restore original handler
@@ -211,23 +364,45 @@ const TestAndPreviewSlidebox = (props) => {
211
364
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
212
365
 
213
366
  if (templateContent) {
367
+ // Validate content tags
368
+ const isValid = validateContentTags(templateContent);
369
+ setIsContentValid(isValid);
370
+
214
371
  // Update preview with initial content
215
372
  setPreviewDataHtml({
216
373
  resolvedBody: templateContent,
217
374
  resolvedTitle: formData['template-subject'] || ''
218
375
  });
219
376
 
220
- // Always extract tags when showing
221
- const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
222
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
377
+ // Only extract tags if content is valid
378
+ if (isValid) {
379
+ const payloadContent = convert(templateContent, GLOBAL_CONVERT_OPTIONS);
380
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
381
+ } else {
382
+ // Show error notification for invalid content
383
+ CapNotification.error({
384
+ message: formatMessage(messages.contentInvalid),
385
+ });
386
+ }
223
387
  } else {
224
388
  // Fallback to content prop if no template content
225
- const payloadContent = convert(
226
- getCurrentContent,
227
- GLOBAL_CONVERT_OPTIONS
228
- );
229
- // Always extract tags when showing
230
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
389
+ const contentToValidate = getCurrentContent;
390
+ const isValid = validateContentTags(contentToValidate);
391
+ setIsContentValid(isValid);
392
+
393
+ // Only extract tags if content is valid
394
+ if (isValid) {
395
+ const payloadContent = convert(
396
+ contentToValidate,
397
+ GLOBAL_CONVERT_OPTIONS
398
+ );
399
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
400
+ } else {
401
+ // Show error notification for invalid content
402
+ CapNotification.error({
403
+ message: formatMessage(messages.contentInvalid),
404
+ });
405
+ }
231
406
  }
232
407
  }
233
408
 
@@ -243,17 +418,28 @@ const TestAndPreviewSlidebox = (props) => {
243
418
  const isDragDrop = currentTabData?.[activeTab]?.is_drag_drop;
244
419
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
245
420
 
246
- if (templateContent && templateContent.trim() !== '') {
247
- // Common function to handle content update
421
+ if (templateContent && templateContent.trim() !== '' && show) {
422
+ // Common function to handle content update with validation
248
423
  const handleContentUpdate = (content) => {
424
+ // Validate content tags for each update
425
+ const isValid = validateContentTags(content);
426
+ setIsContentValid(isValid);
427
+
249
428
  setPreviewDataHtml({
250
429
  resolvedBody: content,
251
430
  resolvedTitle: formData['template-subject'] || ''
252
431
  });
253
432
 
254
- // Extract tags from content
255
- const payloadContent = convert(content, GLOBAL_CONVERT_OPTIONS);
256
- actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
433
+ // Only extract tags if content is valid
434
+ if (isValid) {
435
+ const payloadContent = convert(content, GLOBAL_CONVERT_OPTIONS);
436
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
437
+ } else {
438
+ // Show error notification for invalid content
439
+ CapNotification.error({
440
+ message: formatMessage(messages.contentInvalid),
441
+ });
442
+ }
257
443
  };
258
444
 
259
445
  if (isDragDrop) {
@@ -287,6 +473,7 @@ const TestAndPreviewSlidebox = (props) => {
287
473
  setTagsExtracted(false);
288
474
  setPreviewDevice('desktop');
289
475
  setSelectedTestEntities([]);
476
+ setIsContentValid(true);
290
477
  actions.clearPrefilledValues();
291
478
  }
292
479
  }, [show]);
@@ -530,6 +717,22 @@ const TestAndPreviewSlidebox = (props) => {
530
717
 
531
718
  // Handle update preview
532
719
  const handleUpdatePreview = async () => {
720
+ // Re-validate content to get latest state (in case liquid errors were fixed)
721
+ const currentTabData = formData[currentTab - 1];
722
+ const activeTab = currentTabData?.activeTab;
723
+ const templateContent = currentTabData?.[activeTab]?.['template-content'];
724
+ const contentToValidate = templateContent || getCurrentContent;
725
+ const isValid = validateContentTags(contentToValidate);
726
+ setIsContentValid(isValid);
727
+
728
+ // Check if content is valid before updating preview
729
+ if (!isValid) {
730
+ CapNotification.error({
731
+ message: formatMessage(messages.contentInvalid),
732
+ });
733
+ return;
734
+ }
735
+
533
736
  try {
534
737
  // Include unsubscribe tag if content contains it
535
738
  const resolvedTags = { ...customValues };
@@ -559,9 +762,20 @@ const TestAndPreviewSlidebox = (props) => {
559
762
  const currentTabData = formData[currentTab - 1];
560
763
  const activeTab = currentTabData?.activeTab;
561
764
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
765
+ const content = templateContent || getCurrentContent;
766
+
767
+ // Validate content tags before extracting
768
+ const isValid = validateContentTags(content);
769
+ setIsContentValid(isValid);
770
+
771
+ if (!isValid) {
772
+ CapNotification.error({
773
+ message: formatMessage(messages.contentInvalid),
774
+ });
775
+ return;
776
+ }
562
777
 
563
778
  // Check for personalization tags (excluding unsubscribe)
564
- const content = templateContent || getCurrentContent;
565
779
  const tags = content.match(/{{[^}]+}}/g) || [];
566
780
  const hasPersonalizationTags = tags.some(tag => !tag.includes('unsubscribe'));
567
781
 
@@ -590,6 +804,22 @@ const TestAndPreviewSlidebox = (props) => {
590
804
  };
591
805
 
592
806
  const handleSendTestMessage = () => {
807
+ // Re-validate content to get latest state (in case liquid errors were fixed)
808
+ const currentTabData = formData[currentTab - 1];
809
+ const activeTab = currentTabData?.activeTab;
810
+ const templateContent = currentTabData?.[activeTab]?.['template-content'];
811
+ const contentToValidate = templateContent || getCurrentContent;
812
+ const isValid = validateContentTags(contentToValidate);
813
+ setIsContentValid(isValid);
814
+
815
+ // Check if content is valid before sending test message
816
+ if (!isValid) {
817
+ CapNotification.error({
818
+ message: formatMessage(messages.contentInvalid),
819
+ });
820
+ return;
821
+ }
822
+
593
823
  const allUserIds = [];
594
824
  selectedTestEntities.forEach((entityId) => {
595
825
  const group = testGroups.find((g) => g.groupId === entityId);
@@ -685,6 +915,7 @@ const TestAndPreviewSlidebox = (props) => {
685
915
  formData={formData}
686
916
  isSendingTestMessage={isSendingTestMessage}
687
917
  formatMessage={formatMessage}
918
+ isContentValid={isContentValid}
688
919
  />
689
920
  );
690
921
 
@@ -144,4 +144,12 @@ export default defineMessages({
144
144
  id: `${scope}.invalidJSON`,
145
145
  defaultMessage: 'Invalid JSON input',
146
146
  },
147
+ contentInvalid: {
148
+ id: `${scope}.contentInvalid`,
149
+ defaultMessage: 'Content is invalid. Please fix the tags in your content before testing or previewing.',
150
+ },
151
+ previewUpdateError: {
152
+ id: `${scope}.previewUpdateError`,
153
+ defaultMessage: 'Failed to update preview',
154
+ },
147
155
  });