@capillarytech/creatives-library 9.0.59 → 9.0.61-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 +2 -3
- package/package.json +1 -1
- package/utils/common.js +0 -8
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +69 -324
- package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
- package/v2Components/FormBuilder/Functional/constants.js +8 -46
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
- package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
- package/v2Components/FormBuilder/_formBuilder.scss +0 -11
- package/v2Components/FormBuilder/index.js +13 -20
- package/v2Components/FormBuilder/tests/entryGate.test.js +7 -87
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +12 -25
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +9 -28
- package/v2Containers/CommunicationFlow/hooks/Tests/useFetchCommDefinition.test.js +118 -0
- package/v2Containers/CommunicationFlow/hooks/useFetchCommDefinition.js +49 -0
- package/v2Containers/TagList/index.js +3 -3
- package/v2Containers/TagList/tests/TagList.test.js +16 -0
- package/v2Containers/Viber/constants.js +6 -0
- package/v2Containers/Viber/index.js +22 -16
- package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -75
- package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
- package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -21
- package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -85
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -448
- package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +0 -135
- package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -320
- package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -148
- package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
- package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +0 -147
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -200
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -376
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
- package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
|
@@ -6,11 +6,10 @@
|
|
|
6
6
|
* monolith, frozen) and the new functional implementation (./Functional)
|
|
7
7
|
* based on the per-channel, per-org feature flag.
|
|
8
8
|
*
|
|
9
|
-
* Phase 1
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* flag — keeps using Classic with byte-identical behavior. The flags default
|
|
9
|
+
* Phase 1 migrates SMS only, behind `ENABLE_NEW_FORMBUILDER_SMS`. The new path
|
|
10
|
+
* is taken ONLY when (a) the org has that flag AND (b) this FormBuilder instance
|
|
11
|
+
* is rendering the SMS channel. Every other channel — and every org without the
|
|
12
|
+
* flag — keeps using Classic with byte-identical behavior. The flag defaults
|
|
14
13
|
* OFF, so until rollout this gate always renders Classic.
|
|
15
14
|
*
|
|
16
15
|
* This component is a pure pass-through: no defaultProps, no prop mapping, no
|
|
@@ -22,30 +21,24 @@
|
|
|
22
21
|
import React, { useState } from 'react';
|
|
23
22
|
import ClassicFormBuilder from './Classic';
|
|
24
23
|
import FunctionalFormBuilder from './Functional';
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
hasNewFormBuilderEnabledForMpush,
|
|
28
|
-
} from '../../utils/common';
|
|
29
|
-
import { SMS, MOBILE_PUSH } from '../../v2Containers/CreativesContainer/constants';
|
|
24
|
+
import { hasNewFormBuilderEnabledForSms } from '../../utils/common';
|
|
25
|
+
import { SMS } from '../../v2Containers/CreativesContainer/constants';
|
|
30
26
|
|
|
31
27
|
const FormBuilder = (props) => {
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
const [newBuilderFlags] = useState(() => {
|
|
28
|
+
// The try/catch guards the early bootstrap / test case where window.capAuth is not yet initialized — any failure routes safely to Classic.
|
|
29
|
+
const [isSmsNewBuilderOrg] = useState(() => {
|
|
35
30
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
[MOBILE_PUSH]: Boolean(hasNewFormBuilderEnabledForMpush()),
|
|
39
|
-
};
|
|
31
|
+
console.log('### ENABLE_NEW_FORMBUILDER_SMS enabled for org:', hasNewFormBuilderEnabledForSms());
|
|
32
|
+
return Boolean(hasNewFormBuilderEnabledForSms());
|
|
40
33
|
} catch (e) {
|
|
41
|
-
return
|
|
34
|
+
return false;
|
|
42
35
|
}
|
|
43
36
|
});
|
|
44
37
|
|
|
45
|
-
// Derived every render: the schema (and therefore the channel) arrives asynchronously.
|
|
46
38
|
const channel = (props?.channel || props.schema?.channel)?.toUpperCase();
|
|
47
39
|
|
|
48
|
-
|
|
40
|
+
const useNewFormBuilder = isSmsNewBuilderOrg && channel === SMS;
|
|
41
|
+
return useNewFormBuilder ? (
|
|
49
42
|
<FunctionalFormBuilder {...props} />
|
|
50
43
|
) : (
|
|
51
44
|
<ClassicFormBuilder {...props} />
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* implementations are mocked with trivial stubs so this stays a fast unit test
|
|
6
6
|
* of the gate, independent of the 4,400-line monolith.
|
|
7
7
|
*
|
|
8
|
-
* Contract pinned here
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
8
|
+
* Contract pinned here:
|
|
9
|
+
* - flag OFF -> Classic, for every channel (the default until rollout)
|
|
10
|
+
* - flag ON + SMS channel (via `channel` prop OR `schema.channel`) -> Functional
|
|
11
|
+
* - flag ON + non-SMS channel -> Classic (other channels untouched)
|
|
12
12
|
* - flag ON + channel unknown (async schema not yet arrived) -> Classic
|
|
13
13
|
* - flag read throws (bootstrap/test race) -> Classic, no crash
|
|
14
14
|
*/
|
|
@@ -25,26 +25,20 @@ jest.mock('../Functional', () => (props) => (
|
|
|
25
25
|
<div data-testid="impl-functional">functional:{(props.schema && props.schema.channel) || props.channel || ''}</div>
|
|
26
26
|
));
|
|
27
27
|
|
|
28
|
-
// Mock only the flag
|
|
28
|
+
// Mock only the one flag reader the gate uses; keep the rest of common.js intact.
|
|
29
29
|
jest.mock('../../../utils/common', () => ({
|
|
30
30
|
...jest.requireActual('../../../utils/common'),
|
|
31
31
|
hasNewFormBuilderEnabledForSms: jest.fn(),
|
|
32
|
-
hasNewFormBuilderEnabledForMpush: jest.fn(),
|
|
33
32
|
}));
|
|
34
33
|
|
|
35
34
|
// eslint-disable-next-line import/first
|
|
36
35
|
import FormBuilder from '../index';
|
|
37
36
|
// eslint-disable-next-line import/first
|
|
38
|
-
import { hasNewFormBuilderEnabledForSms
|
|
37
|
+
import { hasNewFormBuilderEnabledForSms } from '../../../utils/common';
|
|
39
38
|
|
|
40
39
|
const renderGate = (props = {}) => render(<FormBuilder {...props} />);
|
|
41
40
|
|
|
42
41
|
describe('FormBuilder entry gate — routing', () => {
|
|
43
|
-
beforeEach(() => {
|
|
44
|
-
hasNewFormBuilderEnabledForSms.mockReturnValue(false);
|
|
45
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(false);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
42
|
describe('flag OFF (default)', () => {
|
|
49
43
|
beforeEach(() => hasNewFormBuilderEnabledForSms.mockReturnValue(false));
|
|
50
44
|
|
|
@@ -99,73 +93,8 @@ describe('FormBuilder entry gate — routing', () => {
|
|
|
99
93
|
});
|
|
100
94
|
});
|
|
101
95
|
|
|
102
|
-
describe('MPUSH flag (ENABLE_NEW_FORMBUILDER_MPUSH — Phase 2)', () => {
|
|
103
|
-
it('flag OFF: MOBILEPUSH renders Classic', () => {
|
|
104
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
105
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('flag ON: routes MOBILEPUSH to Functional (schema.channel)', () => {
|
|
109
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
110
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
111
|
-
expect(screen.getByTestId('impl-functional')).toBeInTheDocument();
|
|
112
|
-
expect(screen.queryByTestId('impl-classic')).toBeNull();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('flag ON: is case-insensitive and honors the channel prop', () => {
|
|
116
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
117
|
-
renderGate({ channel: 'mobilepush', schema: {} });
|
|
118
|
-
expect(screen.getByTestId('impl-functional')).toBeInTheDocument();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('flags are independent: MPUSH flag ON does not move SMS (and vice versa)', () => {
|
|
122
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
123
|
-
renderGate({ schema: { channel: 'SMS' } });
|
|
124
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('SMS flag ON does not move MOBILEPUSH', () => {
|
|
128
|
-
hasNewFormBuilderEnabledForSms.mockReturnValue(true);
|
|
129
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
130
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('both flags ON route each channel to Functional', () => {
|
|
134
|
-
hasNewFormBuilderEnabledForSms.mockReturnValue(true);
|
|
135
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
136
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
137
|
-
expect(screen.getByTestId('impl-functional')).toHaveTextContent('functional:MOBILEPUSH');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('unmigrated channels stay on Classic even with both flags ON', () => {
|
|
141
|
-
hasNewFormBuilderEnabledForSms.mockReturnValue(true);
|
|
142
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
143
|
-
renderGate({ schema: { channel: 'EMAIL' } });
|
|
144
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('the gate is purely org-flag controlled (no localStorage override)', () => {
|
|
149
|
-
afterEach(() => {
|
|
150
|
-
window.localStorage.removeItem('ENABLE_NEW_FORMBUILDER_MPUSH');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('a localStorage key does NOT enable the new path when the org flag is OFF', () => {
|
|
154
|
-
window.localStorage.setItem('ENABLE_NEW_FORMBUILDER_MPUSH', 'true');
|
|
155
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
156
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('a localStorage key does NOT disable the new path when the org flag is ON', () => {
|
|
160
|
-
hasNewFormBuilderEnabledForMpush.mockReturnValue(true);
|
|
161
|
-
window.localStorage.setItem('ENABLE_NEW_FORMBUILDER_MPUSH', 'false');
|
|
162
|
-
renderGate({ schema: { channel: 'MOBILEPUSH' } });
|
|
163
|
-
expect(screen.getByTestId('impl-functional')).toBeInTheDocument();
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
96
|
describe('resilience', () => {
|
|
168
|
-
it('falls back to Classic if
|
|
97
|
+
it('falls back to Classic if the flag read throws (window.capAuth not initialized)', () => {
|
|
169
98
|
hasNewFormBuilderEnabledForSms.mockImplementation(() => {
|
|
170
99
|
throw new Error('capAuth not initialized');
|
|
171
100
|
});
|
|
@@ -173,14 +102,5 @@ describe('FormBuilder entry gate — routing', () => {
|
|
|
173
102
|
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
174
103
|
expect(screen.queryByTestId('impl-functional')).toBeNull();
|
|
175
104
|
});
|
|
176
|
-
|
|
177
|
-
it('one flag reader throwing routes EVERY channel to Classic (whole map falls back)', () => {
|
|
178
|
-
hasNewFormBuilderEnabledForSms.mockReturnValue(true);
|
|
179
|
-
hasNewFormBuilderEnabledForMpush.mockImplementation(() => {
|
|
180
|
-
throw new Error('capAuth not initialized');
|
|
181
|
-
});
|
|
182
|
-
renderGate({ schema: { channel: 'SMS' } });
|
|
183
|
-
expect(screen.getByTestId('impl-classic')).toBeInTheDocument();
|
|
184
|
-
});
|
|
185
105
|
});
|
|
186
106
|
});
|
|
@@ -20,7 +20,7 @@ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
|
|
|
20
20
|
import injectReducer from '../../utils/injectReducer';
|
|
21
21
|
import { makeSelectAuthenticated } from '../Cap/selectors';
|
|
22
22
|
import capReducer from '../Cap/reducer';
|
|
23
|
-
import { createCommDefinition, editCommDefinition
|
|
23
|
+
import { createCommDefinition, editCommDefinition } from '../../services/api';
|
|
24
24
|
import DynamicControlsStep from './steps/DynamicControlsStep';
|
|
25
25
|
import MessageTypeStep from './steps/MessageTypeStep';
|
|
26
26
|
import CommunicationStrategyStep from './steps/CommunicationStrategyStep';
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
SINGLE_TEMPLATE,
|
|
45
45
|
} from './constants';
|
|
46
46
|
import { getEnabledSteps } from './utils/getEnabledSteps';
|
|
47
|
-
import
|
|
47
|
+
import useFetchCommDefinition from './hooks/useFetchCommDefinition';
|
|
48
48
|
import messages from './messages';
|
|
49
49
|
import './CommunicationFlow.scss';
|
|
50
50
|
|
|
@@ -79,11 +79,12 @@ const CommunicationFlow = ({
|
|
|
79
79
|
const { formatMessage } = intl || {};
|
|
80
80
|
const { messageTypeData = {}, communicationStrategyData = {}, contentTemplateData = {} } = config?.features || {};
|
|
81
81
|
|
|
82
|
-
// Other consumers may provide only commDefinitionId and rely on CommunicationFlow to fetch
|
|
82
|
+
// Other consumers may provide only commDefinitionId and rely on CommunicationFlow to fetch
|
|
83
|
+
// content; captured once at mount so it also seeds messageType/communicationStrategy below,
|
|
84
|
+
// before useFetchCommDefinition's effect (mount-only) has a chance to run.
|
|
83
85
|
const [shouldFetchCommDefinition] = useState(
|
|
84
86
|
() => !!config?.context?.existingCommDefinitionId && !initialData?.contentItems?.length,
|
|
85
87
|
);
|
|
86
|
-
const [isFetchingCommDefinition, setIsFetchingCommDefinition] = useState(shouldFetchCommDefinition);
|
|
87
88
|
|
|
88
89
|
// Initialize step data from initialData or defaults
|
|
89
90
|
const [stepData, setStepData] = useState(() => {
|
|
@@ -103,27 +104,13 @@ const CommunicationFlow = ({
|
|
|
103
104
|
const [validationErrors, setValidationErrors] = useState({});
|
|
104
105
|
|
|
105
106
|
// Fetch-by-id: populate stepData from the CommDefinition itself when the consumer only
|
|
106
|
-
// supplied a commDefinitionId (see shouldFetchCommDefinition above).
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
getCommDefinition,
|
|
114
|
-
getCommDefinitionVersion,
|
|
115
|
-
});
|
|
116
|
-
if (!cancelled && mapped) {
|
|
117
|
-
setStepData((prevStepData) => ({ ...prevStepData, ...mapped }));
|
|
118
|
-
}
|
|
119
|
-
} catch (error) {
|
|
120
|
-
console.error('[CommunicationFlow] fetchAndMapCommDefinition error:', error);
|
|
121
|
-
} finally {
|
|
122
|
-
if (!cancelled) setIsFetchingCommDefinition(false);
|
|
123
|
-
}
|
|
124
|
-
})();
|
|
125
|
-
return () => { cancelled = true; };
|
|
126
|
-
}, []);
|
|
107
|
+
// supplied a commDefinitionId (see shouldFetchCommDefinition above). Shared with
|
|
108
|
+
// CommunicationFlowCard.js via useFetchCommDefinition.
|
|
109
|
+
const isFetchingCommDefinition = useFetchCommDefinition({
|
|
110
|
+
existingCommDefinitionId: config?.context?.existingCommDefinitionId,
|
|
111
|
+
hasLocalContent: !!initialData?.contentItems?.length,
|
|
112
|
+
onFetched: (mapped) => setStepData((prevStepData) => ({ ...prevStepData, ...mapped })),
|
|
113
|
+
});
|
|
127
114
|
|
|
128
115
|
// Memoize enabled steps
|
|
129
116
|
const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
|
|
@@ -27,8 +27,7 @@ import {
|
|
|
27
27
|
VIBER, SMS, EMAIL, WHATSAPP, RCS, ZALO,
|
|
28
28
|
} from '../CreativesContainer/constants';
|
|
29
29
|
import getContentBody from './utils/getContentBody';
|
|
30
|
-
import
|
|
31
|
-
import { fetchAndMapCommDefinition } from './utils/mapCommDefinitionToStepData';
|
|
30
|
+
import useFetchCommDefinition from './hooks/useFetchCommDefinition';
|
|
32
31
|
import messages from './messages';
|
|
33
32
|
import addCreativesIllustration from '../Assets/images/addCreativesIllustration.svg';
|
|
34
33
|
import './CommunicationFlow.scss';
|
|
@@ -93,32 +92,14 @@ const CommunicationFlowCard = ({
|
|
|
93
92
|
}
|
|
94
93
|
}, [initialData]);
|
|
95
94
|
|
|
96
|
-
// Other consumers may provide only commDefinitionId; fetch it here so the summary card shows
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let cancelled = false;
|
|
105
|
-
(async () => {
|
|
106
|
-
try {
|
|
107
|
-
const mapped = await fetchAndMapCommDefinition(existingCommDefinitionId, {
|
|
108
|
-
getCommDefinition,
|
|
109
|
-
getCommDefinitionVersion,
|
|
110
|
-
});
|
|
111
|
-
if (!cancelled && mapped) {
|
|
112
|
-
setSavedData((prevSavedData) => ({ ...prevSavedData, ...mapped }));
|
|
113
|
-
}
|
|
114
|
-
} catch (error) {
|
|
115
|
-
console.error('[CommunicationFlowCard] fetchAndMapCommDefinition error:', error);
|
|
116
|
-
} finally {
|
|
117
|
-
if (!cancelled) setIsFetchingCommDefinition(false);
|
|
118
|
-
}
|
|
119
|
-
})();
|
|
120
|
-
return () => { cancelled = true; };
|
|
121
|
-
}, []);
|
|
95
|
+
// Other consumers may provide only commDefinitionId; fetch it here so the summary card shows
|
|
96
|
+
// configured content immediately instead of "Add creatives". Shared with CommunicationFlow.js
|
|
97
|
+
// via useFetchCommDefinition.
|
|
98
|
+
const isFetchingCommDefinition = useFetchCommDefinition({
|
|
99
|
+
existingCommDefinitionId: config?.context?.existingCommDefinitionId,
|
|
100
|
+
hasLocalContent: !!initialData?.contentItems?.length,
|
|
101
|
+
onFetched: (mapped) => setSavedData((prevSavedData) => ({ ...prevSavedData, ...mapped })),
|
|
102
|
+
});
|
|
122
103
|
|
|
123
104
|
const handleSave = useCallback((data) => {
|
|
124
105
|
setSavedData(data);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react-hooks';
|
|
2
|
+
import { waitFor } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
jest.mock('../../../../services/api', () => ({
|
|
5
|
+
getCommDefinition: jest.fn(),
|
|
6
|
+
getCommDefinitionVersion: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
jest.mock('../../utils/mapCommDefinitionToStepData', () => ({
|
|
10
|
+
fetchAndMapCommDefinition: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import useFetchCommDefinition from '../useFetchCommDefinition';
|
|
14
|
+
import { getCommDefinition, getCommDefinitionVersion } from '../../../../services/api';
|
|
15
|
+
import { fetchAndMapCommDefinition } from '../../utils/mapCommDefinitionToStepData';
|
|
16
|
+
|
|
17
|
+
describe('useFetchCommDefinition', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
fetchAndMapCommDefinition.mockReset();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('starts in a fetching state and calls onFetched with the mapped result once the fetch resolves', async () => {
|
|
23
|
+
fetchAndMapCommDefinition.mockResolvedValue({ channel: 'SMS', contentItems: [{ contentId: 'cd_1' }] });
|
|
24
|
+
const onFetched = jest.fn();
|
|
25
|
+
|
|
26
|
+
const { result } = renderHook(() => useFetchCommDefinition({
|
|
27
|
+
existingCommDefinitionId: 'cd_1',
|
|
28
|
+
hasLocalContent: false,
|
|
29
|
+
onFetched,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
expect(result.current).toBe(true);
|
|
33
|
+
|
|
34
|
+
await waitFor(() => expect(result.current).toBe(false));
|
|
35
|
+
expect(fetchAndMapCommDefinition).toHaveBeenCalledWith('cd_1', {
|
|
36
|
+
getCommDefinition,
|
|
37
|
+
getCommDefinitionVersion,
|
|
38
|
+
});
|
|
39
|
+
expect(onFetched).toHaveBeenCalledWith({ channel: 'SMS', contentItems: [{ contentId: 'cd_1' }] });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not call onFetched when the fetch resolves with nothing mappable, but still stops fetching', async () => {
|
|
43
|
+
fetchAndMapCommDefinition.mockResolvedValue(null);
|
|
44
|
+
const onFetched = jest.fn();
|
|
45
|
+
|
|
46
|
+
const { result } = renderHook(() => useFetchCommDefinition({
|
|
47
|
+
existingCommDefinitionId: 'cd_1',
|
|
48
|
+
hasLocalContent: false,
|
|
49
|
+
onFetched,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
await waitFor(() => expect(result.current).toBe(false));
|
|
53
|
+
expect(onFetched).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('does not crash and stops fetching when the fetch rejects', async () => {
|
|
57
|
+
fetchAndMapCommDefinition.mockRejectedValue(new Error('network error'));
|
|
58
|
+
const onFetched = jest.fn();
|
|
59
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
60
|
+
|
|
61
|
+
const { result } = renderHook(() => useFetchCommDefinition({
|
|
62
|
+
existingCommDefinitionId: 'cd_1',
|
|
63
|
+
hasLocalContent: false,
|
|
64
|
+
onFetched,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
await waitFor(() => expect(result.current).toBe(false));
|
|
68
|
+
expect(onFetched).not.toHaveBeenCalled();
|
|
69
|
+
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
70
|
+
consoleErrorSpy.mockRestore();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('never fetches, and reports isFetchingCommDefinition as false throughout, when hasLocalContent is true', async () => {
|
|
74
|
+
const onFetched = jest.fn();
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() => useFetchCommDefinition({
|
|
77
|
+
existingCommDefinitionId: 'cd_1',
|
|
78
|
+
hasLocalContent: true,
|
|
79
|
+
onFetched,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
expect(result.current).toBe(false);
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
84
|
+
expect(fetchAndMapCommDefinition).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('never fetches when existingCommDefinitionId is absent', async () => {
|
|
88
|
+
const onFetched = jest.fn();
|
|
89
|
+
|
|
90
|
+
const { result } = renderHook(() => useFetchCommDefinition({
|
|
91
|
+
existingCommDefinitionId: undefined,
|
|
92
|
+
hasLocalContent: false,
|
|
93
|
+
onFetched,
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
expect(result.current).toBe(false);
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
98
|
+
expect(fetchAndMapCommDefinition).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('does not call onFetched after unmount, even if the fetch resolves late', async () => {
|
|
102
|
+
let resolveFetch;
|
|
103
|
+
fetchAndMapCommDefinition.mockReturnValue(new Promise((resolve) => { resolveFetch = resolve; }));
|
|
104
|
+
const onFetched = jest.fn();
|
|
105
|
+
|
|
106
|
+
const { unmount } = renderHook(() => useFetchCommDefinition({
|
|
107
|
+
existingCommDefinitionId: 'cd_1',
|
|
108
|
+
hasLocalContent: false,
|
|
109
|
+
onFetched,
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
unmount();
|
|
113
|
+
resolveFetch({ channel: 'SMS', contentItems: [{ contentId: 'cd_1' }] });
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
115
|
+
|
|
116
|
+
expect(onFetched).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared by CommunicationFlow.js and CommunicationFlowCard.js — both need to fetch + reverse-map
|
|
3
|
+
* a CommDefinition by id when a consumer supplies only config.context.existingCommDefinitionId
|
|
4
|
+
* and no local content copy (see mapCommDefinitionToStepData.js). Previously duplicated in both
|
|
5
|
+
* files; this hook is the single place that owns the fetch, its loading state, and error handling.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useEffect } from 'react';
|
|
8
|
+
import { getCommDefinition, getCommDefinitionVersion } from '../../../services/api';
|
|
9
|
+
import { fetchAndMapCommDefinition } from '../utils/mapCommDefinitionToStepData';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string|undefined} existingCommDefinitionId - config.context.existingCommDefinitionId
|
|
13
|
+
* @param {boolean} hasLocalContent - true when the consumer already supplied content
|
|
14
|
+
* (e.g. CapNotify, which always pre-fetches and passes full initialData itself) — skips the fetch.
|
|
15
|
+
* @param {(mapped: object) => void} onFetched - called once, only when the fetch resolves with
|
|
16
|
+
* mappable data; the caller merges `mapped` into its own state (stepData/savedData).
|
|
17
|
+
* @returns {boolean} isFetchingCommDefinition - true from mount until the fetch settles; always
|
|
18
|
+
* false when existingCommDefinitionId is absent or hasLocalContent is true.
|
|
19
|
+
*/
|
|
20
|
+
export default function useFetchCommDefinition({ existingCommDefinitionId, hasLocalContent, onFetched }) {
|
|
21
|
+
// Captured once at mount — existingCommDefinitionId/hasLocalContent aren't expected to change
|
|
22
|
+
// identity across the component's lifetime for this decision.
|
|
23
|
+
const [shouldFetch] = useState(() => !!existingCommDefinitionId && !hasLocalContent);
|
|
24
|
+
const [isFetchingCommDefinition, setIsFetchingCommDefinition] = useState(shouldFetch);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!shouldFetch) return undefined;
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
(async () => {
|
|
30
|
+
try {
|
|
31
|
+
const mapped = await fetchAndMapCommDefinition(existingCommDefinitionId, {
|
|
32
|
+
getCommDefinition,
|
|
33
|
+
getCommDefinitionVersion,
|
|
34
|
+
});
|
|
35
|
+
if (!cancelled && mapped) {
|
|
36
|
+
onFetched(mapped);
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('[useFetchCommDefinition] fetchAndMapCommDefinition error:', error);
|
|
40
|
+
} finally {
|
|
41
|
+
if (!cancelled) setIsFetchingCommDefinition(false);
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
return () => { cancelled = true; };
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
return isFetchingCommDefinition;
|
|
49
|
+
}
|
|
@@ -110,9 +110,9 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
|
|
|
110
110
|
} = prevProps;
|
|
111
111
|
|
|
112
112
|
if (
|
|
113
|
-
tags
|
|
114
|
-
|| injectedTags
|
|
115
|
-
|| selectedOfferDetails
|
|
113
|
+
!_.isEqual(tags, prevTags)
|
|
114
|
+
|| !_.isEqual(injectedTags, prevInjectedTags)
|
|
115
|
+
|| !_.isEqual(selectedOfferDetails, prevSelectedOfferDetails)
|
|
116
116
|
|| !_.isEqual(eventContextTags, prevEventContextTags)
|
|
117
117
|
|| !_.isEqual(waitEventContextTags, prevWaitEventContextTags)
|
|
118
118
|
) {
|
|
@@ -134,6 +134,22 @@ describe("TagList test : UNIT", () => {
|
|
|
134
134
|
expect(() => unmount()).not.toThrow();
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
it('does not regenerate tags when selectedOfferDetails gets a new [] reference with same content', () => {
|
|
138
|
+
const spy = jest.spyOn(TagList.prototype, 'populateTags');
|
|
139
|
+
const { rerender, Component, store } = initializeTagList({
|
|
140
|
+
selectedOfferDetails: [],
|
|
141
|
+
tags: TagListData.tags,
|
|
142
|
+
});
|
|
143
|
+
spy.mockClear();
|
|
144
|
+
rerender(
|
|
145
|
+
<Provider store={store}>
|
|
146
|
+
<Component {...buildProps({ selectedOfferDetails: [], tags: TagListData.tags })} />
|
|
147
|
+
</Provider>
|
|
148
|
+
);
|
|
149
|
+
expect(spy).not.toHaveBeenCalled();
|
|
150
|
+
spy.mockRestore();
|
|
151
|
+
});
|
|
152
|
+
|
|
137
153
|
it('regenerates tags when props.tags change (componentDidUpdate)', () => {
|
|
138
154
|
const { rerender, Component, store } = initializeTagList({ tags: TagListData.tags });
|
|
139
155
|
const extra = [
|
|
@@ -13,6 +13,12 @@ export const VIBER_IMG_SIZE = 10000000;
|
|
|
13
13
|
export const VIBER_VIDEO_SIZE = 209715200;
|
|
14
14
|
export const charLimit = 1000;
|
|
15
15
|
|
|
16
|
+
/** Stable fallbacks for TagList props — inline `= []` / `|| {}` allocate new refs every render. */
|
|
17
|
+
export const EMPTY_OFFER_DETAILS = [];
|
|
18
|
+
export const EMPTY_INJECTED_TAGS = {};
|
|
19
|
+
export const EMPTY_TAGS = [];
|
|
20
|
+
export const EMPTY_TEMPLATE_DATA = {};
|
|
21
|
+
|
|
16
22
|
export const UPLOAD_VIBER_ASSET_REQUEST = 'app/v2Containers/Viber/UPLOAD_ASSET_REQUEST';
|
|
17
23
|
export const UPLOAD_VIBER_ASSET_SUCCESS = 'app/v2Containers/Viber/UPLOAD_ASSET_SUCCESS';
|
|
18
24
|
export const UPLOAD_VIBER_ASSET_FAILURE = 'app/v2Containers/Viber/UPLOAD_ASSET_FAILURE';
|
|
@@ -63,6 +63,10 @@ import {
|
|
|
63
63
|
VIBER_CAROUSEL_IMG_SIZE,
|
|
64
64
|
STATIC_URL,
|
|
65
65
|
DYNAMIC_URL,
|
|
66
|
+
EMPTY_OFFER_DETAILS,
|
|
67
|
+
EMPTY_INJECTED_TAGS,
|
|
68
|
+
EMPTY_TAGS,
|
|
69
|
+
EMPTY_TEMPLATE_DATA,
|
|
66
70
|
} from './constants';
|
|
67
71
|
import withCreatives from '../../hoc/withCreatives';
|
|
68
72
|
import {
|
|
@@ -129,12 +133,12 @@ export const Viber = (props) => {
|
|
|
129
133
|
handleClose,
|
|
130
134
|
onCreateComplete,
|
|
131
135
|
params,
|
|
132
|
-
templateData =
|
|
136
|
+
templateData = EMPTY_TEMPLATE_DATA,
|
|
133
137
|
actions,
|
|
134
138
|
viber = {},
|
|
135
139
|
getFormSubscriptionData,
|
|
136
140
|
viberData = {},
|
|
137
|
-
selectedOfferDetails =
|
|
141
|
+
selectedOfferDetails = EMPTY_OFFER_DETAILS,
|
|
138
142
|
eventContextTags,
|
|
139
143
|
waitEventContextTags,
|
|
140
144
|
// TestAndPreviewSlidebox props
|
|
@@ -258,7 +262,7 @@ export const Viber = (props) => {
|
|
|
258
262
|
setTemplateMediaType(VIBER_MEDIA_TYPES.TEXT);
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
|
-
}, [viber.templateDetails
|
|
265
|
+
}, [params?.id, viber.templateDetails, templateData]);
|
|
262
266
|
|
|
263
267
|
// Reports live message-content validity to a parent (e.g. CreativesContainer's
|
|
264
268
|
// slidebox) so it can disable its own Done/Preview-and-test buttons whenever the
|
|
@@ -332,7 +336,8 @@ export const Viber = (props) => {
|
|
|
332
336
|
globalActionsProps.fetchSchemaForEntity(query);
|
|
333
337
|
};
|
|
334
338
|
|
|
335
|
-
const tags = metaEntities?.tags?.standard
|
|
339
|
+
const tags = metaEntities?.tags?.standard ?? EMPTY_TAGS;
|
|
340
|
+
const resolvedInjectedTags = injectedTags ?? EMPTY_INJECTED_TAGS;
|
|
336
341
|
// tags Code end here
|
|
337
342
|
|
|
338
343
|
// validation on Text area and tags validation
|
|
@@ -393,7 +398,7 @@ export const Viber = (props) => {
|
|
|
393
398
|
onContextChange={handleOnTagsContextChange}
|
|
394
399
|
location={location}
|
|
395
400
|
tags={tags}
|
|
396
|
-
injectedTags={
|
|
401
|
+
injectedTags={resolvedInjectedTags}
|
|
397
402
|
id="viber_tags"
|
|
398
403
|
userLocale={localStorage.getItem("jlocale") || "en"}
|
|
399
404
|
selectedOfferDetails={selectedOfferDetails}
|
|
@@ -882,7 +887,7 @@ export const Viber = (props) => {
|
|
|
882
887
|
onContextChange={handleOnTagsContextChange}
|
|
883
888
|
location={location}
|
|
884
889
|
tags={tags}
|
|
885
|
-
injectedTags={
|
|
890
|
+
injectedTags={resolvedInjectedTags}
|
|
886
891
|
id={`viber_carousel_card_tags_${cardIndex}`}
|
|
887
892
|
userLocale={localStorage.getItem("jlocale") || "en"}
|
|
888
893
|
selectedOfferDetails={selectedOfferDetails}
|
|
@@ -1260,7 +1265,7 @@ export const Viber = (props) => {
|
|
|
1260
1265
|
onContextChange={handleOnTagsContextChange}
|
|
1261
1266
|
location={location}
|
|
1262
1267
|
tags={tags}
|
|
1263
|
-
injectedTags={
|
|
1268
|
+
injectedTags={resolvedInjectedTags}
|
|
1264
1269
|
userLocale={localStorage.getItem("jlocale") || "en"}
|
|
1265
1270
|
selectedOfferDetails={selectedOfferDetails}
|
|
1266
1271
|
eventContextTags={eventContextTags}
|
|
@@ -1578,15 +1583,16 @@ export const Viber = (props) => {
|
|
|
1578
1583
|
</CapButton>
|
|
1579
1584
|
</ViberFooter>
|
|
1580
1585
|
</CapSpin>
|
|
1581
|
-
{
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1586
|
+
{(showTestAndPreviewSlidebox || propsShowTestAndPreviewSlidebox) && (
|
|
1587
|
+
<TestAndPreviewSlidebox
|
|
1588
|
+
show={showTestAndPreviewSlidebox || propsShowTestAndPreviewSlidebox}
|
|
1589
|
+
onClose={handleCloseTestAndPreview}
|
|
1590
|
+
channel={VIBER}
|
|
1591
|
+
formData={getTemplateContent()}
|
|
1592
|
+
content={getTemplateContent()}
|
|
1593
|
+
formatMessage={formatMessage}
|
|
1594
|
+
/>
|
|
1595
|
+
)}
|
|
1590
1596
|
</>
|
|
1591
1597
|
);
|
|
1592
1598
|
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MOBILEPUSH submit payload — identity, exactly like Classic: the container
|
|
3
|
-
* receives the full legacy formData object and builds the API payload itself
|
|
4
|
-
* (getTransformedData). Note that getChannelData has NO MOBILEPUSH branch —
|
|
5
|
-
* Classic short-circuits before reaching it (Classic.js:1402-1405) — so this
|
|
6
|
-
* adapter must never call it.
|
|
7
|
-
*/
|
|
8
|
-
const buildSubmitPayload = (legacyFormData) => legacyFormData;
|
|
9
|
-
|
|
10
|
-
export default buildSubmitPayload;
|