@capillarytech/creatives-library 9.0.70-alpha.sprint-cap-213712.13 → 9.0.70
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/v2Components/FormBuilder/Classic.js +3 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +39 -30
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
- package/v2Containers/CreativesContainer/constants.js +1 -0
- package/v2Containers/CreativesContainer/index.js +26 -1
- package/v2Containers/CreativesContainer/messages.js +4 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +40 -12
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +10 -0
- package/v2Containers/CreativesContainer/tests/switchToEditAfterCreate.test.js +152 -0
- package/v2Containers/Email/index.js +41 -6
- package/v2Containers/Email/tests/index.test.js +123 -0
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +32 -12
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +34 -0
- package/v2Containers/InApp/index.js +19 -23
- package/v2Containers/InApp/tests/index.test.js +52 -9
- package/v2Containers/InappAdvance/index.js +19 -24
- package/v2Containers/InappAdvance/tests/index.test.js +39 -15
- package/v2Containers/Line/Container/index.js +18 -8
- package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +129 -108
- package/v2Containers/Line/Container/tests/index.test.js +68 -0
- package/v2Containers/MobilePush/Create/index.js +24 -5
- package/v2Containers/MobilePush/Create/test/stayOpenOnSave.test.js +80 -0
- package/v2Containers/MobilePush/Edit/index.js +15 -9
- package/v2Containers/MobilePush/Edit/test/stayOpenOnSave.test.js +60 -0
- package/v2Containers/MobilePushNew/index.js +41 -7
- package/v2Containers/MobilePushNew/tests/index.test.js +61 -0
- package/v2Containers/Sms/Create/index.js +32 -14
- package/v2Containers/Sms/Create/test/stayOpenOnSave.test.js +92 -0
- package/v2Containers/Sms/Edit/index.js +22 -14
- package/v2Containers/Sms/Edit/tests/index.test.js +66 -0
- package/v2Containers/Templates/index.js +17 -4
- package/v2Containers/Viber/index.js +15 -6
- package/v2Containers/Viber/tests/index.test.js +97 -28
- package/v2Containers/WebPush/Create/components/FormActions.js +1 -1
- package/v2Containers/WebPush/Create/components/FormActions.test.js +3 -3
- package/v2Containers/WebPush/Create/index.js +32 -6
- package/v2Containers/WebPush/Create/messages.js +0 -4
- package/v2Containers/WebPush/Create/tests/stayOpenOnSave.test.js +268 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { shallow } from 'enzyme';
|
|
3
|
+
import { Edit } from '../index';
|
|
4
|
+
|
|
5
|
+
jest.mock('@capillarytech/cap-ui-library', () => ({
|
|
6
|
+
...jest.requireActual('@capillarytech/cap-ui-library'),
|
|
7
|
+
CapNotification: { success: jest.fn(), error: jest.fn() },
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const baseProps = (overrides = {}) => ({
|
|
11
|
+
actions: {
|
|
12
|
+
getWeCrmAccounts: jest.fn(),
|
|
13
|
+
setTemplateDetails: jest.fn(),
|
|
14
|
+
getTemplateDetails: jest.fn(),
|
|
15
|
+
clearEditResponse: jest.fn(),
|
|
16
|
+
clearData: jest.fn(),
|
|
17
|
+
editTemplate: jest.fn(),
|
|
18
|
+
},
|
|
19
|
+
globalActions: { fetchSchemaForEntity: jest.fn(), addMessageToQueue: jest.fn() },
|
|
20
|
+
Templates: { selectedWeChatAccount: { id: 'acc-1' } },
|
|
21
|
+
Edit: {},
|
|
22
|
+
Create: {},
|
|
23
|
+
params: {},
|
|
24
|
+
templateData: {},
|
|
25
|
+
location: { query: { type: 'library', module: 'default' }, pathname: '/mobilepush/edit/' },
|
|
26
|
+
route: { name: 'mobilepush' },
|
|
27
|
+
router: { push: jest.fn() },
|
|
28
|
+
metaEntities: {},
|
|
29
|
+
intl: { formatMessage: (m) => (m && (m.defaultMessage || m.id)) || '' },
|
|
30
|
+
isFullMode: true,
|
|
31
|
+
getFormLibraryData: jest.fn(),
|
|
32
|
+
onValidationFail: jest.fn(),
|
|
33
|
+
...overrides,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('MobilePush/Edit — full mode keeps the slidebox open after saving', () => {
|
|
37
|
+
it('keeps the form (and its _id) and asks the host to stay open', () => {
|
|
38
|
+
const props = baseProps();
|
|
39
|
+
const wrapper = shallow(<Edit {...props} />);
|
|
40
|
+
const instance = wrapper.instance();
|
|
41
|
+
instance.getTransformedData = () => ({ _id: 'mp-9', name: 'Push', definition: { mode: 'text' } });
|
|
42
|
+
const formData = { 0: { 'message-title': 'Hi' } };
|
|
43
|
+
wrapper.setState({ formData, editData: { _id: 'mp-9' } });
|
|
44
|
+
|
|
45
|
+
instance.onUpdateTemplateComplete({ templateId: 'mp-9' });
|
|
46
|
+
|
|
47
|
+
expect(wrapper.state('formData')).toEqual(formData);
|
|
48
|
+
expect(wrapper.state('editData')).toEqual({ _id: 'mp-9' });
|
|
49
|
+
expect(props.getFormLibraryData).toHaveBeenCalledWith({ validity: true, keepSlideBoxOpen: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('resets the host Save trigger when the update fails so the user can retry', () => {
|
|
53
|
+
const props = baseProps({ Edit: { editTemplateError: false } });
|
|
54
|
+
const wrapper = shallow(<Edit {...props} />);
|
|
55
|
+
|
|
56
|
+
wrapper.setProps({ Edit: { editTemplateError: true, editTemplateErrorMessage: 'boom' } });
|
|
57
|
+
|
|
58
|
+
expect(props.onValidationFail).toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -8,7 +8,9 @@ import {
|
|
|
8
8
|
injectReducer,
|
|
9
9
|
injectSaga,
|
|
10
10
|
} from "@capillarytech/vulcan-react-sdk/utils";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
cloneDeep, get, isEmpty, isPlainObject,
|
|
13
|
+
} from "lodash";
|
|
12
14
|
import CapCheckbox from "@capillarytech/cap-ui-library/CapCheckbox";
|
|
13
15
|
import CapInput from "@capillarytech/cap-ui-library/CapInput";
|
|
14
16
|
import CapButton from "@capillarytech/cap-ui-library/CapButton";
|
|
@@ -541,6 +543,9 @@ export const MobilePushNew = ({
|
|
|
541
543
|
const [showTestAndPreviewSlidebox, setShowTestAndPreviewSlidebox] = useState(false);
|
|
542
544
|
|
|
543
545
|
const [iosContent, setIosContent] = useState(INITIAL_CONTENT);
|
|
546
|
+
// Full mode: id of the template created in this session — the form stays as is and later saves update it
|
|
547
|
+
const [savedTemplateId, setSavedTemplateId] = useState('');
|
|
548
|
+
const prevIsGetFormDataRef = useRef(false);
|
|
544
549
|
// Refs to track object references for debugging
|
|
545
550
|
const prevEditDataRef = useRef(null);
|
|
546
551
|
const prevUploadedAssetDataRef = useRef(null);
|
|
@@ -2405,9 +2410,10 @@ export const MobilePushNew = ({
|
|
|
2405
2410
|
intl,
|
|
2406
2411
|
});
|
|
2407
2412
|
|
|
2408
|
-
//
|
|
2409
|
-
|
|
2410
|
-
|
|
2413
|
+
// Update when editing, or when continuing a template created earlier in this session (full mode)
|
|
2414
|
+
const isUpdate = ((isEditMode || computedCreativesMode === 'edit') && !!templateId) || !!savedTemplateId;
|
|
2415
|
+
if (isUpdate) {
|
|
2416
|
+
payload._id = templateId || savedTemplateId;
|
|
2411
2417
|
}
|
|
2412
2418
|
|
|
2413
2419
|
const content = getContent(payload);
|
|
@@ -2427,7 +2433,7 @@ export const MobilePushNew = ({
|
|
|
2427
2433
|
channel: MOBILE_PUSH_CHANNEL,
|
|
2428
2434
|
timeTaken,
|
|
2429
2435
|
content,
|
|
2430
|
-
mode:
|
|
2436
|
+
mode: isUpdate ? EDIT : CREATE,
|
|
2431
2437
|
imageAdded: definitionMode === IMAGE.toLowerCase(),
|
|
2432
2438
|
});
|
|
2433
2439
|
// --- BEGIN: Library mode communication fix ---
|
|
@@ -2460,6 +2466,29 @@ export const MobilePushNew = ({
|
|
|
2460
2466
|
}
|
|
2461
2467
|
// --- END: Library mode communication fix ---
|
|
2462
2468
|
|
|
2469
|
+
// Full mode stays open; later saves update the template (host handles edit toast/refresh).
|
|
2470
|
+
if (isFullMode) {
|
|
2471
|
+
if (!isUpdate && response && (response.templateId || response._id)) {
|
|
2472
|
+
const message = `${intl.formatMessage(messages.templateCreateSuccess)}`;
|
|
2473
|
+
CapNotification.success({ key: 'createSuccess', message });
|
|
2474
|
+
const savedResponse = response.templateId || response._id;
|
|
2475
|
+
const newTemplateId = (isPlainObject(savedResponse) ? savedResponse._id : savedResponse) || '';
|
|
2476
|
+
setSavedTemplateId(newTemplateId);
|
|
2477
|
+
setTimeout(() => {
|
|
2478
|
+
mobilePushActions.clearCreateResponse();
|
|
2479
|
+
}, 100);
|
|
2480
|
+
if (getFormLibraryData) {
|
|
2481
|
+
getFormLibraryData({ _id: newTemplateId, validity: true, keepSlideBoxOpen: true });
|
|
2482
|
+
}
|
|
2483
|
+
} else if (isUpdate) {
|
|
2484
|
+
// Let the host's edit-response handler read the response before it is cleared
|
|
2485
|
+
setTimeout(() => {
|
|
2486
|
+
mobilePushActions.clearEditResponse();
|
|
2487
|
+
}, 100);
|
|
2488
|
+
}
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2463
2492
|
// Show success toast and refresh list after create (not edit)
|
|
2464
2493
|
if ((!isEditMode || computedCreativesMode !== 'edit') && response && (response.templateId || response._id)) {
|
|
2465
2494
|
const message = `${intl.formatMessage(messages.templateCreateSuccess)}`;
|
|
@@ -2505,7 +2534,7 @@ export const MobilePushNew = ({
|
|
|
2505
2534
|
|
|
2506
2535
|
// --- Only call create/edit API in full mode ---
|
|
2507
2536
|
if (isFullMode) {
|
|
2508
|
-
if (
|
|
2537
|
+
if (isUpdate) {
|
|
2509
2538
|
mobilePushActions.editTemplate(payload, (response) => {
|
|
2510
2539
|
// Guard: If response is an Error object, show error notification and return
|
|
2511
2540
|
if (response instanceof Error) {
|
|
@@ -2579,6 +2608,8 @@ export const MobilePushNew = ({
|
|
|
2579
2608
|
isCarouselDataValid,
|
|
2580
2609
|
isFullMode,
|
|
2581
2610
|
templateId,
|
|
2611
|
+
savedTemplateId,
|
|
2612
|
+
computedCreativesMode,
|
|
2582
2613
|
]);
|
|
2583
2614
|
|
|
2584
2615
|
// Helper to sync content between platforms
|
|
@@ -2931,7 +2962,10 @@ export const MobilePushNew = ({
|
|
|
2931
2962
|
// In library mode: run liquid validation (extractTags) first; on success liquidMiddleWare calls handleSave
|
|
2932
2963
|
// In full mode: call handleSave directly
|
|
2933
2964
|
useEffect(() => {
|
|
2934
|
-
|
|
2965
|
+
// Save once per Save click (false → true), not on every dependency change.
|
|
2966
|
+
const isNewSaveClick = isGetFormData && !prevIsGetFormDataRef.current;
|
|
2967
|
+
prevIsGetFormDataRef.current = isGetFormData;
|
|
2968
|
+
if (isNewSaveClick) {
|
|
2935
2969
|
if (!isFullMode) {
|
|
2936
2970
|
liquidMiddleWare();
|
|
2937
2971
|
} else {
|
|
@@ -117,3 +117,64 @@ describe('MobilePushNew', () => {
|
|
|
117
117
|
await screen.findByPlaceholderText('Enter title');
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
|
+
|
|
121
|
+
describe('MobilePushNew — full mode keeps the slidebox open after saving', () => {
|
|
122
|
+
const buildProps = (overrides = {}) => ({
|
|
123
|
+
accountData: {
|
|
124
|
+
...accountData, accountId: 'acc-1', licenseCode: 'lic-1', sourceType: 'FCM',
|
|
125
|
+
},
|
|
126
|
+
templateData: {},
|
|
127
|
+
location: {},
|
|
128
|
+
injectedTags: {},
|
|
129
|
+
getDefaultTags: jest.fn(),
|
|
130
|
+
globalActions: { ...globalActions, addMessageToQueue: jest.fn() },
|
|
131
|
+
isFullMode: true,
|
|
132
|
+
isGetFormData: false,
|
|
133
|
+
getFormLibraryData: jest.fn(),
|
|
134
|
+
onValidationFail: jest.fn(),
|
|
135
|
+
onCreateComplete: jest.fn(),
|
|
136
|
+
handleClose: jest.fn(),
|
|
137
|
+
mobilePushActions: {
|
|
138
|
+
createTemplate: jest.fn((payload, callback) => callback({ templateId: 'mp-1' })),
|
|
139
|
+
editTemplate: jest.fn((payload, callback) => callback({ templateId: { _id: 'mp-1' } })),
|
|
140
|
+
clearCreateResponse: jest.fn(),
|
|
141
|
+
clearEditResponse: jest.fn(),
|
|
142
|
+
getWeCrmAccounts: jest.fn(),
|
|
143
|
+
getTemplateDetails: jest.fn(),
|
|
144
|
+
setTemplateDetails: jest.fn(),
|
|
145
|
+
uploadAsset: jest.fn(),
|
|
146
|
+
clearAsset: jest.fn(),
|
|
147
|
+
clearData: jest.fn(),
|
|
148
|
+
getIosCtas: jest.fn(),
|
|
149
|
+
},
|
|
150
|
+
...overrides,
|
|
151
|
+
});
|
|
152
|
+
const ComponentWithStore = (props) => (
|
|
153
|
+
<Provider store={store}>
|
|
154
|
+
<ComponentToRender {...props} />
|
|
155
|
+
</Provider>
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
it('creates on the first save, stays open, and updates the same template on the next save', async () => {
|
|
159
|
+
const props = buildProps();
|
|
160
|
+
const { rerender } = render(<ComponentWithStore {...props} />);
|
|
161
|
+
|
|
162
|
+
fireEvent.change(await screen.findByPlaceholderText('Enter creative name'), { target: { value: 'Promo push' } });
|
|
163
|
+
fireEvent.change(await screen.findByPlaceholderText('Enter title'), { target: { value: 'Hello' } });
|
|
164
|
+
fireEvent.change(await screen.findByPlaceholderText('Enter message'), { target: { value: 'World' } });
|
|
165
|
+
|
|
166
|
+
rerender(<ComponentWithStore {...props} isGetFormData />); // Save click 1
|
|
167
|
+
expect(props.mobilePushActions.createTemplate).toHaveBeenCalledTimes(1);
|
|
168
|
+
expect(props.mobilePushActions.createTemplate.mock.calls[0][0]._id).toBeUndefined();
|
|
169
|
+
expect(props.getFormLibraryData).toHaveBeenCalledWith({ _id: 'mp-1', validity: true, keepSlideBoxOpen: true });
|
|
170
|
+
expect(props.handleClose).not.toHaveBeenCalled();
|
|
171
|
+
expect(props.onCreateComplete).not.toHaveBeenCalled();
|
|
172
|
+
expect(screen.getByPlaceholderText('Enter title')).toHaveValue('Hello'); // form kept as is
|
|
173
|
+
|
|
174
|
+
rerender(<ComponentWithStore {...props} isGetFormData={false} />);
|
|
175
|
+
rerender(<ComponentWithStore {...props} isGetFormData />); // Save click 2
|
|
176
|
+
expect(props.mobilePushActions.editTemplate).toHaveBeenCalledTimes(1);
|
|
177
|
+
expect(props.mobilePushActions.editTemplate.mock.calls[0][0]._id).toBe('mp-1');
|
|
178
|
+
expect(props.handleClose).not.toHaveBeenCalled();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -18,6 +18,7 @@ import './_smsCreate.scss';
|
|
|
18
18
|
import FormBuilder from '../../../v2Components/FormBuilder';
|
|
19
19
|
import * as actions from './actions';
|
|
20
20
|
import messages from './messages';
|
|
21
|
+
import editMessages from '../Edit/messages';
|
|
21
22
|
import * as charCount from '../../../utils/smsCharCount';
|
|
22
23
|
import {checkUnicode, updateCharCount} from '../../../utils/smsCharCountV2';
|
|
23
24
|
import {getMessageObject} from '../../../utils/messageUtils';
|
|
@@ -29,7 +30,7 @@ import { EMBEDDED, FULL } from '../../SmsTrai/Edit/constants';
|
|
|
29
30
|
import {showError} from '../commonMethods';
|
|
30
31
|
import withCreatives from '../../../hoc/withCreatives';
|
|
31
32
|
import { GA } from '@capillarytech/cap-ui-utils';
|
|
32
|
-
import { CREATE, TRACK_CREATE_SMS } from '../../App/constants';
|
|
33
|
+
import { CREATE, EDIT, TRACK_CREATE_SMS } from '../../App/constants';
|
|
33
34
|
import injectReducer from '../../../utils/injectReducer';
|
|
34
35
|
import v2SmsCreateReducer from './reducer';
|
|
35
36
|
import * as globalActions from '../../Cap/actions';
|
|
@@ -57,6 +58,8 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
57
58
|
showTestAndPreviewSlidebox: false,
|
|
58
59
|
isTestAndPreviewMode: false,
|
|
59
60
|
pendingGetFormData: false,
|
|
61
|
+
// Full mode: id of the template created in this session — the form stays as is and later saves update it
|
|
62
|
+
savedTemplateId: '',
|
|
60
63
|
};
|
|
61
64
|
// Tracks the last validity value reported to SmsFallback so componentDidUpdate
|
|
62
65
|
// does not dispatch on every render and create an infinite update loop.
|
|
@@ -263,30 +266,41 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
263
266
|
});
|
|
264
267
|
const {
|
|
265
268
|
formData,
|
|
269
|
+
savedTemplateId,
|
|
266
270
|
} = this.state;
|
|
267
|
-
|
|
271
|
+
const isUpdate = !!savedTemplateId; // a later save of the template created in this session
|
|
272
|
+
if (editResponse?.templateId) {
|
|
273
|
+
const savedResponse = editResponse.templateId; // the template or just its id
|
|
274
|
+
const templateId = (_.isPlainObject(savedResponse) ? savedResponse._id : savedResponse) || savedTemplateId;
|
|
268
275
|
gtmPush('creativeDetails', {
|
|
269
|
-
id:
|
|
276
|
+
id: templateId,
|
|
270
277
|
name: formData?.['template-name'],
|
|
271
278
|
channel: SMS,
|
|
272
279
|
timeTaken,
|
|
273
280
|
content: formData?.base?.['sms-editor'],
|
|
274
|
-
mode: CREATE,
|
|
281
|
+
mode: isUpdate ? EDIT : CREATE,
|
|
275
282
|
});
|
|
276
|
-
this.
|
|
277
|
-
this.setState({
|
|
278
|
-
formData: {},
|
|
279
|
-
tabCount: 1,
|
|
280
|
-
currentTab: 1,
|
|
281
|
-
editData: {},
|
|
282
|
-
});
|
|
283
|
-
const message = this.props.intl.formatMessage(messages.smsTemplateCreatedSuccess);
|
|
283
|
+
const message = this.props.intl.formatMessage(isUpdate ? editMessages.templateEditedSuccessfully : messages.smsTemplateCreatedSuccess);
|
|
284
284
|
CapNotification.success({key: 'createMessageSuccess', message});
|
|
285
285
|
const module = this.props.location.query.module ? this.props.location.query.module : 'default';
|
|
286
286
|
const type = this.props.location.query.type;
|
|
287
287
|
if (this.props.isFullMode) {
|
|
288
|
-
|
|
288
|
+
// Stay open: later saves update this template; first save switches the header.
|
|
289
|
+
this.setState({ savedTemplateId: templateId });
|
|
290
|
+
this.props.getFormSubscriptionData({
|
|
291
|
+
_id: templateId,
|
|
292
|
+
validity: true,
|
|
293
|
+
keepSlideBoxOpen: true,
|
|
294
|
+
...(isUpdate ? {} : { continuedAsEdit: true }),
|
|
295
|
+
});
|
|
289
296
|
} else {
|
|
297
|
+
this.resetSchema();
|
|
298
|
+
this.setState({
|
|
299
|
+
formData: {},
|
|
300
|
+
tabCount: 1,
|
|
301
|
+
currentTab: 1,
|
|
302
|
+
editData: {},
|
|
303
|
+
});
|
|
290
304
|
this.props.router.push({
|
|
291
305
|
pathname: `/sms/`,
|
|
292
306
|
query: type === EMBEDDED ? {type: EMBEDDED, module} : {module},
|
|
@@ -995,7 +1009,8 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
995
1009
|
}
|
|
996
1010
|
|
|
997
1011
|
validateAndSaveFormData = () => {
|
|
998
|
-
|
|
1012
|
+
// Re-arm per Save click (saves only on startValidation false → true).
|
|
1013
|
+
this.setState({startValidation: false}, () => this.setState({startValidation: true}));
|
|
999
1014
|
}
|
|
1000
1015
|
stopValidation = () => {
|
|
1001
1016
|
this.setState({startValidation: false});
|
|
@@ -1083,6 +1098,9 @@ export class Create extends React.Component { // eslint-disable-line react/prefe
|
|
|
1083
1098
|
}
|
|
1084
1099
|
obj.name = formData['template-name'];
|
|
1085
1100
|
obj.versions.base = formData.base;
|
|
1101
|
+
if (this.state.savedTemplateId) {
|
|
1102
|
+
obj._id = this.state.savedTemplateId; // created earlier in this session: update it
|
|
1103
|
+
}
|
|
1086
1104
|
|
|
1087
1105
|
this.props.actions.createTemplate(obj, this.onCreateTemplateComplete);
|
|
1088
1106
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { shallow } from 'enzyme';
|
|
3
|
+
import { Create } from '../index';
|
|
4
|
+
|
|
5
|
+
jest.mock('@capillarytech/cap-ui-library', () => ({
|
|
6
|
+
...jest.requireActual('@capillarytech/cap-ui-library'),
|
|
7
|
+
CapNotification: { success: jest.fn(), error: jest.fn() },
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
// Shallow-render the plain class (bypassing the withCreatives/Redux HOC) to exercise the
|
|
11
|
+
// full-mode save flow: the form stays open and a create continues in place as an update.
|
|
12
|
+
const baseProps = (overrides = {}) => ({
|
|
13
|
+
actions: { createTemplate: jest.fn() },
|
|
14
|
+
globalActions: { fetchSchemaForEntity: jest.fn(), addMessageToQueue: jest.fn() },
|
|
15
|
+
Create: {},
|
|
16
|
+
location: { query: { module: 'default' }, pathname: '/sms/create' },
|
|
17
|
+
route: { name: 'sms' },
|
|
18
|
+
router: { push: jest.fn() },
|
|
19
|
+
metaEntities: {},
|
|
20
|
+
intl: { formatMessage: (m) => (m && (m.defaultMessage || m.id)) || '' },
|
|
21
|
+
isFullMode: true,
|
|
22
|
+
getFormSubscriptionData: jest.fn(),
|
|
23
|
+
...overrides,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const formData = {
|
|
27
|
+
0: { 'sms-editor': 'Hello {{optout}}', base: true },
|
|
28
|
+
base: { 'sms-editor': 'Hello {{optout}}', base: true },
|
|
29
|
+
'template-name': 'Promo',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe('Sms/Create — full mode keeps the slidebox open after saving', () => {
|
|
33
|
+
it('create: keeps the form, remembers the id, and asks the host to stay open + switch the header', () => {
|
|
34
|
+
const props = baseProps();
|
|
35
|
+
const wrapper = shallow(<Create {...props} />);
|
|
36
|
+
wrapper.setState({ formData, tabCount: 1 });
|
|
37
|
+
const instance = wrapper.instance();
|
|
38
|
+
|
|
39
|
+
instance.saveFormData();
|
|
40
|
+
expect(props.actions.createTemplate.mock.calls[0][0]._id).toBeUndefined();
|
|
41
|
+
instance.onCreateTemplateComplete({ templateId: 'sms-1' });
|
|
42
|
+
|
|
43
|
+
expect(wrapper.state('savedTemplateId')).toBe('sms-1');
|
|
44
|
+
expect(wrapper.state('formData')).toEqual(formData);
|
|
45
|
+
expect(props.getFormSubscriptionData).toHaveBeenCalledWith({
|
|
46
|
+
_id: 'sms-1', validity: true, keepSlideBoxOpen: true, continuedAsEdit: true,
|
|
47
|
+
});
|
|
48
|
+
expect(props.router.push).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('the next save updates the created template (and does not re-send the header switch)', () => {
|
|
52
|
+
const props = baseProps();
|
|
53
|
+
const wrapper = shallow(<Create {...props} />);
|
|
54
|
+
wrapper.setState({ formData, tabCount: 1, savedTemplateId: 'sms-1' });
|
|
55
|
+
const instance = wrapper.instance();
|
|
56
|
+
|
|
57
|
+
instance.saveFormData();
|
|
58
|
+
expect(props.actions.createTemplate.mock.calls[0][0]._id).toBe('sms-1');
|
|
59
|
+
instance.onCreateTemplateComplete({ templateId: { _id: 'sms-1' } });
|
|
60
|
+
|
|
61
|
+
expect(props.getFormSubscriptionData).toHaveBeenCalledWith({ _id: 'sms-1', validity: true, keepSlideBoxOpen: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('re-arms validation on every Save click (false, then true)', () => {
|
|
65
|
+
const wrapper = shallow(<Create {...baseProps()} />);
|
|
66
|
+
wrapper.setState({ startValidation: true }); // left set by the previous save
|
|
67
|
+
const instance = wrapper.instance();
|
|
68
|
+
const updates = [];
|
|
69
|
+
const originalSetState = instance.setState.bind(instance);
|
|
70
|
+
instance.setState = (update, callback) => {
|
|
71
|
+
if (update && typeof update === 'object' && 'startValidation' in update) updates.push(update.startValidation);
|
|
72
|
+
return originalSetState(update, callback);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
instance.validateAndSaveFormData();
|
|
76
|
+
|
|
77
|
+
expect(updates).toEqual([false, true]);
|
|
78
|
+
expect(wrapper.state('startValidation')).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('outside full mode keeps the old behaviour (form reset and navigation)', () => {
|
|
82
|
+
const props = baseProps({ isFullMode: false, metaEntities: { layouts: [{ definition: {} }] } });
|
|
83
|
+
const wrapper = shallow(<Create {...props} />);
|
|
84
|
+
wrapper.setState({ formData, tabCount: 1 });
|
|
85
|
+
|
|
86
|
+
wrapper.instance().onCreateTemplateComplete({ templateId: 'sms-1' });
|
|
87
|
+
|
|
88
|
+
expect(wrapper.state('formData')).toEqual({});
|
|
89
|
+
expect(props.router.push).toHaveBeenCalled();
|
|
90
|
+
expect(props.getFormSubscriptionData).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -151,8 +151,10 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
151
151
|
this.setEditState(nextProps.templateDetails);
|
|
152
152
|
}
|
|
153
153
|
if (nextProps.metaEntities && nextProps.metaEntities.layouts && nextProps.metaEntities.layouts.length > 0 && _.isEmpty(this.state.schema)) {
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
// Copy the shared SMS layout (also used by Sms/Create) so Edit's handlers don't leak into it.
|
|
155
|
+
const schema = _.cloneDeep(nextProps.metaEntities.layouts[0].definition);
|
|
156
|
+
this.setState({schema, loadingStatus: this.state.loadingStatus + 1}, () => {
|
|
157
|
+
this.injectEvents(schema);
|
|
156
158
|
});
|
|
157
159
|
if (this.props.location.query.module !== 'library' || (this.props.location.query.module === 'library' && this.props.getDefaultTags)) {
|
|
158
160
|
const query = {
|
|
@@ -170,7 +172,8 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
|
-
|
|
175
|
+
// Full mode stays in edit mode after saving — handled in onUpdateTemplateComplete
|
|
176
|
+
if (!this.props.isFullMode && nextProps.Edit.editResponse && nextProps.Edit.editResponse.templateId) {
|
|
174
177
|
this.resetSchema();
|
|
175
178
|
this.setState({
|
|
176
179
|
formData: {},
|
|
@@ -269,7 +272,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
269
272
|
const {
|
|
270
273
|
formData
|
|
271
274
|
} = this.state;
|
|
272
|
-
if (editResponse
|
|
275
|
+
if (editResponse?.templateId) {
|
|
273
276
|
gtmPush('creativeDetails', {
|
|
274
277
|
id: editResponse.templateId,
|
|
275
278
|
name: formData?.['template-name'],
|
|
@@ -278,22 +281,23 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
278
281
|
content: formData?.base?.['sms-editor'],
|
|
279
282
|
mode: EDIT
|
|
280
283
|
})
|
|
281
|
-
this.resetSchema();
|
|
282
|
-
this.setState({
|
|
283
|
-
formData: {},
|
|
284
|
-
tabCount: 1,
|
|
285
|
-
currentTab: 1,
|
|
286
|
-
editData: {},
|
|
287
|
-
});
|
|
288
284
|
const message = this.props.intl.formatMessage(messages.templateEditedSuccessfully);
|
|
289
285
|
CapNotification.success({key: 'editTemplateSuccess', message});
|
|
290
286
|
this.props.actions.clearEditResponse();
|
|
291
287
|
const module = this.props.location.query.module ? this.props.location.query.module : 'default';
|
|
292
288
|
const type = this.props.location.query.type;
|
|
293
289
|
if (this.props.isFullMode) {
|
|
294
|
-
|
|
295
|
-
|
|
290
|
+
// Stay in edit mode with the saved content; the slidebox is closed only via its close icon
|
|
291
|
+
const {versions, ...rest} = _.isPlainObject(editResponse.templateId) ? editResponse.templateId : {};
|
|
292
|
+
this.props.getFormSubscriptionData({ value: versions, ...rest, validity: true, keepSlideBoxOpen: true });
|
|
296
293
|
} else {
|
|
294
|
+
this.resetSchema();
|
|
295
|
+
this.setState({
|
|
296
|
+
formData: {},
|
|
297
|
+
tabCount: 1,
|
|
298
|
+
currentTab: 1,
|
|
299
|
+
editData: {},
|
|
300
|
+
});
|
|
297
301
|
this.props.router.push({
|
|
298
302
|
pathname: `/sms/`,
|
|
299
303
|
query: type === 'embedded' ? {type: 'embedded', module} : {module},
|
|
@@ -539,7 +543,8 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
539
543
|
}
|
|
540
544
|
|
|
541
545
|
resetSchema() {
|
|
542
|
-
|
|
546
|
+
// Back to Edit's own injected layout (a fresh copy, since versions mutate the active schema)
|
|
547
|
+
this.setState({ schema: _.cloneDeep(this.injectedSchema || this.props.metaEntities?.layouts?.[0]?.definition) });
|
|
543
548
|
}
|
|
544
549
|
|
|
545
550
|
validateFormData(formData) {
|
|
@@ -569,6 +574,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
569
574
|
tempContainer = this.injectContainer(tempContainer);
|
|
570
575
|
return tempContainer;
|
|
571
576
|
});
|
|
577
|
+
this.injectedSchema = _.cloneDeep(schema); // pristine injected layout for resetSchema
|
|
572
578
|
this.setState({schema, loadingStatus: this.state.loadingStatus + 1});
|
|
573
579
|
return schema;
|
|
574
580
|
}
|
|
@@ -982,6 +988,8 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
982
988
|
//Logic to save in db etc
|
|
983
989
|
//saveFormData gets called only when validation result is true
|
|
984
990
|
|
|
991
|
+
// Reset so the next Save click re-triggers FormBuilder (it only reacts to false → true).
|
|
992
|
+
this.stopValidation();
|
|
985
993
|
const editData = _.cloneDeep(this.state.editData);
|
|
986
994
|
const formData = _.cloneDeep(this.state.formData);
|
|
987
995
|
editData.name = formData['template-name'];
|
|
@@ -83,3 +83,69 @@ describe('Sms/Edit — onEmbeddedSmsFooterValidity reporting', () => {
|
|
|
83
83
|
expect(onEmbeddedSmsFooterValidity).not.toHaveBeenCalled();
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
describe('Sms/Edit — full mode saves repeatedly while the slidebox stays open', () => {
|
|
88
|
+
const fullModeProps = () => ({
|
|
89
|
+
...baseProps(),
|
|
90
|
+
isFullMode: true,
|
|
91
|
+
location: { query: { module: 'default' } },
|
|
92
|
+
});
|
|
93
|
+
const savedState = {
|
|
94
|
+
editData: { _id: 'tpl-1', name: 'old', versions: { base: {}, history: [{}] } },
|
|
95
|
+
formData: { 'template-name': 'new', 0: { 'sms-editor': 'Hi' }, base: { 'sms-editor': 'Hi' } },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
it('resets startValidation on submit so the next Save click triggers validation + API again', () => {
|
|
99
|
+
const props = fullModeProps();
|
|
100
|
+
const wrapper = shallow(<Edit {...props} />);
|
|
101
|
+
wrapper.setState(savedState);
|
|
102
|
+
|
|
103
|
+
wrapper.instance().validateAndSaveFormData();
|
|
104
|
+
expect(wrapper.state('startValidation')).toBe(true);
|
|
105
|
+
wrapper.instance().saveFormData(); // FormBuilder's onSubmit after validation passes
|
|
106
|
+
expect(props.actions.editTemplate).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(wrapper.state('startValidation')).toBe(false);
|
|
108
|
+
|
|
109
|
+
// Second Save: a fresh false → true edge for FormBuilder
|
|
110
|
+
wrapper.instance().validateAndSaveFormData();
|
|
111
|
+
expect(wrapper.state('startValidation')).toBe(true);
|
|
112
|
+
wrapper.instance().saveFormData();
|
|
113
|
+
expect(props.actions.editTemplate).toHaveBeenCalledTimes(2);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('keeps the form and asks the host to keep the slidebox open after a successful save', () => {
|
|
117
|
+
const props = fullModeProps();
|
|
118
|
+
const wrapper = shallow(<Edit {...props} />);
|
|
119
|
+
wrapper.setState(savedState);
|
|
120
|
+
|
|
121
|
+
wrapper.instance().onUpdateTemplateComplete({ templateId: { _id: 'tpl-1', versions: { base: {} } } });
|
|
122
|
+
|
|
123
|
+
expect(wrapper.state('formData')).toEqual(savedState.formData);
|
|
124
|
+
expect(props.getFormSubscriptionData).toHaveBeenCalledWith(
|
|
125
|
+
expect.objectContaining({ _id: 'tpl-1', validity: true, keepSlideBoxOpen: true }),
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('Sms/Edit — does not mutate the shared SMS layout', () => {
|
|
131
|
+
it('injects into its own copy so Edit handlers never leak into Create', () => {
|
|
132
|
+
const sharedLayout = {
|
|
133
|
+
standalone: {
|
|
134
|
+
sections: [{
|
|
135
|
+
type: 'multicols',
|
|
136
|
+
inputFields: [{ cols: [{ id: 'save-button', type: 'button', value: 'Save' }] }],
|
|
137
|
+
}],
|
|
138
|
+
},
|
|
139
|
+
containers: [],
|
|
140
|
+
};
|
|
141
|
+
const props = { ...baseProps(), isFullMode: true, location: { query: { module: 'default' } } };
|
|
142
|
+
const wrapper = shallow(<Edit {...props} />);
|
|
143
|
+
|
|
144
|
+
wrapper.setProps({ metaEntities: { layouts: [{ definition: sharedLayout }] } });
|
|
145
|
+
|
|
146
|
+
const ownSaveButton = wrapper.state('schema').standalone.sections[0].inputFields[0].cols[0];
|
|
147
|
+
expect(ownSaveButton.value).toBe('Save');
|
|
148
|
+
expect(ownSaveButton.injectedEvents).toBeDefined();
|
|
149
|
+
expect(sharedLayout.standalone.sections[0].inputFields[0].cols[0].injectedEvents).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -726,9 +726,12 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
726
726
|
});
|
|
727
727
|
|
|
728
728
|
this.getAllTemplates({params, resetPage: true});
|
|
729
|
-
|
|
730
|
-
this
|
|
731
|
-
|
|
729
|
+
|
|
730
|
+
// Open form still uses this data; cleared on close (onCreateComplete).
|
|
731
|
+
if (!this.state.routeParams) {
|
|
732
|
+
this.props.mobilePushNewActions.clearData();
|
|
733
|
+
}
|
|
734
|
+
|
|
732
735
|
this.props.mobilePushNewActions.clearEditResponse();
|
|
733
736
|
|
|
734
737
|
// Reset flag after a delay to allow for next edit
|
|
@@ -1728,6 +1731,10 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
1728
1731
|
}
|
|
1729
1732
|
|
|
1730
1733
|
onCreateComplete = (isReloadTemplates) => {
|
|
1734
|
+
// Push edit data kept for the open slidebox (see the MobilePushNew edit-response handling) is cleared on close
|
|
1735
|
+
if (this.state.channel?.toLowerCase() === MOBILE_PUSH_LOWERCASE && this.props.mobilePushNewActions?.clearData) {
|
|
1736
|
+
this.props.mobilePushNewActions.clearData();
|
|
1737
|
+
}
|
|
1731
1738
|
this.setState({routeParams: null, previewOpen: false}, () => {
|
|
1732
1739
|
if (this.isFullMode() && isReloadTemplates) {
|
|
1733
1740
|
this.getAllTemplates({params: {}}, true);
|
|
@@ -5205,9 +5212,15 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
|
|
|
5205
5212
|
messages={messages}
|
|
5206
5213
|
isFullMode={isFullMode}
|
|
5207
5214
|
templateData={creativesParams}
|
|
5208
|
-
getCreativesData={() => {
|
|
5215
|
+
getCreativesData={(creativesData) => {
|
|
5216
|
+
// Saved but slidebox stays open: refresh the list without closing.
|
|
5217
|
+
if (creativesData?.keepSlideBoxOpen) {
|
|
5218
|
+
this.getAllTemplates({params: {}}, true);
|
|
5219
|
+
return;
|
|
5220
|
+
}
|
|
5209
5221
|
this.onCreateComplete(true);
|
|
5210
5222
|
}}
|
|
5223
|
+
switchToEditAfterCreate={this.canPerform(PERMISSIONS.CREATIVE_EDIT)}
|
|
5211
5224
|
channel={route.name.toUpperCase()}
|
|
5212
5225
|
handleCloseCreatives={this.onCreateComplete}
|
|
5213
5226
|
selectedWeChatAccount={get(
|