@capillarytech/creatives-library 8.0.280 → 8.0.282
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/package.json +1 -1
- package/utils/tagValidations.js +6 -2
- package/utils/tests/tagValidations.test.js +1 -1
- package/v2Components/FormBuilder/index.js +20 -6
- package/v2Containers/CreativesContainer/SlideBoxContent.js +3 -2
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +36 -14
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +89 -30
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +14 -12
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +124 -0
- package/v2Containers/InApp/index.js +2 -0
- package/v2Containers/InappAdvance/index.js +2 -0
- package/v2Containers/MobilePush/Create/index.js +20 -10
- package/v2Containers/MobilePush/Edit/index.js +69 -12
package/package.json
CHANGED
package/utils/tagValidations.js
CHANGED
|
@@ -201,6 +201,7 @@ export const validateTags = ({
|
|
|
201
201
|
unsupportedTags: [],
|
|
202
202
|
isBraceError: false,
|
|
203
203
|
};
|
|
204
|
+
// Mandatory-tags check: only when we have a tags list and are in library mode
|
|
204
205
|
if (tags && tags.length && !isFullMode) {
|
|
205
206
|
lodashForEach(tags, ({
|
|
206
207
|
definition: {
|
|
@@ -217,6 +218,9 @@ export const validateTags = ({
|
|
|
217
218
|
}
|
|
218
219
|
});
|
|
219
220
|
});
|
|
221
|
+
}
|
|
222
|
+
// In library mode, always scan content for {{...}} and flag unsupported tags (even when tags list is empty)
|
|
223
|
+
if (!isFullMode && content) {
|
|
220
224
|
const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
|
|
221
225
|
let match = regex.exec(content);
|
|
222
226
|
while (match !== null) {
|
|
@@ -224,8 +228,8 @@ export const validateTags = ({
|
|
|
224
228
|
const tagIndex = match?.index;
|
|
225
229
|
match = regex.exec(content);
|
|
226
230
|
let ifSupported = false;
|
|
227
|
-
lodashForEach(tags, (tag) => {
|
|
228
|
-
if (tag
|
|
231
|
+
lodashForEach(tags || [], (tag) => {
|
|
232
|
+
if (tag?.definition?.value === tagValue) {
|
|
229
233
|
ifSupported = true;
|
|
230
234
|
}
|
|
231
235
|
});
|
|
@@ -104,7 +104,7 @@ describe("validateTags", () => {
|
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
expect(result.valid).toEqual(true);
|
|
107
|
-
expect(result.unsupportedTags).toEqual([]);
|
|
107
|
+
expect(result.unsupportedTags).toEqual(["tag1", "tag2", "tag3"]);
|
|
108
108
|
expect(result.isBraceError).toEqual(false);
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -1247,10 +1247,11 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1247
1247
|
},
|
|
1248
1248
|
}),
|
|
1249
1249
|
() => {
|
|
1250
|
-
// Callback after the state is updated
|
|
1251
1250
|
this.props.showLiquidErrorInFooter(this.state.liquidErrorMessage, this.props.channel === SMS? null: this.state.currentTab);
|
|
1252
1251
|
}
|
|
1253
1252
|
);
|
|
1253
|
+
// Show toast for liquid flow too so user sees error (scenario 3)
|
|
1254
|
+
this.openNotificationWithIcon('error', errorString, "email-validation-error");
|
|
1254
1255
|
} else {
|
|
1255
1256
|
this.openNotificationWithIcon('error', errorString, "email-validation-error");
|
|
1256
1257
|
}
|
|
@@ -1518,7 +1519,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1518
1519
|
response.isBraceError = false;
|
|
1519
1520
|
response.isContentEmpty = false;
|
|
1520
1521
|
const contentForValidation = isEmail ? convert(content, GLOBAL_CONVERT_OPTIONS) : content ;
|
|
1521
|
-
|
|
1522
|
+
const isModuleTypeOutbound = (this.props?.moduleType || '').toUpperCase() === OUTBOUND;
|
|
1523
|
+
// Run tag validation (missing + unsupported): library mode, or full mode with liquid support, or
|
|
1524
|
+
// legacy Email (CK Editor) when unsubscribe is required (EMAIL_UNSUBSCRIBE_TAG_MANDATORY false) so missing-unsubscribe error shows
|
|
1525
|
+
const shouldRunTagValidation = !isFullMode
|
|
1526
|
+
|| (isEmail && hasLiquidSupportFeature())
|
|
1527
|
+
|| (isEmail && !isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound);
|
|
1528
|
+
if (tags && tags.length && !isFullMode) {
|
|
1522
1529
|
_.forEach(tags, (tag) => {
|
|
1523
1530
|
_.forEach(tag.definition.supportedModules, (module) => {
|
|
1524
1531
|
if (module.mandatory && (currentModule === module.context)) {
|
|
@@ -1529,6 +1536,14 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1529
1536
|
}
|
|
1530
1537
|
});
|
|
1531
1538
|
});
|
|
1539
|
+
// Legacy Email (CK Editor): when unsubscribe is required, ensure we validate it even if tag schema didn't mark it mandatory for this module
|
|
1540
|
+
if (isEmail && !isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound) {
|
|
1541
|
+
const hasUnsubscribeInContent = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g.test(content);
|
|
1542
|
+
if (!hasUnsubscribeInContent && response.missingTags.indexOf('unsubscribe') === -1) {
|
|
1543
|
+
response.valid = false;
|
|
1544
|
+
response.missingTags.push('unsubscribe');
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1532
1547
|
const regex = /{{[(A-Z\w+(\s\w+)*$\(\)@!#$%^&*~.,/\\]+}}/g;
|
|
1533
1548
|
let match = regex.exec(content);
|
|
1534
1549
|
const regexImgSrc=/<img[^>]*\bsrc\s*=\s*"[^"]*{{customer_barcode}}[^"]*"/;
|
|
@@ -1537,7 +1552,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1537
1552
|
let matchCustomerBarcode = regexCustomerBarcode.exec(content);
|
|
1538
1553
|
// \S matches anything other than a space, a tab, a newline, or a carriage return.
|
|
1539
1554
|
const validString= /\S/.test(contentForValidation);
|
|
1540
|
-
if (isEmailUnsubscribeTagMandatory() && isEmail &&
|
|
1555
|
+
if (isEmailUnsubscribeTagMandatory() && isEmail && isModuleTypeOutbound) {
|
|
1541
1556
|
const missingTagIndex = response?.missingTags?.indexOf("unsubscribe");
|
|
1542
1557
|
if(missingTagIndex != -1) { //skip regex tags for mandatory tags also
|
|
1543
1558
|
response?.missingTags?.splice(missingTagIndex, 1);
|
|
@@ -1581,9 +1596,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1581
1596
|
});
|
|
1582
1597
|
|
|
1583
1598
|
ifSupported = ifSupported || this.checkIfSupportedTag(tagValue, injectedTags);
|
|
1584
|
-
// Only add to unsupportedTags if not inside a {% ... %} block
|
|
1585
|
-
|
|
1586
|
-
if (!ifSupported && !this.liquidFlow() && !isInsideLiquidBlock(content, tagIndex)) {
|
|
1599
|
+
// Only add to unsupportedTags if not inside a {% ... %} block (scenario 3: liquid orgs also get unsupported-tag errors)
|
|
1600
|
+
if (!ifSupported && !isInsideLiquidBlock(content, tagIndex)) {
|
|
1587
1601
|
response.unsupportedTags.push(tagValue);
|
|
1588
1602
|
response.valid = false;
|
|
1589
1603
|
}
|
|
@@ -682,8 +682,9 @@ export function SlideBoxContent(props) {
|
|
|
682
682
|
{(isEditEmailWithId || isEmailEditWithContent) && (
|
|
683
683
|
(() => {
|
|
684
684
|
const supportCKEditor = commonUtil.hasSupportCKEditor();
|
|
685
|
-
// When supportCKEditor is true:
|
|
686
|
-
|
|
685
|
+
// When supportCKEditor is true: Use Email component (legacy flow with CKEditor).
|
|
686
|
+
// When supportCKEditor is false: Always use EmailWrapper (BEE or HTML editor, never CKEditor).
|
|
687
|
+
if (supportCKEditor) {
|
|
687
688
|
return (
|
|
688
689
|
<Email
|
|
689
690
|
key="cretives-container-email-edit"
|
|
@@ -108,6 +108,24 @@ const EmailHTMLEditor = (props) => {
|
|
|
108
108
|
standardErrors: [],
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
// Merge tag validation errors (unsupported/missing) into apiValidationErrors so they show in ValidationErrorDisplay
|
|
112
|
+
const mergedApiValidationErrors = useMemo(() => {
|
|
113
|
+
const tagMessages = [];
|
|
114
|
+
if (tagValidationError?.unsupportedTags?.length) {
|
|
115
|
+
tagMessages.push(`Unsupported tags are: ${tagValidationError.unsupportedTags.join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
if (tagValidationError?.missingTags?.length && !isEmailUnsubscribeTagMandatory()) {
|
|
118
|
+
tagMessages.push(`Missing tags are: ${tagValidationError.missingTags.join(', ')}`);
|
|
119
|
+
}
|
|
120
|
+
if (tagMessages.length === 0) {
|
|
121
|
+
return apiValidationErrors;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
liquidErrors: apiValidationErrors?.liquidErrors || [],
|
|
125
|
+
standardErrors: [...(apiValidationErrors?.standardErrors || []), ...tagMessages],
|
|
126
|
+
};
|
|
127
|
+
}, [apiValidationErrors, tagValidationError]);
|
|
128
|
+
|
|
111
129
|
// Refs for tracking initialization and previous values
|
|
112
130
|
const contentInitializedRef = useRef(false);
|
|
113
131
|
const subjectInitializedRef = useRef(false);
|
|
@@ -479,6 +497,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
479
497
|
location,
|
|
480
498
|
tagModule: getDefaultTags,
|
|
481
499
|
eventContextTags,
|
|
500
|
+
isFullMode,
|
|
482
501
|
});
|
|
483
502
|
|
|
484
503
|
if (!validationResult.valid) {
|
|
@@ -691,26 +710,27 @@ const EmailHTMLEditor = (props) => {
|
|
|
691
710
|
// 2. Validate Unsubscribe Tag when feature is OFF (when flag is false we require unsubscribe)
|
|
692
711
|
// When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is true: do NOT validate for unsubscribe (aligned with FormBuilder).
|
|
693
712
|
// When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is false: validate and require unsubscribe tag.
|
|
694
|
-
|
|
695
|
-
|
|
713
|
+
// Run for both library and full mode so liquid-enabled orgs also get the error (notification + ValidationErrorDisplay).
|
|
714
|
+
const isModuleTypeOutbound = (moduleType || '').toUpperCase() === OUTBOUND;
|
|
715
|
+
if (!isEmailUnsubscribeTagMandatory() && isModuleTypeOutbound) {
|
|
696
716
|
const unsubscribeRegex = /{{unsubscribe(\(#[a-zA-Z\d]{6}\))?}}/g; // eslint-disable-line no-useless-escape
|
|
697
717
|
const hasUnsubscribeTag = unsubscribeRegex.test(htmlContent);
|
|
698
718
|
|
|
699
719
|
if (!hasUnsubscribeTag) {
|
|
700
|
-
|
|
720
|
+
setTagValidationError({ valid: false, missingTags: ['unsubscribe'] });
|
|
701
721
|
const missingTagsMsg = intl.formatMessage(formBuilderMessages.missingTags);
|
|
702
722
|
const errorMessage = `${missingTagsMsg} unsubscribe`;
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
723
|
+
setTimeout(() => {
|
|
724
|
+
CapNotification.error({
|
|
725
|
+
message: 'ERROR ! ! !',
|
|
726
|
+
description: errorMessage,
|
|
727
|
+
duration: 5,
|
|
728
|
+
});
|
|
729
|
+
}, 0);
|
|
708
730
|
|
|
709
|
-
// Reset parent state so next click is detected as a change
|
|
710
731
|
if (onValidationFail) {
|
|
711
732
|
onValidationFail();
|
|
712
733
|
}
|
|
713
|
-
// Block save - unsubscribe tag is required when validation is enabled
|
|
714
734
|
return;
|
|
715
735
|
}
|
|
716
736
|
}
|
|
@@ -718,7 +738,9 @@ const EmailHTMLEditor = (props) => {
|
|
|
718
738
|
// 3. Validate Content Tags
|
|
719
739
|
// For NON-liquid orgs: BLOCKING validation (matches CK/BEE behavior)
|
|
720
740
|
// For liquid orgs: Non-blocking (extractTags API will validate)
|
|
721
|
-
if
|
|
741
|
+
// In library mode, always validate when there is content (even if tags list is empty)
|
|
742
|
+
const shouldValidateTags = (tags.length > 0 || !isEmpty(injectedTags)) || (!isFullMode && !!htmlContent);
|
|
743
|
+
if (shouldValidateTags) {
|
|
722
744
|
const validationResult = validateTags({
|
|
723
745
|
content: htmlContent,
|
|
724
746
|
tagsParam: tags,
|
|
@@ -726,12 +748,12 @@ const EmailHTMLEditor = (props) => {
|
|
|
726
748
|
location,
|
|
727
749
|
tagModule: getDefaultTags,
|
|
728
750
|
eventContextTags,
|
|
751
|
+
isFullMode,
|
|
729
752
|
});
|
|
730
753
|
|
|
731
754
|
const hasUnsupportedTags = validationResult?.unsupportedTags?.length > 0;
|
|
732
|
-
if (!validationResult?.valid || hasUnsupportedTags) {
|
|
755
|
+
if (!validationResult?.valid || (hasUnsupportedTags && !isFullMode)) {
|
|
733
756
|
setTagValidationError(validationResult);
|
|
734
|
-
|
|
735
757
|
// IMPORTANT: For non-liquid orgs, block save (like CK/BEE editor)
|
|
736
758
|
// For liquid orgs, continue (extractTags API will validate)
|
|
737
759
|
if (!isLiquidEnabled) {
|
|
@@ -1157,7 +1179,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
1157
1179
|
isFullMode={isFullMode}
|
|
1158
1180
|
onErrorAcknowledged={handleErrorAcknowledged}
|
|
1159
1181
|
onValidationChange={handleValidationChange}
|
|
1160
|
-
apiValidationErrors={
|
|
1182
|
+
apiValidationErrors={mergedApiValidationErrors}
|
|
1161
1183
|
/>
|
|
1162
1184
|
</CapColumn>
|
|
1163
1185
|
</CapRow>
|
|
@@ -49,10 +49,16 @@ const mockGetValidationState = jest.fn(() => ({
|
|
|
49
49
|
issueCounts: { errors: 0, warnings: 0, total: 0 },
|
|
50
50
|
}));
|
|
51
51
|
|
|
52
|
+
// Ref to capture apiValidationErrors passed to HTMLEditor (for mergedApiValidationErrors tests)
|
|
53
|
+
const capturedApiValidationErrorsRef = { current: null };
|
|
54
|
+
|
|
52
55
|
// Mock HtmlEditor - it exports a lazy-loaded component by default
|
|
53
56
|
jest.mock('../../../../v2Components/HtmlEditor/index.lazy', () => {
|
|
54
57
|
const React = require('react');
|
|
55
58
|
const MockHTMLEditor = React.forwardRef((props, ref) => {
|
|
59
|
+
if (global.__captureApiValidationErrorsRef && props.apiValidationErrors) {
|
|
60
|
+
global.__captureApiValidationErrorsRef.current = props.apiValidationErrors;
|
|
61
|
+
}
|
|
56
62
|
React.useImperativeHandle(ref, () => ({
|
|
57
63
|
getAllIssues: () => mockGetAllIssues(),
|
|
58
64
|
getValidationState: () => mockGetValidationState(),
|
|
@@ -109,6 +115,9 @@ jest.mock('../../../../v2Components/HtmlEditor/index.lazy', () => {
|
|
|
109
115
|
jest.mock('../../../../v2Components/HtmlEditor', () => {
|
|
110
116
|
const React = require('react');
|
|
111
117
|
const MockHTMLEditor = React.forwardRef((props, ref) => {
|
|
118
|
+
if (global.__captureApiValidationErrorsRef && props.apiValidationErrors) {
|
|
119
|
+
global.__captureApiValidationErrorsRef.current = props.apiValidationErrors;
|
|
120
|
+
}
|
|
112
121
|
React.useImperativeHandle(ref, () => ({
|
|
113
122
|
getAllIssues: () => mockGetAllIssues(),
|
|
114
123
|
getValidationState: () => mockGetValidationState(),
|
|
@@ -421,6 +430,86 @@ describe('EmailHTMLEditor', () => {
|
|
|
421
430
|
});
|
|
422
431
|
// Reset hasLiquidSupportFeature mock to return true by default
|
|
423
432
|
mockHasLiquidSupportFeature.mockReturnValue(true);
|
|
433
|
+
capturedApiValidationErrorsRef.current = null;
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe('mergedApiValidationErrors (lines 124-125)', () => {
|
|
437
|
+
beforeEach(() => {
|
|
438
|
+
global.__captureApiValidationErrorsRef = capturedApiValidationErrorsRef;
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
afterEach(() => {
|
|
442
|
+
delete global.__captureApiValidationErrorsRef;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('merges tag unsupported errors into standardErrors when tagValidationError has unsupportedTags', async () => {
|
|
446
|
+
validateTags.mockReturnValue({
|
|
447
|
+
valid: false,
|
|
448
|
+
unsupportedTags: ['tagA', 'tagB'],
|
|
449
|
+
});
|
|
450
|
+
renderWithIntl({
|
|
451
|
+
metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
|
|
452
|
+
tags: [{ name: 'customer.name' }],
|
|
453
|
+
supportedTags: [],
|
|
454
|
+
});
|
|
455
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
456
|
+
await act(async () => {
|
|
457
|
+
fireEvent.click(changeButton);
|
|
458
|
+
});
|
|
459
|
+
await waitFor(() => {
|
|
460
|
+
expect(capturedApiValidationErrorsRef.current).not.toBeNull();
|
|
461
|
+
expect(capturedApiValidationErrorsRef.current.liquidErrors).toEqual([]);
|
|
462
|
+
expect(capturedApiValidationErrorsRef.current.standardErrors).toContain(
|
|
463
|
+
'Unsupported tags are: tagA, tagB',
|
|
464
|
+
);
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('merges tag missing errors into standardErrors when tagValidationError has missingTags and unsubscribe not mandatory', async () => {
|
|
469
|
+
isEmailUnsubscribeTagMandatory.mockReturnValue(false);
|
|
470
|
+
validateTags.mockReturnValue({
|
|
471
|
+
valid: false,
|
|
472
|
+
missingTags: ['unsubscribe'],
|
|
473
|
+
});
|
|
474
|
+
renderWithIntl({
|
|
475
|
+
metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
|
|
476
|
+
tags: [{ name: 'customer.name' }],
|
|
477
|
+
supportedTags: [],
|
|
478
|
+
});
|
|
479
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
480
|
+
await act(async () => {
|
|
481
|
+
fireEvent.click(changeButton);
|
|
482
|
+
});
|
|
483
|
+
await waitFor(() => {
|
|
484
|
+
expect(capturedApiValidationErrorsRef.current).not.toBeNull();
|
|
485
|
+
expect(capturedApiValidationErrorsRef.current.standardErrors).toContain(
|
|
486
|
+
'Missing tags are: unsubscribe',
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('uses apiValidationErrors.liquidErrors and concatenates apiValidationErrors.standardErrors with tag messages (merge shape)', async () => {
|
|
492
|
+
// When tag messages exist, mergedApiValidationErrors returns liquidErrors from apiValidationErrors
|
|
493
|
+
// and standardErrors = [...(apiValidationErrors?.standardErrors || []), ...tagMessages] (lines 124-125)
|
|
494
|
+
validateTags.mockReturnValue({
|
|
495
|
+
valid: false,
|
|
496
|
+
unsupportedTags: ['customTag'],
|
|
497
|
+
});
|
|
498
|
+
renderWithIntl({
|
|
499
|
+
metaEntities: { tags: { standard: [{ name: 'customer.name' }] } },
|
|
500
|
+
tags: [{ name: 'customer.name' }],
|
|
501
|
+
});
|
|
502
|
+
const changeButton = screen.getByTestId('trigger-content-change');
|
|
503
|
+
await act(async () => {
|
|
504
|
+
fireEvent.click(changeButton);
|
|
505
|
+
});
|
|
506
|
+
await waitFor(() => {
|
|
507
|
+
expect(capturedApiValidationErrorsRef.current).not.toBeNull();
|
|
508
|
+
const { liquidErrors, standardErrors } = capturedApiValidationErrorsRef.current;
|
|
509
|
+
expect(liquidErrors).toEqual([]);
|
|
510
|
+
expect(standardErrors).toContain('Unsupported tags are: customTag');
|
|
511
|
+
});
|
|
512
|
+
});
|
|
424
513
|
});
|
|
425
514
|
|
|
426
515
|
describe('Default Parameter Values (lines 60-63)', () => {
|
|
@@ -1016,36 +1105,6 @@ describe('EmailHTMLEditor', () => {
|
|
|
1016
1105
|
}, { timeout: 3000 });
|
|
1017
1106
|
});
|
|
1018
1107
|
|
|
1019
|
-
it('blocks save when unsubscribe validation is on (flag false) and tag is missing', async () => {
|
|
1020
|
-
// When EMAIL_UNSUBSCRIBE_TAG_MANDATORY is false we validate and require unsubscribe
|
|
1021
|
-
isEmailUnsubscribeTagMandatory.mockReturnValue(false);
|
|
1022
|
-
const onValidationFail = jest.fn();
|
|
1023
|
-
const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
|
|
1024
|
-
|
|
1025
|
-
// Set subject via input and content via HTMLEditor mock
|
|
1026
|
-
const { rerender } = renderWithIntl({
|
|
1027
|
-
onValidationFail,
|
|
1028
|
-
isGetFormData: false,
|
|
1029
|
-
moduleType: 'OUTBOUND',
|
|
1030
|
-
});
|
|
1031
|
-
const input = screen.getByTestId('subject-input');
|
|
1032
|
-
fireEvent.change(input, { target: { value: 'Valid Subject' } });
|
|
1033
|
-
// Trigger content change to set htmlContent
|
|
1034
|
-
const changeButton = screen.getByTestId('trigger-content-change');
|
|
1035
|
-
fireEvent.click(changeButton);
|
|
1036
|
-
// Now trigger save
|
|
1037
|
-
rerender(
|
|
1038
|
-
<IntlProvider locale="en" messages={{}}>
|
|
1039
|
-
<EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData moduleType="OUTBOUND" />
|
|
1040
|
-
</IntlProvider>
|
|
1041
|
-
);
|
|
1042
|
-
|
|
1043
|
-
await waitFor(() => {
|
|
1044
|
-
expect(CapNotification.error).toHaveBeenCalled();
|
|
1045
|
-
expect(onValidationFail).toHaveBeenCalled();
|
|
1046
|
-
}, { timeout: 3000 });
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
1108
|
it('allows save when unsubscribe validation is on and tag is present', () => {
|
|
1050
1109
|
isEmailUnsubscribeTagMandatory.mockReturnValue(false);
|
|
1051
1110
|
renderWithIntl({
|
|
@@ -206,7 +206,11 @@ const useEmailWrapper = ({
|
|
|
206
206
|
|
|
207
207
|
// Only fetch if we have an ID, don't have template data yet, and not already loading
|
|
208
208
|
if (hasParamsId && !hasTemplateDetails && !hasTemplateDataProp && !isTemplateLoading && emailActions?.getTemplateDetails) {
|
|
209
|
-
|
|
209
|
+
let templateId = params?.id || location?.query?.id || location?.params?.id;
|
|
210
|
+
if (!templateId && location?.pathname?.includes('/edit/')) {
|
|
211
|
+
const [, extractedId] = location.pathname.match(/\/edit\/([^/]+)/) || [];
|
|
212
|
+
if (extractedId) templateId = extractedId;
|
|
213
|
+
}
|
|
210
214
|
if (templateId) {
|
|
211
215
|
emailActions.getTemplateDetails(templateId, 'email');
|
|
212
216
|
}
|
|
@@ -581,8 +585,6 @@ const useEmailWrapper = ({
|
|
|
581
585
|
// CRITICAL: Only treat as edit mode if we have params.id (actual edit URL) or templateData prop
|
|
582
586
|
// Don't use templateDetails existence alone, as it might persist from previous template
|
|
583
587
|
const hasParamsId = params?.id || location?.query?.id || location?.params?.id || location?.pathname?.includes('/edit/');
|
|
584
|
-
const hasTemplateDetails = Email?.templateDetails && !isEmpty(Email.templateDetails);
|
|
585
|
-
const hasBEETemplate = Email?.BEETemplate && !isEmpty(Email.BEETemplate);
|
|
586
588
|
const hasTemplateDataProp = templateData && !isEmpty(templateData);
|
|
587
589
|
// CRITICAL: Consider it edit mode if we have params.id OR templateData prop (library mode)
|
|
588
590
|
// This allows editor type determination even when template data is still loading
|
|
@@ -590,13 +592,14 @@ const useEmailWrapper = ({
|
|
|
590
592
|
|
|
591
593
|
if (isEditMode) {
|
|
592
594
|
// Edit mode: Determine editor based on template data
|
|
593
|
-
// Check if template was created in BEE editor
|
|
594
595
|
// Priority: Email.templateDetails > Email.BEETemplate > templateData prop
|
|
595
|
-
|
|
596
|
-
|
|
596
|
+
// Use first non-empty source (empty array/object can appear while template is loading)
|
|
597
|
+
const editTemplateData = [Email?.templateDetails, Email?.BEETemplate, templateData].find(
|
|
598
|
+
(d) => d && !isEmpty(d)
|
|
599
|
+
) || null;
|
|
597
600
|
// Helper function to safely get is_drag_drop from various possible paths
|
|
598
601
|
const getIsDragDrop = (data) => {
|
|
599
|
-
if (!data) return false;
|
|
602
|
+
if (!data || isEmpty(data)) return false;
|
|
600
603
|
|
|
601
604
|
// Check common paths for is_drag_drop
|
|
602
605
|
// Path 1: versions.base.is_drag_drop (most common)
|
|
@@ -630,8 +633,10 @@ const useEmailWrapper = ({
|
|
|
630
633
|
return false;
|
|
631
634
|
};
|
|
632
635
|
|
|
633
|
-
|
|
634
|
-
|
|
636
|
+
// When template data is still loading (editTemplateData empty), trust editor prop so BEE templates open in BEE
|
|
637
|
+
const hasMeaningfulTemplateData = editTemplateData && !isEmpty(editTemplateData);
|
|
638
|
+
const isDragDrop = getIsDragDrop(editTemplateData)
|
|
639
|
+
|| (!hasMeaningfulTemplateData && editor === 'BEE');
|
|
635
640
|
// Check if BEE is enabled for org (equivalent to checkBeeEditorAllowedForLibrary)
|
|
636
641
|
// For editor selection:
|
|
637
642
|
// - In full mode: BEE is always enabled
|
|
@@ -643,7 +648,6 @@ const useEmailWrapper = ({
|
|
|
643
648
|
|| (editor === "BEE" && !isFullMode)
|
|
644
649
|
|| beeEnabledFromAPI
|
|
645
650
|
|| (isAPIResponsePending && isDragDrop && !isFullMode); // Optimistic: if template is BEE and API pending, allow BEE
|
|
646
|
-
|
|
647
651
|
// If template was created in BEE AND BEE is enabled → open in BEE editor
|
|
648
652
|
// Otherwise → open in HTML editor (fallback)
|
|
649
653
|
// IMPORTANT: When supportCKEditor is false, default to HTML editor unless explicitly BEE
|
|
@@ -790,8 +794,6 @@ const useEmailWrapper = ({
|
|
|
790
794
|
// In edit mode (when supportCKEditor is false), always show editor
|
|
791
795
|
if (!supportCKEditorFlag && isEditMode) {
|
|
792
796
|
// Check if it's explicitly BEE editor
|
|
793
|
-
const isExplicitlyBEE = emailCreateMode === EMAIL_CREATE_MODES.DRAG_DROP
|
|
794
|
-
|| (emailProps?.editor === 'BEE' && emailProps?.selectedEditorMode === null);
|
|
795
797
|
// Show editor for both BEE and HTML in edit mode
|
|
796
798
|
return true;
|
|
797
799
|
}
|
|
@@ -768,6 +768,130 @@ describe('useEmailWrapper', () => {
|
|
|
768
768
|
});
|
|
769
769
|
});
|
|
770
770
|
|
|
771
|
+
describe('templateId resolution (lines 209-213)', () => {
|
|
772
|
+
it('should call getTemplateDetails with templateId from params.id', async () => {
|
|
773
|
+
const templateId = 'from-params-id';
|
|
774
|
+
const editProps = {
|
|
775
|
+
...newFlowMockProps,
|
|
776
|
+
params: { id: templateId },
|
|
777
|
+
location: { pathname: '/email/edit/other', query: {} },
|
|
778
|
+
Email: {
|
|
779
|
+
...newFlowMockProps.Email,
|
|
780
|
+
templateDetails: null,
|
|
781
|
+
getTemplateDetailsInProgress: false,
|
|
782
|
+
},
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
786
|
+
initialProps: editProps,
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
await waitFor(() => {
|
|
790
|
+
expect(mockEmailActions.getTemplateDetails).toHaveBeenCalledWith(templateId, 'email');
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('should call getTemplateDetails with templateId from location.query.id when params.id is missing', async () => {
|
|
795
|
+
const templateId = 'from-query-id';
|
|
796
|
+
const editProps = {
|
|
797
|
+
...newFlowMockProps,
|
|
798
|
+
params: {},
|
|
799
|
+
location: {
|
|
800
|
+
pathname: '/email/edit/something',
|
|
801
|
+
query: { id: templateId },
|
|
802
|
+
},
|
|
803
|
+
Email: {
|
|
804
|
+
...newFlowMockProps.Email,
|
|
805
|
+
templateDetails: null,
|
|
806
|
+
getTemplateDetailsInProgress: false,
|
|
807
|
+
},
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
811
|
+
initialProps: editProps,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
await waitFor(() => {
|
|
815
|
+
expect(mockEmailActions.getTemplateDetails).toHaveBeenCalledWith(templateId, 'email');
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('should call getTemplateDetails with templateId from location.params.id when params and query id are missing', async () => {
|
|
820
|
+
const templateId = 'from-location-params-id';
|
|
821
|
+
const editProps = {
|
|
822
|
+
...newFlowMockProps,
|
|
823
|
+
params: {},
|
|
824
|
+
location: {
|
|
825
|
+
pathname: '/email/edit/fallback',
|
|
826
|
+
query: {},
|
|
827
|
+
params: { id: templateId },
|
|
828
|
+
},
|
|
829
|
+
Email: {
|
|
830
|
+
...newFlowMockProps.Email,
|
|
831
|
+
templateDetails: null,
|
|
832
|
+
getTemplateDetailsInProgress: false,
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
837
|
+
initialProps: editProps,
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
await waitFor(() => {
|
|
841
|
+
expect(mockEmailActions.getTemplateDetails).toHaveBeenCalledWith(templateId, 'email');
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('should call getTemplateDetails with templateId extracted from pathname /edit/ID when no params or query id', async () => {
|
|
846
|
+
const templateId = 'extracted-from-pathname';
|
|
847
|
+
const editProps = {
|
|
848
|
+
...newFlowMockProps,
|
|
849
|
+
params: {},
|
|
850
|
+
location: {
|
|
851
|
+
pathname: `/email/edit/${templateId}`,
|
|
852
|
+
query: {},
|
|
853
|
+
},
|
|
854
|
+
Email: {
|
|
855
|
+
...newFlowMockProps.Email,
|
|
856
|
+
templateDetails: null,
|
|
857
|
+
getTemplateDetailsInProgress: false,
|
|
858
|
+
},
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
862
|
+
initialProps: editProps,
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
await waitFor(() => {
|
|
866
|
+
expect(mockEmailActions.getTemplateDetails).toHaveBeenCalledWith(templateId, 'email');
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('should not call getTemplateDetails when pathname includes /edit/ but has no id segment', async () => {
|
|
871
|
+
const editProps = {
|
|
872
|
+
...newFlowMockProps,
|
|
873
|
+
params: {},
|
|
874
|
+
location: {
|
|
875
|
+
pathname: '/email/edit/',
|
|
876
|
+
query: {},
|
|
877
|
+
},
|
|
878
|
+
Email: {
|
|
879
|
+
...newFlowMockProps.Email,
|
|
880
|
+
templateDetails: null,
|
|
881
|
+
getTemplateDetailsInProgress: false,
|
|
882
|
+
},
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
renderHook((props) => useEmailWrapper(props), {
|
|
886
|
+
initialProps: editProps,
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
await waitFor(() => {
|
|
890
|
+
expect(mockEmailActions.getTemplateDetails).not.toHaveBeenCalled();
|
|
891
|
+
}, { timeout: 500 });
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
771
895
|
describe('Edit Flow - BEE Editor Template', () => {
|
|
772
896
|
it('should call getTemplateDetails and set BEE editor for BEE template', async () => {
|
|
773
897
|
const templateId = 'bee-template-123';
|
|
@@ -1155,6 +1155,7 @@ export const InApp = (props) => {
|
|
|
1155
1155
|
location,
|
|
1156
1156
|
tagModule: getDefaultTags,
|
|
1157
1157
|
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1158
|
+
isFullMode,
|
|
1158
1159
|
}) || {};
|
|
1159
1160
|
|
|
1160
1161
|
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
@@ -1182,6 +1183,7 @@ export const InApp = (props) => {
|
|
|
1182
1183
|
location,
|
|
1183
1184
|
tagModule: getDefaultTags,
|
|
1184
1185
|
eventContextTags: metaEntities?.eventContextTags || [],
|
|
1186
|
+
isFullMode,
|
|
1185
1187
|
}) || {};
|
|
1186
1188
|
|
|
1187
1189
|
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
@@ -878,6 +878,7 @@ export const InappAdvanced = (props) => {
|
|
|
878
878
|
location,
|
|
879
879
|
tagModule: getDefaultTags,
|
|
880
880
|
eventContextTags: metaEntities?.eventContextTags || [],
|
|
881
|
+
isFullMode,
|
|
881
882
|
}) || {};
|
|
882
883
|
|
|
883
884
|
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
@@ -905,6 +906,7 @@ export const InappAdvanced = (props) => {
|
|
|
905
906
|
location,
|
|
906
907
|
tagModule: getDefaultTags,
|
|
907
908
|
eventContextTags: metaEntities?.eventContextTags || [],
|
|
909
|
+
isFullMode,
|
|
908
910
|
}) || {};
|
|
909
911
|
|
|
910
912
|
if (validationResponse?.unsupportedTags?.length > 0) {
|
|
@@ -63,6 +63,8 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
63
63
|
injectedTags: {},
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
+
this.hasFetchedInitialTagsRef = false;
|
|
67
|
+
this.lastFetchedTagContextRef = null;
|
|
66
68
|
}
|
|
67
69
|
componentWillMount = () => {
|
|
68
70
|
if (this.props.route.name === 'view') {
|
|
@@ -132,16 +134,19 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
132
134
|
}
|
|
133
135
|
schema.standalone.sections.splice(1, 1);
|
|
134
136
|
this.injectEvents(schema);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
if (!this.hasFetchedInitialTagsRef) {
|
|
138
|
+
this.hasFetchedInitialTagsRef = true;
|
|
139
|
+
const context = this.props.getDefaultTags
|
|
140
|
+
|| (this.props.location.query.type === 'embedded' ? this.props.location.query.module : 'default');
|
|
141
|
+
this.lastFetchedTagContextRef = typeof context === 'string' ? context.toLowerCase() : context;
|
|
142
|
+
const query = {
|
|
143
|
+
layout: 'mobilepush',
|
|
144
|
+
type: 'TAG',
|
|
145
|
+
context: this.lastFetchedTagContextRef,
|
|
146
|
+
embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
|
|
147
|
+
};
|
|
148
|
+
this.props.globalActions.fetchSchemaForEntity(query);
|
|
143
149
|
}
|
|
144
|
-
this.props.globalActions.fetchSchemaForEntity(query);
|
|
145
150
|
}
|
|
146
151
|
};
|
|
147
152
|
componentWillUnmount = () => {
|
|
@@ -1771,10 +1776,15 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
1771
1776
|
this.injectEvents(schema);
|
|
1772
1777
|
};
|
|
1773
1778
|
handleOnTagsContextChange = (data) => {
|
|
1779
|
+
const context = (data || '').toLowerCase() === 'all' ? 'default' : (data || '').toLowerCase();
|
|
1780
|
+
if (this.lastFetchedTagContextRef === context) {
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
this.lastFetchedTagContextRef = context;
|
|
1774
1784
|
const query = {
|
|
1775
1785
|
layout: 'mobilepush',
|
|
1776
1786
|
type: 'TAG',
|
|
1777
|
-
context
|
|
1787
|
+
context,
|
|
1778
1788
|
embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
|
|
1779
1789
|
};
|
|
1780
1790
|
this.props.globalActions.fetchSchemaForEntity(query);
|
|
@@ -68,6 +68,10 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
68
68
|
this.getPrimaryCtaFields = getPrimaryCtaFields.bind(this);
|
|
69
69
|
this.getSecondaryCtaFields = getSecondaryCtaFields.bind(this);
|
|
70
70
|
this.getLinkTypeFields = getLinkTypeFields.bind(this);
|
|
71
|
+
// Guard: only one initial meta/TAG fetch (getTags can be invoked from multiple code paths)
|
|
72
|
+
this.hasFetchedInitialTagsRef = false;
|
|
73
|
+
// Guard: avoid duplicate fetch when multiple TagList instances trigger same context
|
|
74
|
+
this.lastFetchedTagContextRef = null;
|
|
71
75
|
}
|
|
72
76
|
componentWillMount() {
|
|
73
77
|
this.props.actions.getWeCrmAccounts("mobilepush");
|
|
@@ -109,6 +113,10 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
componentWillReceiveProps(nextProps) {
|
|
116
|
+
if (nextProps.params?.id !== this.props.params?.id) {
|
|
117
|
+
this.hasFetchedInitialTagsRef = false;
|
|
118
|
+
this.lastFetchedTagContextRef = null;
|
|
119
|
+
}
|
|
112
120
|
if (nextProps.isGetFormData && !this.props.isFullMode) {
|
|
113
121
|
nextProps.getFormLibraryData(this.getFormData());
|
|
114
122
|
} else if (nextProps.isGetFormData && this.props.isGetFormData !== nextProps.isGetFormData && this.props.isFullMode && !this.props.Create.createTemplateInProgress) {
|
|
@@ -167,11 +175,13 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
167
175
|
};
|
|
168
176
|
this.props.actions.getMobilepushTemplatesList('mobilepush', params);
|
|
169
177
|
}
|
|
170
|
-
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.fullSchema)
|
|
178
|
+
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.fullSchema)) {
|
|
171
179
|
this.setState({fullSchema: nextProps.metaEntities.layouts[0].definition, schema: (nextProps.location.query.module === 'loyalty') ? nextProps.metaEntities.layouts[0].definition.textSchema : {}}, () => {
|
|
172
|
-
this.
|
|
180
|
+
// Use this.props (latest) in callback to avoid race: templateDetails may have arrived by now
|
|
181
|
+
const latestSelectedAccount = this.getSelectedWeChatAccountFromProps(this.props);
|
|
182
|
+
this.handleEditSchemaOnPropsChange(this.props, latestSelectedAccount);
|
|
173
183
|
const templateId = get(this, "props.params.id");
|
|
174
|
-
if (
|
|
184
|
+
if (this.props.location.query.module !== 'loyalty' && templateId && templateId !== 'temp') {
|
|
175
185
|
this.props.actions.getTemplateDetails(templateId);
|
|
176
186
|
}
|
|
177
187
|
if (queryType === EMBEDDED && templateId === 'temp' && _.isEmpty(this.state.formData)) { // when his.props.params.id is temp that means mobile push template content will be passed from post message from parent with startTemplateCreation action
|
|
@@ -719,6 +729,27 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
719
729
|
parent.postMessage(JSON.stringify(response), '*');
|
|
720
730
|
};
|
|
721
731
|
|
|
732
|
+
/**
|
|
733
|
+
* Compute selectedWeChatAccount from props (used so we can call with latest props
|
|
734
|
+
* in setState callback to avoid stale closure and intermittent empty form).
|
|
735
|
+
*/
|
|
736
|
+
getSelectedWeChatAccountFromProps = (props) => {
|
|
737
|
+
const queryType = String(get(props, 'location.query.type', ''))?.toLowerCase();
|
|
738
|
+
const creativesMode = String(get(props, 'creativesMode', ''))?.toLowerCase();
|
|
739
|
+
const { Edit: EditProps, Templates } = props || {};
|
|
740
|
+
const { selectedWeChatAccount: editSelectedWeChatAccount } = EditProps || {};
|
|
741
|
+
const { Templates: nextTemplates } = props || {};
|
|
742
|
+
if (isEmbeddedEditOrPreview(queryType, creativesMode)) {
|
|
743
|
+
return !_.isEmpty(editSelectedWeChatAccount)
|
|
744
|
+
? editSelectedWeChatAccount
|
|
745
|
+
: nextTemplates?.selectedWeChatAccount;
|
|
746
|
+
}
|
|
747
|
+
if (!_.isEmpty(Templates?.selectedWeChatAccount)) {
|
|
748
|
+
return Templates?.selectedWeChatAccount;
|
|
749
|
+
}
|
|
750
|
+
return undefined;
|
|
751
|
+
};
|
|
752
|
+
|
|
722
753
|
getFormData = (e) => {
|
|
723
754
|
const response = {
|
|
724
755
|
action: "getFormData",
|
|
@@ -741,6 +772,9 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
741
772
|
formData["mobilepush-accounts"] = this.state.formData["mobilepush-accounts"];
|
|
742
773
|
formData['mobilepush-template'] = this.state.formData['mobilepush-template'];
|
|
743
774
|
}
|
|
775
|
+
if (data.definition?.accountId) {
|
|
776
|
+
formData['mobilepush-accounts'] = data.definition.accountId;
|
|
777
|
+
}
|
|
744
778
|
formData['template-name'] = data.name;
|
|
745
779
|
const androidData = data.versions.base.ANDROID;
|
|
746
780
|
const iosData = data.versions.base.IOS;
|
|
@@ -968,15 +1002,19 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
968
1002
|
});
|
|
969
1003
|
};
|
|
970
1004
|
getTags = () => {
|
|
1005
|
+
if (this.hasFetchedInitialTagsRef) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
this.hasFetchedInitialTagsRef = true;
|
|
1009
|
+
const context = this.props.getDefaultTags
|
|
1010
|
+
|| (this.props.location.query.type === 'embedded' ? this.props.location.query.module : 'default');
|
|
1011
|
+
this.lastFetchedTagContextRef = typeof context === 'string' ? context.toLowerCase() : context;
|
|
971
1012
|
const query = {
|
|
972
1013
|
layout: 'mobilepush',
|
|
973
1014
|
type: 'TAG',
|
|
974
|
-
context: this.
|
|
1015
|
+
context: this.lastFetchedTagContextRef,
|
|
975
1016
|
embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
|
|
976
1017
|
};
|
|
977
|
-
if (this.props.getDefaultTags) {
|
|
978
|
-
query.context = this.props.getDefaultTags;
|
|
979
|
-
}
|
|
980
1018
|
this.props.globalActions.fetchSchemaForEntity(query);
|
|
981
1019
|
}
|
|
982
1020
|
setModalContent = (type) => {
|
|
@@ -1967,10 +2005,15 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1967
2005
|
this.setState({ schema });
|
|
1968
2006
|
};
|
|
1969
2007
|
handleOnTagsContextChange = (data) => {
|
|
2008
|
+
const context = (data || '').toLowerCase() === 'all' ? 'default' : (data || '').toLowerCase();
|
|
2009
|
+
if (this.lastFetchedTagContextRef === context) {
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
this.lastFetchedTagContextRef = context;
|
|
1970
2013
|
const query = {
|
|
1971
2014
|
layout: 'mobilepush',
|
|
1972
2015
|
type: 'TAG',
|
|
1973
|
-
context
|
|
2016
|
+
context,
|
|
1974
2017
|
embedded: this.props.location.query.type === 'embedded' ? this.props.location.query.type : 'full',
|
|
1975
2018
|
};
|
|
1976
2019
|
this.props.globalActions.fetchSchemaForEntity(query);
|
|
@@ -1987,8 +2030,23 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1987
2030
|
}
|
|
1988
2031
|
|
|
1989
2032
|
handleEditSchemaOnPropsChange = (nextProps, selectedWeChatAccount) => {
|
|
1990
|
-
|
|
2033
|
+
const queryType = String(get(this.props, 'location.query.type', ''))?.toLowerCase();
|
|
2034
|
+
const isEmbeddedLibrary = queryType === EMBEDDED && !nextProps.isFullMode;
|
|
2035
|
+
const canSetAccountFromTemplate =
|
|
2036
|
+
!selectedWeChatAccount &&
|
|
2037
|
+
nextProps.templateDetails?.definition?.accountId &&
|
|
2038
|
+
nextProps.Edit?.weCrmAccounts?.length > 0;
|
|
2039
|
+
const canPopulateForm =
|
|
2040
|
+
!_.isEmpty(nextProps.templateDetails) &&
|
|
2041
|
+
_.isEmpty(this.state.editData) &&
|
|
2042
|
+
!_.isEmpty(this.state.fullSchema) &&
|
|
2043
|
+
(this.props.location.query.type !== 'embedded' || this.props.isFullMode === false) &&
|
|
2044
|
+
(selectedWeChatAccount || isEmbeddedLibrary || canSetAccountFromTemplate);
|
|
2045
|
+
if (canPopulateForm) {
|
|
1991
2046
|
this.props = nextProps;
|
|
2047
|
+
if (canSetAccountFromTemplate) {
|
|
2048
|
+
this.setMobilePushAccountOptions(nextProps.Edit.weCrmAccounts, nextProps.templateDetails.definition.accountId);
|
|
2049
|
+
}
|
|
1992
2050
|
const mode = nextProps.templateDetails.definition ? nextProps.templateDetails.definition.mode : nextProps.templateDetails.mode;
|
|
1993
2051
|
const schema = mode === "text" ? this.state.fullSchema?.textSchema : this.state.fullSchema?.imageSchema;
|
|
1994
2052
|
const isAndroidSupported = get(this, "props.Templates.selectedWeChatAccount.configs.android") === '1';
|
|
@@ -2023,8 +2081,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2023
2081
|
<CapSpin spinning={spinning}>
|
|
2024
2082
|
<CapRow>
|
|
2025
2083
|
<CapColumn>
|
|
2026
|
-
<FormBuilder
|
|
2027
|
-
key={!_.isEmpty(schema) ? 'has-schema' : 'no-schema'}
|
|
2084
|
+
{!this.props.isLoadingMetaEntities && <FormBuilder
|
|
2028
2085
|
channel={MOBILE_PUSH}
|
|
2029
2086
|
schema={schema}
|
|
2030
2087
|
showLiquidErrorInFooter={this.props.showLiquidErrorInFooter}
|
|
@@ -2059,7 +2116,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
2059
2116
|
hideTestAndPreviewBtn={this.props.hideTestAndPreviewBtn}
|
|
2060
2117
|
isFullMode={this.props.isFullMode}
|
|
2061
2118
|
eventContextTags={this.props?.eventContextTags}
|
|
2062
|
-
/>
|
|
2119
|
+
/>}
|
|
2063
2120
|
</CapColumn>
|
|
2064
2121
|
{this.props.iosCtasData && this.state.showIosCtaTable &&
|
|
2065
2122
|
<CapSlideBox
|