@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
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
|
|
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
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
|
|
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
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
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
|
});
|