@capillarytech/creatives-library 9.0.56-betaa.0 → 9.0.57-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/constants/unified.js +7 -1
- package/package.json +1 -1
- package/services/api.js +25 -4
- package/utils/common.js +0 -4
- package/utils/templateVarUtils.js +16 -0
- package/utils/tests/templateVarUtils.test.js +44 -0
- package/v2Components/CapActionButton/index.js +73 -60
- package/v2Components/CapActionButton/index.scss +44 -28
- package/v2Components/CapActionButton/messages.js +7 -3
- package/v2Components/CapActionButton/tests/index.test.js +17 -1
- package/v2Components/CapWhatsappCTA/messages.js +4 -0
- package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
- package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
- package/v2Components/CommonTestAndPreview/actions.js +13 -0
- package/v2Components/CommonTestAndPreview/constants.js +8 -0
- package/v2Components/CommonTestAndPreview/index.js +55 -3
- package/v2Components/CommonTestAndPreview/reducer.js +20 -0
- package/v2Components/CommonTestAndPreview/sagas.js +28 -0
- package/v2Components/CommonTestAndPreview/selectors.js +6 -0
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
- package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
- package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
- package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +36 -22
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +5 -13
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +6 -4
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +13 -14
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +15 -8
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +31 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +13 -0
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +26 -0
- package/v2Components/FormBuilder/_formBuilder.scss +8 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +9 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +140 -75
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +18 -2
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +67 -28
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +503 -51
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +98 -4
- package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
- package/v2Containers/CommunicationFlow/constants.js +65 -4
- package/v2Containers/CommunicationFlow/index.js +16 -23
- package/v2Containers/CommunicationFlow/messages.js +11 -3
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +27 -11
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +176 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +22 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +14 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +51 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
- package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
- package/v2Containers/CreativesContainer/index.js +29 -3
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -5
- package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
- package/v2Containers/MobilePush/Create/index.js +18 -1
- package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
- package/v2Containers/MobilePush/Edit/index.js +18 -1
- package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
- package/v2Containers/MobilePush/commonMethods.js +49 -1
- package/v2Containers/MobilePushNew/index.js +29 -4
- package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
- package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
- package/v2Containers/MobilePushNew/utils.js +34 -1
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/index.js +34 -0
- package/v2Containers/Rcs/index.scss +15 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +115 -19
- package/v2Containers/Rcs/tests/index.test.js +67 -0
- package/v2Containers/Sms/Create/index.js +3 -1
- package/v2Containers/Sms/Edit/index.js +25 -0
- package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +27 -3
- package/v2Containers/Viber/index.js +24 -1
- package/v2Containers/Viber/tests/index.test.js +103 -0
- package/v2Containers/WebPush/Create/index.js +19 -0
- package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
- package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
- package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
- package/v2Containers/Whatsapp/index.js +25 -4
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +306 -34
- package/v2Containers/Whatsapp/tests/index.test.js +115 -0
- package/v2Containers/Zalo/index.js +28 -1
- package/v2Containers/Zalo/tests/index.test.js +99 -0
- package/v2Containers/mockdata.js +25 -0
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
SEND_TEST_MESSAGE_REQUESTED,
|
|
20
20
|
SEND_TEST_MESSAGE_SUCCESS,
|
|
21
21
|
SEND_TEST_MESSAGE_FAILURE,
|
|
22
|
+
SEND_CCS_TEST_MESSAGE_REQUESTED,
|
|
23
|
+
SEND_CCS_TEST_MESSAGE_SUCCESS,
|
|
24
|
+
SEND_CCS_TEST_MESSAGE_FAILURE,
|
|
22
25
|
CLEAR_CUSTOMER_SEARCH_STATE,
|
|
23
26
|
CLEAR_SEARCH_RESULTS,
|
|
24
27
|
GET_TEST_CUSTOMERS_REQUESTED,
|
|
@@ -69,6 +72,8 @@ describe('previewAndTestReducer', () => {
|
|
|
69
72
|
isSendingTestMessage: false,
|
|
70
73
|
sendTestMessageError: null,
|
|
71
74
|
testMessageResponse: null,
|
|
75
|
+
isSendingCcsTestMessage: false,
|
|
76
|
+
sendCcsTestMessageError: null,
|
|
72
77
|
tags: {
|
|
73
78
|
loading: false,
|
|
74
79
|
error: null,
|
|
@@ -533,6 +538,42 @@ describe('previewAndTestReducer', () => {
|
|
|
533
538
|
});
|
|
534
539
|
});
|
|
535
540
|
|
|
541
|
+
describe('SEND_CCS_TEST_MESSAGE_REQUESTED', () => {
|
|
542
|
+
it('should set the sending flag and clear errors', () => {
|
|
543
|
+
const action = { type: SEND_CCS_TEST_MESSAGE_REQUESTED };
|
|
544
|
+
const result = previewAndTestReducer(initialState, action);
|
|
545
|
+
|
|
546
|
+
expect(result.get('isSendingCcsTestMessage')).toBe(true);
|
|
547
|
+
expect(result.get('sendCcsTestMessageError')).toBeNull();
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe('SEND_CCS_TEST_MESSAGE_SUCCESS', () => {
|
|
552
|
+
it('should clear the sending flag and any error', () => {
|
|
553
|
+
const stateWhileSending = initialState
|
|
554
|
+
.set('isSendingCcsTestMessage', true)
|
|
555
|
+
.set('sendCcsTestMessageError', 'previous error');
|
|
556
|
+
const action = { type: SEND_CCS_TEST_MESSAGE_SUCCESS };
|
|
557
|
+
const result = previewAndTestReducer(stateWhileSending, action);
|
|
558
|
+
|
|
559
|
+
expect(result.get('isSendingCcsTestMessage')).toBe(false);
|
|
560
|
+
expect(result.get('sendCcsTestMessageError')).toBeNull();
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe('SEND_CCS_TEST_MESSAGE_FAILURE', () => {
|
|
565
|
+
it('should clear the sending flag and set the error', () => {
|
|
566
|
+
const action = {
|
|
567
|
+
type: SEND_CCS_TEST_MESSAGE_FAILURE,
|
|
568
|
+
payload: { error: 'NO_LIVE_VERSION' },
|
|
569
|
+
};
|
|
570
|
+
const result = previewAndTestReducer(initialState, action);
|
|
571
|
+
|
|
572
|
+
expect(result.get('isSendingCcsTestMessage')).toBe(false);
|
|
573
|
+
expect(result.get('sendCcsTestMessageError')).toBe('NO_LIVE_VERSION');
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
536
577
|
describe('CLEAR_SEARCH_RESULTS', () => {
|
|
537
578
|
it('should clear search results and reset flags', () => {
|
|
538
579
|
const stateWithResults = initialState
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
extractTagsSaga,
|
|
13
13
|
updatePreviewSaga,
|
|
14
14
|
sendTestMessageSaga,
|
|
15
|
+
sendCcsTestMessageSaga,
|
|
15
16
|
getBulkCustomerDetails,
|
|
16
17
|
fetchTestCustomersSaga,
|
|
17
18
|
fetchTestGroupsSaga,
|
|
@@ -45,6 +46,8 @@ import {
|
|
|
45
46
|
UPDATE_PREVIEW_FAILURE,
|
|
46
47
|
SEND_TEST_MESSAGE_SUCCESS,
|
|
47
48
|
SEND_TEST_MESSAGE_FAILURE,
|
|
49
|
+
SEND_CCS_TEST_MESSAGE_SUCCESS,
|
|
50
|
+
SEND_CCS_TEST_MESSAGE_FAILURE,
|
|
48
51
|
GET_TEST_CUSTOMERS_SUCCESS,
|
|
49
52
|
GET_TEST_CUSTOMERS_FAILURE,
|
|
50
53
|
GET_TEST_GROUPS_SUCCESS,
|
|
@@ -747,6 +750,87 @@ describe('CommonTestAndPreview Sagas', () => {
|
|
|
747
750
|
});
|
|
748
751
|
});
|
|
749
752
|
|
|
753
|
+
describe('sendCcsTestMessageSaga', () => {
|
|
754
|
+
it('should send the CCS test message successfully', () => {
|
|
755
|
+
const callback = jest.fn();
|
|
756
|
+
const action = {
|
|
757
|
+
payload: { commDefinitionId: 'cd_123', recipient: { identifiers: [] } },
|
|
758
|
+
callback,
|
|
759
|
+
};
|
|
760
|
+
const response = { response: { success: true, data: {} } };
|
|
761
|
+
|
|
762
|
+
const generator = sendCcsTestMessageSaga(action);
|
|
763
|
+
|
|
764
|
+
expect(generator.next().value).toEqual(
|
|
765
|
+
call(Api.sendTransactionalComm, action.payload)
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
expect(generator.next(response).value).toEqual(
|
|
769
|
+
put({ type: SEND_CCS_TEST_MESSAGE_SUCCESS })
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
generator.next();
|
|
773
|
+
expect(callback).toHaveBeenCalledWith(true);
|
|
774
|
+
expect(generator.next().done).toBe(true);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('should handle a CCS envelope failure (success: false)', () => {
|
|
778
|
+
const callback = jest.fn();
|
|
779
|
+
const action = {
|
|
780
|
+
payload: { commDefinitionId: 'cd_123', recipient: { identifiers: [] } },
|
|
781
|
+
callback,
|
|
782
|
+
};
|
|
783
|
+
const response = {
|
|
784
|
+
response: { success: false, errors: [{ message: 'NO_LIVE_VERSION' }] },
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
const generator = sendCcsTestMessageSaga(action);
|
|
788
|
+
|
|
789
|
+
generator.next();
|
|
790
|
+
expect(generator.next(response).value).toEqual(
|
|
791
|
+
put({ type: SEND_CCS_TEST_MESSAGE_FAILURE, payload: { error: 'NO_LIVE_VERSION' } })
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
generator.next();
|
|
795
|
+
expect(callback).toHaveBeenCalledWith(false);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('falls back to a default error message when the envelope carries none', () => {
|
|
799
|
+
const callback = jest.fn();
|
|
800
|
+
const action = {
|
|
801
|
+
payload: { commDefinitionId: 'cd_123', recipient: { identifiers: [] } },
|
|
802
|
+
callback,
|
|
803
|
+
};
|
|
804
|
+
const response = { response: { success: false, errors: [] } };
|
|
805
|
+
|
|
806
|
+
const generator = sendCcsTestMessageSaga(action);
|
|
807
|
+
|
|
808
|
+
generator.next();
|
|
809
|
+
expect(generator.next(response).value).toEqual(
|
|
810
|
+
put({ type: SEND_CCS_TEST_MESSAGE_FAILURE, payload: { error: 'Failed to send test message' } })
|
|
811
|
+
);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('should handle network error', () => {
|
|
815
|
+
const callback = jest.fn();
|
|
816
|
+
const action = {
|
|
817
|
+
payload: { commDefinitionId: 'cd_123', recipient: { identifiers: [] } },
|
|
818
|
+
callback,
|
|
819
|
+
};
|
|
820
|
+
const error = new Error('Network error');
|
|
821
|
+
|
|
822
|
+
const generator = sendCcsTestMessageSaga(action);
|
|
823
|
+
|
|
824
|
+
generator.next();
|
|
825
|
+
expect(generator.throw(error).value).toEqual(
|
|
826
|
+
put({ type: SEND_CCS_TEST_MESSAGE_FAILURE, payload: { error: 'Network error' } })
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
generator.next();
|
|
830
|
+
expect(callback).toHaveBeenCalledWith(false);
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
750
834
|
describe('fetchTestCustomersSaga', () => {
|
|
751
835
|
it('should fetch test customers successfully', () => {
|
|
752
836
|
const response = {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
makeSelectPrefilledValues,
|
|
29
29
|
makeSelectTestMessageResponse,
|
|
30
30
|
makeSelectIsSendingTestMessage,
|
|
31
|
+
makeSelectIsSendingCcsTestMessage,
|
|
31
32
|
makeSelectUpdatePreviewError,
|
|
32
33
|
makeSelectUpdatePreviewErrors,
|
|
33
34
|
makeSelectFetchPrefilledValuesError,
|
|
@@ -78,6 +79,8 @@ describe('CommonTestAndPreview Selectors', () => {
|
|
|
78
79
|
isSendingTestEmail: false,
|
|
79
80
|
sendTestMessageError: null,
|
|
80
81
|
testMessageResponse: { messageId: '123', status: 'sent' },
|
|
82
|
+
isSendingCcsTestMessage: false,
|
|
83
|
+
sendCcsTestMessageError: null,
|
|
81
84
|
tags: {
|
|
82
85
|
loading: false,
|
|
83
86
|
error: null,
|
|
@@ -470,6 +473,15 @@ describe('CommonTestAndPreview Selectors', () => {
|
|
|
470
473
|
});
|
|
471
474
|
});
|
|
472
475
|
|
|
476
|
+
describe('makeSelectIsSendingCcsTestMessage', () => {
|
|
477
|
+
it('should select isSendingCcsTestMessage flag', () => {
|
|
478
|
+
const selector = makeSelectIsSendingCcsTestMessage();
|
|
479
|
+
const result = selector(mockState);
|
|
480
|
+
|
|
481
|
+
expect(result).toBe(false);
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
473
485
|
describe('makeSelectUpdatePreviewError', () => {
|
|
474
486
|
it('should select updatePreviewError from state', () => {
|
|
475
487
|
const selector = makeSelectUpdatePreviewError();
|
|
@@ -70,8 +70,7 @@ const FormBuilderShell = (props) => {
|
|
|
70
70
|
|
|
71
71
|
const channel = (channelProp || schema?.channel || SMS).toUpperCase();
|
|
72
72
|
const adapter = getChannelAdapter(channel);
|
|
73
|
-
// Channels ship their own registry via the adapter; SMS predates the hook and
|
|
74
|
-
// stays on its original factory.
|
|
73
|
+
// Channels ship their own registry via the adapter; SMS predates the hook and stays on its original factory.
|
|
75
74
|
const registry = useMemo(
|
|
76
75
|
() => (adapter?.createRegistry ? adapter.createRegistry() : createSmsRegistry()),
|
|
77
76
|
[channel],
|
|
@@ -98,23 +97,16 @@ const FormBuilderShell = (props) => {
|
|
|
98
97
|
const didInitRef = useRef(false);
|
|
99
98
|
const prevStartValidationRef = useRef(startValidation);
|
|
100
99
|
const prevSchemaRef = useRef(null);
|
|
101
|
-
// Deep snapshot of the last parent formData
|
|
102
|
-
//
|
|
103
|
-
// reference-keyed effect would miss genuine pushes — we deep-compare on every
|
|
104
|
-
// render instead, exactly like Classic's componentWillReceiveProps.
|
|
100
|
+
// Deep snapshot of the last processed parent formData — containers mutate it in
|
|
101
|
+
// place (copy-content, tag insertion), so pushes are deep-compared, not ref-keyed.
|
|
105
102
|
const lastSeenParentRef = useRef(null);
|
|
106
|
-
//
|
|
107
|
-
// modal contract): Classic keeps these in state only after a validate pass.
|
|
103
|
+
// android/ios validity from the last save-validation (single-platform modal contract).
|
|
108
104
|
const platformRef = useRef(null);
|
|
109
|
-
// Content snapshot of the tags prop
|
|
110
|
-
// (Classic.js:569-578) — containers rebuild the array each render, so identity
|
|
111
|
-
// is useless. Seeded by the init effect (init already validates with those tags).
|
|
105
|
+
// Content snapshot of the tags prop (containers rebuild the array each render).
|
|
112
106
|
const lastTagsRef = useRef(null);
|
|
113
107
|
const lastNextStateRef = useRef(null);
|
|
114
|
-
// GOLDEN parity
|
|
115
|
-
//
|
|
116
|
-
// tabCount PROP only when hydrating from a parent formData push (edit sync) —
|
|
117
|
-
// so the MPUSH create flow emits tabCount 1 even though the containers pass 2.
|
|
108
|
+
// GOLDEN parity: Classic emits tabCount starting at 1, mirroring the prop only
|
|
109
|
+
// on a parent formData sync — the create flow emits 1 though the containers pass 2.
|
|
118
110
|
const emittedTabCountRef = useRef(1);
|
|
119
111
|
|
|
120
112
|
// Stable handler identities for the memoized FieldSlot: these wrappers delegate to
|
|
@@ -216,6 +208,12 @@ const FormBuilderShell = (props) => {
|
|
|
216
208
|
);
|
|
217
209
|
useEffect(() => () => debouncedEmit.cancel(), [debouncedEmit]);
|
|
218
210
|
|
|
211
|
+
// Classic pushes formData up (props.onChange) on init AND on every schema change
|
|
212
|
+
// (initialiseForm -> resetTabKeys when isSchemaChanged, Classic.js:1906-1911,
|
|
213
|
+
// 740-748) — the MPUSH containers read that copy (copy-content handlers) before
|
|
214
|
+
// any user input. MPUSH-only; SMS keeps its shipped behavior.
|
|
215
|
+
const emitFormDataOnSchemaSync = Boolean(adapter.config?.features?.emitFormDataOnSchemaSync);
|
|
216
|
+
|
|
219
217
|
// ---- initialize once the (possibly async) schema is ready, then validate+emit ----
|
|
220
218
|
const schemaReady = Boolean(schema?.standalone || schema?.containers?.length);
|
|
221
219
|
useEffect(() => {
|
|
@@ -232,6 +230,8 @@ const FormBuilderShell = (props) => {
|
|
|
232
230
|
lastTagsRef.current = cloneDeep(propsRef.current.tags || null);
|
|
233
231
|
const legacy = toLegacy(init);
|
|
234
232
|
emitValidity(runValidate(legacy), legacy);
|
|
233
|
+
if (emitFormDataOnSchemaSync) emitChange(legacy); // seed the container's formData copy
|
|
234
|
+
|
|
235
235
|
}
|
|
236
236
|
}, [schemaReady]);
|
|
237
237
|
|
|
@@ -246,6 +246,7 @@ const FormBuilderShell = (props) => {
|
|
|
246
246
|
// be stale mid container-setState-cascade): hydration-family effects only ARM this
|
|
247
247
|
// flag; the [state]-keyed effect below emits once the new state has rendered.
|
|
248
248
|
const pendingBackgroundValidateRef = useRef(false);
|
|
249
|
+
const pendingEmitChangeRef = useRef(false);
|
|
249
250
|
|
|
250
251
|
// ---- reconcile on a genuine schema change (CTA splices, text<->image swap) ----
|
|
251
252
|
// Runs inside the reducer (reconcileSchema) — see the action's note on stale snapshots.
|
|
@@ -258,6 +259,8 @@ const FormBuilderShell = (props) => {
|
|
|
258
259
|
if (validationActive || validateOnExternalChange) {
|
|
259
260
|
pendingBackgroundValidateRef.current = true;
|
|
260
261
|
}
|
|
262
|
+
// Classic re-emits onChange after a schema-change re-init (Classic.js:1908-1909).
|
|
263
|
+
if (emitFormDataOnSchemaSync) pendingEmitChangeRef.current = true;
|
|
261
264
|
}, [schema]);
|
|
262
265
|
|
|
263
266
|
// ---- re-hydrate on a GENUINE external parent push (edit load / discard / copy) ----
|
|
@@ -282,12 +285,18 @@ const FormBuilderShell = (props) => {
|
|
|
282
285
|
}
|
|
283
286
|
});
|
|
284
287
|
|
|
285
|
-
// ---- background validity emission from the SETTLED state (see
|
|
288
|
+
// ---- background validity / formData emission from the SETTLED state (see flags above) ----
|
|
286
289
|
useEffect(() => {
|
|
287
|
-
if (!didInitRef.current || !pendingBackgroundValidateRef.current) return;
|
|
288
|
-
pendingBackgroundValidateRef.current = false;
|
|
290
|
+
if (!didInitRef.current || (!pendingBackgroundValidateRef.current && !pendingEmitChangeRef.current)) return;
|
|
289
291
|
const legacy = toLegacy(state);
|
|
290
|
-
|
|
292
|
+
if (pendingBackgroundValidateRef.current) {
|
|
293
|
+
pendingBackgroundValidateRef.current = false;
|
|
294
|
+
emitValidity(runValidate(legacy), legacy);
|
|
295
|
+
}
|
|
296
|
+
if (pendingEmitChangeRef.current) {
|
|
297
|
+
pendingEmitChangeRef.current = false;
|
|
298
|
+
emitChange(legacy);
|
|
299
|
+
}
|
|
291
300
|
}, [state]);
|
|
292
301
|
|
|
293
302
|
// ---- re-validate when the tag catalog changes (Classic.js:569-578) ----
|
|
@@ -566,14 +575,19 @@ const FormBuilderShell = (props) => {
|
|
|
566
575
|
// Confirmation modal (Classic getModal 'confirm' variant, Classic.js:597-641).
|
|
567
576
|
// Only channels that declare modal semantics render it (MOBILEPUSH); the footer's
|
|
568
577
|
// primary button carries the modal id — that id IS the handleOk dispatch key.
|
|
578
|
+
// width 520 = the antd Modal default Classic renders (Classic.js:601);
|
|
579
|
+
// CapModal's own default (324) wraps the single-platform prompt to two lines.
|
|
569
580
|
const confirmModal = adapter.modals && modal && (
|
|
570
581
|
<CapModal
|
|
571
|
-
|
|
582
|
+
open={modalVisible}
|
|
583
|
+
width={520}
|
|
572
584
|
title={modal.title || ''}
|
|
573
585
|
onCancel={handleModalCancel}
|
|
574
586
|
footer={[
|
|
575
|
-
<CapButton key="back" onClick={handleModalCancel}>
|
|
576
|
-
<CapButton key="submit" type="primary" id={modal.id} onClick={handleModalOk}>
|
|
587
|
+
<CapButton key="back" onClick={handleModalCancel}>{intl.formatMessage(formMessages.cancel)}</CapButton>,
|
|
588
|
+
<CapButton key="submit" type="primary" id={modal.id} onClick={handleModalOk}>
|
|
589
|
+
{intl.formatMessage(formMessages.yes)}
|
|
590
|
+
</CapButton>,
|
|
577
591
|
]}
|
|
578
592
|
>
|
|
579
593
|
{modal.body || ''}
|
|
@@ -65,18 +65,10 @@ export default {
|
|
|
65
65
|
preview: true,
|
|
66
66
|
imageUpload: true,
|
|
67
67
|
cta: true,
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
// (Classic.js:
|
|
73
|
-
inlineErrorWithLiquid: true,
|
|
74
|
-
// Classic re-validates typing only under startValidation (Classic.js:334-345):
|
|
75
|
-
// no pre-save errorData, so no tag/footer message before a save attempt.
|
|
76
|
-
liveValidation: false,
|
|
77
|
-
// Validity is still maintained in the background (Classic validates + emits on
|
|
78
|
-
// parent pushes / schema changes / tags changes, Classic.js:516-578, 1361) —
|
|
79
|
-
// the containers gate their save flows on it (Create/index.js:101).
|
|
80
|
-
validateOnExternalChange: true,
|
|
68
|
+
bridgeFieldEvents: true, // checkbox/radio/select commits use the performFormDataUpdate signature (true, formData, field)
|
|
69
|
+
inlineErrorWithLiquid: true, // MPUSH keeps the inline textarea error despite liquid (Classic.js:2908-2915)
|
|
70
|
+
liveValidation: false, // Classic re-validates typing only under startValidation (Classic.js:334-345)
|
|
71
|
+
validateOnExternalChange: true, // background validity on parent/schema/tags changes gates the save flows (Classic.js:516-578, 1361)
|
|
72
|
+
emitFormDataOnSchemaSync: true, // push formData up on init + schema re-inits; copy-content handlers read it (Classic.js:1906-1911)
|
|
81
73
|
},
|
|
82
74
|
};
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
* closes the modal (Classic parity). Modal CONTENT stays container-owned.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { ANDROID, IOS } from '../../../../../constants/unified';
|
|
9
|
+
|
|
8
10
|
const mpushModals = {
|
|
9
11
|
handleOk: (id, { platform, submitSingleTab } = {}) => {
|
|
10
12
|
if (!platform || typeof submitSingleTab !== 'function') return;
|
|
11
|
-
if (id ===
|
|
12
|
-
submitSingleTab(
|
|
13
|
-
} else if (id ===
|
|
14
|
-
submitSingleTab(
|
|
13
|
+
if (id === ANDROID && platform.androidValid) {
|
|
14
|
+
submitSingleTab(ANDROID);
|
|
15
|
+
} else if (id === IOS && platform.iosValid) {
|
|
16
|
+
submitSingleTab(IOS);
|
|
15
17
|
}
|
|
16
18
|
},
|
|
17
19
|
};
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
import { validateTagsCore } from '../../../../../utils/tagValidations';
|
|
9
9
|
import { hasPersonalizationTags } from '../../../../../utils/commonUtils';
|
|
10
10
|
import { isUrl } from '../../../../../v2Containers/Line/Container/Wrapper/utils';
|
|
11
|
-
import { DEFAULT as DEFAULT_MODULE } from '../../../../../constants/unified';
|
|
11
|
+
import { DEFAULT as DEFAULT_MODULE, ANDROID, IOS } from '../../../../../constants/unified';
|
|
12
12
|
import { TEMPLATE_NAME_FIELD } from '../../constants';
|
|
13
13
|
import { ERROR_VALUE, TABS, CTA_RULES } from './config';
|
|
14
14
|
|
|
15
15
|
const isTabKey = (key) => /^\d+$/.test(`${key}`);
|
|
16
16
|
|
|
17
|
+
// Tab metadata keys skipped when seeding a tab's error map (Classic.js:1627-1656).
|
|
18
|
+
const ERROR_SEED_SKIP_KEYS = ['tabKey', 'base', 'selectedLanguages'];
|
|
19
|
+
|
|
17
20
|
/** Truthy-value projection of a tab, excluding the given metadata keys. */
|
|
18
21
|
const pickTruthy = (tab, excluded) => {
|
|
19
22
|
const out = {};
|
|
@@ -51,12 +54,11 @@ const runTagValidation = (content, { tags, currentModule, isFullMode }) => {
|
|
|
51
54
|
: { valid: false, isBraceError: false };
|
|
52
55
|
};
|
|
53
56
|
|
|
54
|
-
// Classic seeds EVERY tab's error map from tab 0's keys
|
|
55
|
-
// tabKey/base/selectedLanguages (initializeEditErrorData, Classic.js:1627-1656).
|
|
57
|
+
// Classic seeds EVERY tab's error map from tab 0's keys (initializeEditErrorData).
|
|
56
58
|
const seedTabErrors = (tab0) => {
|
|
57
59
|
const seeded = {};
|
|
58
60
|
Object.keys(tab0 || {}).forEach((key) => {
|
|
59
|
-
if (key
|
|
61
|
+
if (!ERROR_SEED_SKIP_KEYS.includes(key)) seeded[key] = false;
|
|
60
62
|
});
|
|
61
63
|
return seeded;
|
|
62
64
|
};
|
|
@@ -204,10 +206,9 @@ export default function validateMobilePush(formData = {}, options = {}) {
|
|
|
204
206
|
});
|
|
205
207
|
}
|
|
206
208
|
|
|
207
|
-
//
|
|
208
|
-
if (index
|
|
209
|
-
|
|
210
|
-
else if (index == 1) iosValid = isCurrentTabValid;
|
|
209
|
+
// index is a string key ('0'/'1'/metadata) — Number() keeps Classic's `==` semantics.
|
|
210
|
+
if (Number(index) === TABS.ANDROID) androidValid = isCurrentTabValid;
|
|
211
|
+
else if (Number(index) === TABS.IOS) iosValid = isCurrentTabValid;
|
|
211
212
|
});
|
|
212
213
|
|
|
213
214
|
// --- template-name: root, skipped in embedded mode; failure also suppresses
|
|
@@ -224,15 +225,13 @@ export default function validateMobilePush(formData = {}, options = {}) {
|
|
|
224
225
|
// --- single-platform save prompt (Classic.js:1171-1204) ---
|
|
225
226
|
if (isSave && (androidValid || iosValid)) {
|
|
226
227
|
const androidData = pickTruthy(formData[TABS.ANDROID], ['tabKey', 'base']);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
singlePlatformPrompt = 'ios';
|
|
228
|
+
if (isEmptyObject(androidData) && Number(currentTab) === 2) {
|
|
229
|
+
singlePlatformPrompt = IOS;
|
|
230
230
|
} else {
|
|
231
231
|
// Classic's inner rebuild excludes only tabKey — NOT base (line 1191). Preserved.
|
|
232
232
|
const iosData = pickTruthy(formData[TABS.IOS], ['tabKey']);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
singlePlatformPrompt = 'android';
|
|
233
|
+
if (isEmptyObject(iosData) && Number(currentTab) === 1) {
|
|
234
|
+
singlePlatformPrompt = ANDROID;
|
|
236
235
|
}
|
|
237
236
|
}
|
|
238
237
|
}
|
|
@@ -34,6 +34,9 @@ import { ERROR_VALUE } from '../channels/mobilepush/config';
|
|
|
34
34
|
|
|
35
35
|
const { TextArea } = CapInput;
|
|
36
36
|
|
|
37
|
+
// Image types the upload accepts (Classic.js:2746 wrong-file check).
|
|
38
|
+
const ALLOWED_IMAGE_EXTENSIONS = /(\.bmp|\.jpeg|\.png|\.gif|\.avif|\.jpg)$/i;
|
|
39
|
+
|
|
37
40
|
const dataFieldPropTypes = {
|
|
38
41
|
field: PropTypes.object.isRequired,
|
|
39
42
|
value: PropTypes.any,
|
|
@@ -264,12 +267,9 @@ export const UploadField = ({
|
|
|
264
267
|
const files = e?.target?.files;
|
|
265
268
|
const file = files && files[0];
|
|
266
269
|
if (!file) return;
|
|
267
|
-
if (field.supportedExtensions) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// Classic fires the wrong-file event and CONTINUES (no early return) — preserved.
|
|
271
|
-
onEvent(field.submitAction, { file, type: 'wrong file' });
|
|
272
|
-
}
|
|
270
|
+
if (field.supportedExtensions && !ALLOWED_IMAGE_EXTENSIONS.exec(file.name)) {
|
|
271
|
+
// Classic fires the wrong-file event and CONTINUES (no early return) — preserved.
|
|
272
|
+
onEvent(field.submitAction, { file, type: 'wrong file' });
|
|
273
273
|
}
|
|
274
274
|
const objectUrl = (window.URL || window.webkitURL).createObjectURL(file);
|
|
275
275
|
const img = new Image();
|
|
@@ -291,7 +291,7 @@ export const UploadField = ({
|
|
|
291
291
|
{isImage
|
|
292
292
|
? <CapImage src={value} alt={imageProps.alt} style={imageProps.style} />
|
|
293
293
|
: (
|
|
294
|
-
<div
|
|
294
|
+
<div className="fb-upload-placeholder">
|
|
295
295
|
<span className="image-placeholder">{imageProps.placeholder}</span>
|
|
296
296
|
</div>
|
|
297
297
|
)}
|
|
@@ -312,9 +312,12 @@ export const UploadField = ({
|
|
|
312
312
|
/>
|
|
313
313
|
)}
|
|
314
314
|
<form encType="multipart/form-data" id={field.id}>
|
|
315
|
+
{/* Native file input on purpose — openFileDialog clicks it via #fileName;
|
|
316
|
+
CapInput wraps antd's text Input (affix wrapper + suffix), which cannot
|
|
317
|
+
host type="file". Hidden: the visible control is the CapButton below. */}
|
|
315
318
|
<input
|
|
316
319
|
key={field.id}
|
|
317
|
-
|
|
320
|
+
className="fb-upload-file-input"
|
|
318
321
|
id="fileName"
|
|
319
322
|
type="file"
|
|
320
323
|
onChange={onFileChange}
|
|
@@ -325,6 +328,8 @@ export const UploadField = ({
|
|
|
325
328
|
type="link"
|
|
326
329
|
prefix={<CapIcon size="s" type="add-photo" />}
|
|
327
330
|
onClick={openFileDialog}
|
|
331
|
+
// color is state-dependent and must beat CapButton's own runtime styling —
|
|
332
|
+
// kept inline (Classic.js:3399 verbatim).
|
|
328
333
|
style={{ float: 'right', color: `${field.disabled ? FONT_COLOR_04 : FONT_COLOR_05}` }}
|
|
329
334
|
>
|
|
330
335
|
{field.label}
|
|
@@ -428,6 +433,8 @@ export const createMpushRegistry = () => createFieldRegistry({
|
|
|
428
433
|
[FIELD_TYPE.TEXTAREA]: MpushTextAreaField,
|
|
429
434
|
[FIELD_TYPE.CHECKBOX]: MpushCheckboxField,
|
|
430
435
|
[FIELD_TYPE.BUTTON]: ButtonField,
|
|
436
|
+
// TagListField mirrors Classic's SEMANTIC tag-list path (Classic.js:3730-3769):
|
|
437
|
+
// span = field.width || '', field.style applied — the CTA splices rely on it.
|
|
431
438
|
[FIELD_TYPE.TAG_LIST]: TagListField,
|
|
432
439
|
[FIELD_TYPE.RADIO_GROUP]: RadioGroupField,
|
|
433
440
|
[FIELD_TYPE.SELECT]: SelectField,
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import React from 'react';
|
|
23
23
|
import '@testing-library/jest-dom';
|
|
24
24
|
import _ from 'lodash';
|
|
25
|
+
import { notification } from 'antd';
|
|
25
26
|
import { Router } from 'react-router-dom';
|
|
26
27
|
import { render, act, fireEvent } from '../../../../utils/test-utils';
|
|
27
28
|
import history from '../../../../utils/history';
|
|
@@ -279,4 +280,34 @@ describe.each([
|
|
|
279
280
|
expectSaveProceeds(r);
|
|
280
281
|
});
|
|
281
282
|
}
|
|
283
|
+
|
|
284
|
+
// Copy with EMPTY source fields must warn ("Cannot copy empty field values."):
|
|
285
|
+
// the container handler reads its own formData copy (Create/index.js:1456-1465),
|
|
286
|
+
// seeded by the builder's init onChange push (Classic.js:1906-1911 -> 740-748).
|
|
287
|
+
it('copy from Android with empty fields shows the cannot-copy warning', () => {
|
|
288
|
+
const warnSpy = jest.spyOn(notification, 'warning').mockImplementation(() => {});
|
|
289
|
+
try {
|
|
290
|
+
const props = buildProps();
|
|
291
|
+
const ui = (p) => (
|
|
292
|
+
<Router history={history}>
|
|
293
|
+
<Create {...p} />
|
|
294
|
+
</Router>
|
|
295
|
+
);
|
|
296
|
+
const utils = render(ui(props));
|
|
297
|
+
const meta = { layouts: [{ definition: fullDefinition() }], tags: { standard: [] } };
|
|
298
|
+
act(() => {
|
|
299
|
+
utils.rerender(ui({ ...props, metaEntities: meta }));
|
|
300
|
+
});
|
|
301
|
+
act(() => { fireEvent.click(document.querySelectorAll('.ant-tabs-tab')[1]); });
|
|
302
|
+
const copyLink = document.getElementById('copy-android-content2')
|
|
303
|
+
|| document.getElementById('copy-android-content');
|
|
304
|
+
if (!copyLink) throw new Error('no copy-android-content link found');
|
|
305
|
+
act(() => { fireEvent.click(copyLink); });
|
|
306
|
+
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
307
|
+
expect(warnSpy.mock.calls[0][0].description).toBe('Cannot copy empty field values.');
|
|
308
|
+
utils.unmount();
|
|
309
|
+
} finally {
|
|
310
|
+
warnSpy.mockRestore();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
282
313
|
});
|
|
@@ -145,7 +145,20 @@ describe('MPUSH shell event bridge', () => {
|
|
|
145
145
|
const footer = document.querySelector('.ant-modal-footer');
|
|
146
146
|
expect(footer).toBeTruthy();
|
|
147
147
|
const [cancel] = footer.querySelectorAll('button');
|
|
148
|
+
expect(cancel.textContent).toBe('Cancel'); // Classic's footer labels (messages.cancel/yes)
|
|
148
149
|
act(() => { fireEvent.click(cancel); });
|
|
149
150
|
expect(props.handleCancelModal).toHaveBeenCalled();
|
|
150
151
|
});
|
|
152
|
+
|
|
153
|
+
// Classic seeds the container's formData copy on init (initialiseForm ->
|
|
154
|
+
// resetTabKeys -> onChange, Classic.js:1906-1911, 740-748); the copy-content
|
|
155
|
+
// handlers read that copy before any user input.
|
|
156
|
+
it('emits the seeded legacy formData (onChange) on init', () => {
|
|
157
|
+
const props = buildProps(buildSchema(), { formData: {} });
|
|
158
|
+
mount(props);
|
|
159
|
+
expect(props.onChange).toHaveBeenCalled();
|
|
160
|
+
const [legacy] = props.onChange.mock.calls[0];
|
|
161
|
+
expect(legacy[0]).toEqual(expect.objectContaining({ 'message-title': '' }));
|
|
162
|
+
expect(legacy[1]).toEqual(expect.objectContaining({ 'message-title2': '' }));
|
|
163
|
+
});
|
|
151
164
|
});
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
SelectField, UploadField, MpushDivField, IconField, MobilePushPreviewField,
|
|
10
10
|
createMpushRegistry,
|
|
11
11
|
} from '../renderers/mpushRenderers';
|
|
12
|
+
import { TagListField } from '../renderers/smsRenderers';
|
|
12
13
|
import { ERROR_VALUE } from '../channels/mobilepush/config';
|
|
13
14
|
import { isAiContentBotDisabled } from '../../../../utils/common';
|
|
14
15
|
|
|
@@ -300,6 +301,31 @@ describe('IconField', () => {
|
|
|
300
301
|
});
|
|
301
302
|
});
|
|
302
303
|
|
|
304
|
+
describe('tag-list rendering (shared TagListField)', () => {
|
|
305
|
+
it('CLASSIC PARITY PIN: column takes field.width and field.style (semantic path, Classic.js:3730-3769)', () => {
|
|
306
|
+
// the container-spliced CTA tagLists position "Add label" via style
|
|
307
|
+
// (marginRight 24%/10%) — Classic's semantic tag-list case honors both.
|
|
308
|
+
const field = {
|
|
309
|
+
id: 'title-tagList',
|
|
310
|
+
label: 'Add label',
|
|
311
|
+
offset: 3,
|
|
312
|
+
style: { marginRight: '10%', display: 'flex', justifyContent: 'end', width: '100%' },
|
|
313
|
+
};
|
|
314
|
+
const el = TagListField({ field, onEvent: jest.fn(), renderContext: {} });
|
|
315
|
+
expect(el.props.span).toBe(''); // no width -> '' exactly like Classic
|
|
316
|
+
expect(el.props.offset).toBe(3);
|
|
317
|
+
expect(el.props.style).toEqual(field.style);
|
|
318
|
+
expect(el.props.children.props.label).toBe('Add label');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('bridges onTagSelect through onEvent', () => {
|
|
322
|
+
const onEvent = jest.fn();
|
|
323
|
+
const el = TagListField({ field: { id: 'title-tagList' }, onEvent, renderContext: {} });
|
|
324
|
+
el.props.children.props.onTagSelect({ definition: { value: 'x' } });
|
|
325
|
+
expect(onEvent).toHaveBeenCalledWith('onTagSelect', { definition: { value: 'x' } });
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
303
329
|
describe('MobilePushPreviewField', () => {
|
|
304
330
|
const field = {
|
|
305
331
|
id: 'mobile-push-preview',
|
|
@@ -67,6 +67,14 @@
|
|
|
67
67
|
margin: auto;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
// Functional MPUSH upload field (values identical to Classic's inline styles).
|
|
71
|
+
.fb-upload-file-input {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
.fb-upload-placeholder {
|
|
75
|
+
width: 100%;
|
|
76
|
+
text-align: center;
|
|
77
|
+
}
|
|
70
78
|
.disabled {
|
|
71
79
|
pointer-events: none;
|
|
72
80
|
border-bottom: 1px solid #C2C2C2 !important;
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
makeSelectMessageMetaConfigId,
|
|
37
37
|
makeSelectPrefilledValues,
|
|
38
38
|
makeSelectIsSendingTestMessage,
|
|
39
|
+
makeSelectIsSendingCcsTestMessage,
|
|
39
40
|
makeSelectUpdatePreviewError,
|
|
40
41
|
makeSelectUpdatePreviewErrors,
|
|
41
42
|
makeSelectFetchPrefilledValuesError,
|
|
@@ -103,11 +104,17 @@ TestAndPreviewSlidebox.propTypes = {
|
|
|
103
104
|
messageMetaConfigId: PropTypes.string,
|
|
104
105
|
prefilledValues: PropTypes.object,
|
|
105
106
|
isSendingTestMessage: PropTypes.bool.isRequired,
|
|
107
|
+
isSendingCcsTestMessage: PropTypes.bool.isRequired,
|
|
106
108
|
intl: PropTypes.object.isRequired,
|
|
107
109
|
senderDetailsByChannel: PropTypes.object,
|
|
108
110
|
wecrmAccounts: PropTypes.array,
|
|
109
111
|
isLoadingSenderDetails: PropTypes.bool,
|
|
110
112
|
orgUnitId: PropTypes.number,
|
|
113
|
+
/** notify/ui only — see CommonTestAndPreview's handleSendTestMessage. */
|
|
114
|
+
ccsSendTransaction: PropTypes.shape({
|
|
115
|
+
commDefinitionId: PropTypes.string,
|
|
116
|
+
referenceId: PropTypes.string,
|
|
117
|
+
}),
|
|
111
118
|
};
|
|
112
119
|
|
|
113
120
|
TestAndPreviewSlidebox.defaultProps = {
|
|
@@ -125,6 +132,7 @@ TestAndPreviewSlidebox.defaultProps = {
|
|
|
125
132
|
isLoadingSenderDetails: false,
|
|
126
133
|
orgUnitId: -1,
|
|
127
134
|
smsFallbackContent: null,
|
|
135
|
+
ccsSendTransaction: null,
|
|
128
136
|
};
|
|
129
137
|
|
|
130
138
|
const mapStateToProps = createStructuredSelector({
|
|
@@ -145,6 +153,7 @@ const mapStateToProps = createStructuredSelector({
|
|
|
145
153
|
fetchPrefilledValuesError: makeSelectFetchPrefilledValuesError(),
|
|
146
154
|
fetchPrefilledValuesErrors: makeSelectFetchPrefilledValuesErrors(),
|
|
147
155
|
isSendingTestMessage: makeSelectIsSendingTestMessage(),
|
|
156
|
+
isSendingCcsTestMessage: makeSelectIsSendingCcsTestMessage(),
|
|
148
157
|
senderDetailsByChannel: makeSelectSenderDetailsByChannel(),
|
|
149
158
|
wecrmAccounts: makeSelectWeCrmAccounts(),
|
|
150
159
|
isLoadingSenderDetails: makeSelectIsLoadingSenderDetails(),
|