@capillarytech/creatives-library 8.0.292-alpha.0 → 8.0.292-alpha.10
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 +1 -3
- package/initialState.js +2 -0
- package/package.json +1 -1
- package/utils/common.js +8 -5
- package/utils/commonUtils.js +85 -4
- package/utils/tagValidations.js +223 -83
- package/utils/tests/commonUtil.test.js +124 -147
- package/utils/tests/tagValidations.test.js +358 -441
- package/v2Components/ErrorInfoNote/index.js +5 -2
- package/v2Components/FormBuilder/index.js +203 -137
- package/v2Components/FormBuilder/messages.js +8 -0
- package/v2Components/HtmlEditor/HTMLEditor.js +11 -2
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
- package/v2Components/HtmlEditor/_htmlEditor.scss +6 -1
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +10 -10
- package/v2Components/HtmlEditor/components/DeviceToggle/FLOW_AND_CLICK_BEHAVIOUR.md +70 -0
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +15 -8
- package/v2Containers/Cap/mockData.js +14 -0
- package/v2Containers/Cap/reducer.js +55 -3
- package/v2Containers/Cap/tests/reducer.test.js +102 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +2 -5
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
- package/v2Containers/CreativesContainer/index.js +10 -30
- package/v2Containers/Email/index.js +5 -1
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +137 -29
- package/v2Containers/FTP/index.js +51 -2
- package/v2Containers/FTP/messages.js +4 -0
- package/v2Containers/InApp/index.js +139 -22
- package/v2Containers/InApp/tests/index.test.js +6 -17
- package/v2Containers/InappAdvance/index.js +118 -8
- package/v2Containers/InappAdvance/tests/index.test.js +2 -3
- package/v2Containers/Line/Container/Text/index.js +1 -0
- package/v2Containers/MobilePush/Create/index.js +19 -42
- package/v2Containers/MobilePush/Edit/index.js +19 -42
- package/v2Containers/MobilePushNew/index.js +32 -12
- package/v2Containers/MobilepushWrapper/index.js +1 -3
- package/v2Containers/Rcs/index.js +37 -12
- package/v2Containers/Sms/Create/index.js +3 -39
- package/v2Containers/Sms/Create/messages.js +0 -4
- package/v2Containers/Sms/Edit/index.js +3 -35
- package/v2Containers/Sms/commonMethods.js +6 -3
- package/v2Containers/SmsTrai/Edit/index.js +47 -11
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
- package/v2Containers/SmsWrapper/index.js +0 -2
- package/v2Containers/Viber/index.js +1 -0
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
- package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
- package/v2Containers/WebPush/Create/index.js +2 -2
- package/v2Containers/WebPush/Create/utils/validation.js +2 -17
- package/v2Containers/WebPush/Create/utils/validation.test.js +24 -59
- package/v2Containers/Whatsapp/index.js +17 -9
- package/v2Containers/Zalo/index.js +11 -3
- package/v2Containers/Sms/tests/commonMethods.test.js +0 -122
|
@@ -102,6 +102,7 @@ const HTMLEditor = forwardRef(({
|
|
|
102
102
|
onTagSelect = null,
|
|
103
103
|
onContextChange = null,
|
|
104
104
|
globalActions = null,
|
|
105
|
+
isLiquidEnabled = false, // Controls Liquid tab visibility in ValidationTabs
|
|
105
106
|
isFullMode = true, // Full mode vs library mode - controls layout and visibility
|
|
106
107
|
onErrorAcknowledged = null, // Callback when user clicks redirection icon to acknowledge errors
|
|
107
108
|
onValidationChange = null, // Callback when validation state changes (for parent to track errors)
|
|
@@ -174,8 +175,12 @@ const HTMLEditor = forwardRef(({
|
|
|
174
175
|
? { [DEVICE_TYPES.ANDROID]: initialContent, [DEVICE_TYPES.IOS]: initialContent }
|
|
175
176
|
: initialContent;
|
|
176
177
|
if (inAppContent.updateContent) {
|
|
177
|
-
const
|
|
178
|
-
const
|
|
178
|
+
const activeDevice = inAppContent.activeDevice;
|
|
179
|
+
const currentContent = inAppContent.getDeviceContent?.(activeDevice);
|
|
180
|
+
// Use active device's value only; empty string is valid (don't fall back to other device)
|
|
181
|
+
const newContent = activeDevice in contentToUpdate
|
|
182
|
+
? (contentToUpdate[activeDevice] ?? '')
|
|
183
|
+
: (contentToUpdate[DEVICE_TYPES.ANDROID] ?? '');
|
|
179
184
|
if (currentContent !== newContent) {
|
|
180
185
|
inAppContent.updateContent(newContent, true);
|
|
181
186
|
}
|
|
@@ -572,6 +577,7 @@ const HTMLEditor = forwardRef(({
|
|
|
572
577
|
content,
|
|
573
578
|
layout,
|
|
574
579
|
validation,
|
|
580
|
+
isLiquidEnabled,
|
|
575
581
|
editorRef: getActiveEditorRef(),
|
|
576
582
|
handleLabelInsert,
|
|
577
583
|
handleSave,
|
|
@@ -591,6 +597,7 @@ const HTMLEditor = forwardRef(({
|
|
|
591
597
|
content,
|
|
592
598
|
layout,
|
|
593
599
|
validation,
|
|
600
|
+
isLiquidEnabled,
|
|
594
601
|
getActiveEditorRef,
|
|
595
602
|
handleLabelInsert,
|
|
596
603
|
handleSave,
|
|
@@ -779,6 +786,7 @@ HTMLEditor.propTypes = {
|
|
|
779
786
|
onTagSelect: PropTypes.func,
|
|
780
787
|
onContextChange: PropTypes.func, // Deprecated: use globalActions instead
|
|
781
788
|
globalActions: PropTypes.object,
|
|
789
|
+
isLiquidEnabled: PropTypes.bool, // Controls Liquid tab visibility in validation
|
|
782
790
|
isFullMode: PropTypes.bool, // Full mode vs library mode
|
|
783
791
|
onErrorAcknowledged: PropTypes.func, // Callback when user clicks redirection icon to acknowledge errors
|
|
784
792
|
onValidationChange: PropTypes.func, // Callback when validation state changes
|
|
@@ -812,6 +820,7 @@ HTMLEditor.defaultProps = {
|
|
|
812
820
|
onTagSelect: null,
|
|
813
821
|
onContextChange: null,
|
|
814
822
|
globalActions: null, // Redux actions for API calls
|
|
823
|
+
isLiquidEnabled: false,
|
|
815
824
|
isFullMode: true, // Default to full mode
|
|
816
825
|
onErrorAcknowledged: null, // Callback when user clicks redirection icon to acknowledge errors
|
|
817
826
|
onValidationChange: null, // Callback when validation state changes
|
|
@@ -3201,6 +3201,7 @@ describe('HTMLEditor', () => {
|
|
|
3201
3201
|
onTagSelect={onTagSelect}
|
|
3202
3202
|
onContextChange={onContextChange}
|
|
3203
3203
|
globalActions={globalActions}
|
|
3204
|
+
isLiquidEnabled={true}
|
|
3204
3205
|
isFullMode={false}
|
|
3205
3206
|
onErrorAcknowledged={onErrorAcknowledged}
|
|
3206
3207
|
onValidationChange={onValidationChange}
|
|
@@ -3260,6 +3261,20 @@ describe('HTMLEditor', () => {
|
|
|
3260
3261
|
expect(screen.getByTestId('device-toggle')).toBeInTheDocument();
|
|
3261
3262
|
});
|
|
3262
3263
|
|
|
3264
|
+
it('should handle isLiquidEnabled prop', () => {
|
|
3265
|
+
render(
|
|
3266
|
+
<TestWrapper>
|
|
3267
|
+
<HTMLEditor isLiquidEnabled={true} />
|
|
3268
|
+
</TestWrapper>
|
|
3269
|
+
);
|
|
3270
|
+
|
|
3271
|
+
act(() => {
|
|
3272
|
+
jest.runAllTimers();
|
|
3273
|
+
});
|
|
3274
|
+
|
|
3275
|
+
expect(screen.getByTestId('editor-toolbar')).toBeInTheDocument();
|
|
3276
|
+
});
|
|
3277
|
+
|
|
3263
3278
|
it('should handle isFullMode prop', () => {
|
|
3264
3279
|
render(
|
|
3265
3280
|
<TestWrapper>
|
|
@@ -175,13 +175,18 @@
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
// Library mode: Position absolute for toolbar in InApp variant
|
|
178
|
-
//
|
|
178
|
+
// Pin toolbar to the right and reserve space so it does not overlap DeviceToggle (iOS tab must stay clickable)
|
|
179
179
|
&.html-editor--library-mode {
|
|
180
180
|
.html-editor__header {
|
|
181
|
+
position: relative;
|
|
182
|
+
padding-right: 14rem; // Reserve space for toolbar so DeviceToggle (flex: 1) doesn't extend under it
|
|
183
|
+
|
|
181
184
|
.editor-toolbar,
|
|
182
185
|
.editor-toolbar.editor-toolbar,
|
|
183
186
|
.ant-layout-header.editor-toolbar {
|
|
184
187
|
position: absolute;
|
|
188
|
+
top: 0;
|
|
189
|
+
right: 0;
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
192
|
}
|
|
@@ -69,7 +69,7 @@ const CodeEditorPaneComponent = ({
|
|
|
69
69
|
}) => {
|
|
70
70
|
const context = useEditorContext();
|
|
71
71
|
const {
|
|
72
|
-
content, validation, variant,
|
|
72
|
+
content, validation, variant, isLiquidEnabled,
|
|
73
73
|
} = context || {};
|
|
74
74
|
const { content: contentValue, updateContent } = content || {};
|
|
75
75
|
const editorRef = useRef(null);
|
|
@@ -250,17 +250,16 @@ const CodeEditorPaneComponent = ({
|
|
|
250
250
|
};
|
|
251
251
|
}, [channel, intl]);
|
|
252
252
|
|
|
253
|
-
// Update editor content when content changes
|
|
253
|
+
// Update editor content when content changes (e.g. when switching Android/iOS tabs).
|
|
254
|
+
// Must run when contentValue is '' so switching to an empty tab clears the doc and does not show the other tab's content.
|
|
254
255
|
useEffect(() => {
|
|
255
|
-
if (viewRef.current
|
|
256
|
-
|
|
257
|
-
|
|
256
|
+
if (!viewRef.current) return;
|
|
257
|
+
const currentDocText = viewRef.current.state.doc.toString();
|
|
258
|
+
if (contentValue !== currentDocText) {
|
|
259
|
+
const view = viewRef.current;
|
|
260
|
+
const { length } = view.state.doc;
|
|
258
261
|
view.dispatch({
|
|
259
|
-
changes: {
|
|
260
|
-
from: 0,
|
|
261
|
-
to: length,
|
|
262
|
-
insert: contentValue || '',
|
|
263
|
-
},
|
|
262
|
+
changes: { from: 0, to: length, insert: contentValue ?? '' },
|
|
264
263
|
});
|
|
265
264
|
}
|
|
266
265
|
}, [contentValue]);
|
|
@@ -298,6 +297,7 @@ const CodeEditorPaneComponent = ({
|
|
|
298
297
|
<ValidationErrorDisplay
|
|
299
298
|
validation={validation}
|
|
300
299
|
onErrorClick={onErrorClick}
|
|
300
|
+
isLiquidEnabled={isLiquidEnabled}
|
|
301
301
|
className="code-editor-pane__validation"
|
|
302
302
|
/>
|
|
303
303
|
</div>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# DeviceToggle – Click flow and when clicks are blocked
|
|
2
|
+
|
|
3
|
+
## 1. Click flow (button → end)
|
|
4
|
+
|
|
5
|
+
1. **User clicks Android or iOS tab**
|
|
6
|
+
- `CapButton` receives the click and runs
|
|
7
|
+
- `onClick={() => handleDeviceClick(DEVICE_TYPES.ANDROID)}` or
|
|
8
|
+
- `onClick={() => handleDeviceClick(DEVICE_TYPES.IOS)}`.
|
|
9
|
+
|
|
10
|
+
2. **`handleDeviceClick(device)`** (in `DeviceToggle/index.js`):
|
|
11
|
+
- Only does something if **both**:
|
|
12
|
+
- `onDeviceChange` is truthy (passed from parent), and
|
|
13
|
+
- `device !== activeDevice` (clicked tab is not already active).
|
|
14
|
+
- If both hold: calls `onDeviceChange(device)`.
|
|
15
|
+
|
|
16
|
+
3. **Parent wiring** (`HTMLEditor.js`):
|
|
17
|
+
- `onDeviceChange={switchDevice}`
|
|
18
|
+
- So `onDeviceChange` is the `switchDevice` from `useInAppContent`.
|
|
19
|
+
|
|
20
|
+
4. **`switchDevice(device)`** (`hooks/useInAppContent.js`):
|
|
21
|
+
- `validDevices = [ANDROID, IOS]` (from `DEVICE_TYPES`: `'android'`, `'ios'`).
|
|
22
|
+
- If `device !== activeDevice` and `validDevices.includes(device)` → `setActiveDevice(device)`.
|
|
23
|
+
- If `device` is invalid → logs a warning and does nothing.
|
|
24
|
+
|
|
25
|
+
5. **Result**
|
|
26
|
+
- `activeDevice` updates → DeviceToggle and preview re-render with the new device; no explicit “block” inside this flow.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. When the click is effectively “blocked” or does nothing
|
|
31
|
+
|
|
32
|
+
- **No handler**
|
|
33
|
+
If the parent does not pass `onDeviceChange` (or passes `undefined`/`null`), `handleDeviceClick` never calls anything. The button still receives the click; there is no visual “disabled” in DeviceToggle.
|
|
34
|
+
|
|
35
|
+
- **Same device**
|
|
36
|
+
If the user clicks the already-active tab (`device === activeDevice`), `handleDeviceClick` does nothing by design.
|
|
37
|
+
|
|
38
|
+
- **Invalid device**
|
|
39
|
+
If `device` is not `'android'` or `'ios'` (e.g. wrong constant when using the repo as a library), `switchDevice` only warns and does not update state.
|
|
40
|
+
|
|
41
|
+
- **Click never reaches the button (layout/CSS)**
|
|
42
|
+
Another element is on top of the tab and captures the click (see below). This is the likely cause when “the iOS button is not clickable in edit mode” when using the repo as a library.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 3. Why the iOS button can be unclickable in library (edit) mode
|
|
47
|
+
|
|
48
|
+
In **library mode** (`isFullMode === false`), InApp uses a special layout:
|
|
49
|
+
|
|
50
|
+
- **Header**: flex row with **DeviceToggle** first and **EditorToolbar** second.
|
|
51
|
+
- **DeviceToggle** has `flex: 1` (takes remaining space).
|
|
52
|
+
- **EditorToolbar** gets `position: absolute` (no `top`/`right`/`left`), so it is taken out of flow.
|
|
53
|
+
|
|
54
|
+
Because the toolbar is out of flow, the only flex child in flow is the DeviceToggle, so it expands to the full width of the header. The absolutely positioned toolbar is then drawn at its “static” position, which is the **right side** of the header. So the toolbar sits **on top of the right part of the header**, which is exactly where the **iOS** tab and the “Keep content same” checkbox are. Clicks on the iOS tab are then hit by the toolbar first, so the **iOS button appears not clickable** even though the handler logic is fine.
|
|
55
|
+
|
|
56
|
+
So the problem is **not** that the button is disabled or that the click handler is blocked; it’s that **the EditorToolbar overlaps the iOS tab in library mode** and steals the click.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 4. Recommended fix (library mode)
|
|
61
|
+
|
|
62
|
+
Ensure the toolbar does not overlap the DeviceToggle:
|
|
63
|
+
|
|
64
|
+
- Either **remove** the library-mode override that sets `position: absolute` on the toolbar so the header stays in normal flex flow, **or**
|
|
65
|
+
- If you keep `position: absolute` for the toolbar:
|
|
66
|
+
- Give the header a **positioning context** (e.g. `position: relative`).
|
|
67
|
+
- Pin the toolbar to the right (e.g. `right: 0; top: 0`).
|
|
68
|
+
- Reserve space for the toolbar so DeviceToggle doesn’t extend under it (e.g. `padding-right` on the header or a fixed/min width for the toolbar and corresponding margin on the DeviceToggle).
|
|
69
|
+
|
|
70
|
+
With that, the DeviceToggle (including the iOS tab) remains fully clickable in edit/library mode.
|
|
@@ -73,9 +73,13 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
73
73
|
const autoSaveTimerRef = useRef(null);
|
|
74
74
|
const changeTimestampRef = useRef(null);
|
|
75
75
|
|
|
76
|
-
// Refs to store current values for auto-save
|
|
76
|
+
// Refs to store current values for auto-save and sync flag
|
|
77
77
|
const onSaveRef = useRef(onSave);
|
|
78
78
|
const deviceContentRef = useRef(deviceContent);
|
|
79
|
+
const keepContentSameRef = useRef(keepContentSame);
|
|
80
|
+
|
|
81
|
+
// Keep ref in sync so updateContent always sees latest (avoids stale closure on first load)
|
|
82
|
+
keepContentSameRef.current = keepContentSame;
|
|
79
83
|
|
|
80
84
|
// Update refs when values change
|
|
81
85
|
useEffect(() => {
|
|
@@ -143,7 +147,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
143
147
|
// Get current active content
|
|
144
148
|
const currentContent = useMemo(() => deviceContent?.[activeDevice] || '', [deviceContent, activeDevice]);
|
|
145
149
|
|
|
146
|
-
// Update content for current device
|
|
150
|
+
// Update content for current device. Use ref so we always respect current checkbox (no stale closure).
|
|
147
151
|
const updateContent = useCallback((newContent) => {
|
|
148
152
|
// Validate input
|
|
149
153
|
if (typeof newContent !== CONTENT_VALIDATION?.DEFAULT_CONTENT_TYPE) {
|
|
@@ -151,10 +155,11 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
151
155
|
return;
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
const shouldSync = keepContentSameRef.current;
|
|
154
159
|
// Create the updated content object
|
|
155
160
|
let updatedDeviceContent;
|
|
156
161
|
|
|
157
|
-
if (
|
|
162
|
+
if (shouldSync) {
|
|
158
163
|
// When sync is enabled, update both devices with the same content
|
|
159
164
|
updatedDeviceContent = {
|
|
160
165
|
[ANDROID]: newContent,
|
|
@@ -230,7 +235,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
230
235
|
currentOnSave?.(deviceContentRef.current);
|
|
231
236
|
}, autoSaveInterval);
|
|
232
237
|
}
|
|
233
|
-
}, [activeDevice,
|
|
238
|
+
}, [activeDevice, autoSave, autoSaveInterval, onChange]);
|
|
234
239
|
|
|
235
240
|
// Switch active device with better validation
|
|
236
241
|
const switchDevice = useCallback((device) => {
|
|
@@ -256,10 +261,12 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
256
261
|
|
|
257
262
|
setDeviceContent(syncedContent);
|
|
258
263
|
setIsDirty(true);
|
|
264
|
+
// Notify parent so preview (e.g. getTemplateContent) shows synced content for both devices
|
|
265
|
+
onChange?.(syncedContent, activeDevice);
|
|
259
266
|
}
|
|
260
267
|
// When disabled (enabled = false), we don't need to do anything special
|
|
261
268
|
// Each device will maintain its current content independently
|
|
262
|
-
}, [activeDevice, deviceContent]);
|
|
269
|
+
}, [activeDevice, deviceContent, onChange]);
|
|
263
270
|
|
|
264
271
|
// Mark content as saved
|
|
265
272
|
const markAsSaved = useCallback(() => {
|
|
@@ -300,21 +307,21 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
300
307
|
return;
|
|
301
308
|
}
|
|
302
309
|
|
|
303
|
-
if (
|
|
310
|
+
if (keepContentSameRef.current) {
|
|
304
311
|
// Update both devices when sync is enabled
|
|
305
312
|
setDeviceContent({
|
|
306
313
|
[ANDROID]: content,
|
|
307
314
|
[IOS]: content,
|
|
308
315
|
});
|
|
309
316
|
} else {
|
|
310
|
-
// Update specific device
|
|
317
|
+
// Update specific device only
|
|
311
318
|
setDeviceContent((prev) => ({
|
|
312
319
|
...prev,
|
|
313
320
|
[device]: content,
|
|
314
321
|
}));
|
|
315
322
|
}
|
|
316
323
|
setIsDirty(true);
|
|
317
|
-
}, [
|
|
324
|
+
}, [ANDROID, IOS]);
|
|
318
325
|
|
|
319
326
|
// Cleanup on unmount
|
|
320
327
|
useEffect(() => () => {
|
|
@@ -2,9 +2,11 @@ export const expectedStateGetLiquidTagsRequest = {
|
|
|
2
2
|
fetchingLiquidTags: true,
|
|
3
3
|
fetchingSchema: true,
|
|
4
4
|
fetchingSchemaError: "",
|
|
5
|
+
liquidTags: [],
|
|
5
6
|
messages: [],
|
|
6
7
|
metaEntities: {
|
|
7
8
|
layouts: [],
|
|
9
|
+
tagLookupMap: {},
|
|
8
10
|
tags: [],
|
|
9
11
|
},
|
|
10
12
|
orgID: "",
|
|
@@ -15,9 +17,11 @@ export const expectedStateGetLiquidTagsFailure = {
|
|
|
15
17
|
fetchingLiquidTags: false,
|
|
16
18
|
fetchingSchema: true,
|
|
17
19
|
fetchingSchemaError: "",
|
|
20
|
+
liquidTags: [],
|
|
18
21
|
messages: [],
|
|
19
22
|
metaEntities: {
|
|
20
23
|
layouts: [],
|
|
24
|
+
tagLookupMap: {},
|
|
21
25
|
tags: [],
|
|
22
26
|
},
|
|
23
27
|
orgID: "",
|
|
@@ -28,9 +32,11 @@ export const expectedStateGetLiquidTagsSuccess = {
|
|
|
28
32
|
fetchingLiquidTags: false,
|
|
29
33
|
fetchingSchema: true,
|
|
30
34
|
fetchingSchemaError: "",
|
|
35
|
+
liquidTags: [],
|
|
31
36
|
messages: [],
|
|
32
37
|
metaEntities: {
|
|
33
38
|
layouts: [],
|
|
39
|
+
tagLookupMap: {},
|
|
34
40
|
tags: [],
|
|
35
41
|
},
|
|
36
42
|
orgID: "",
|
|
@@ -41,9 +47,11 @@ export const expectedStateGetSchemaForEntitySuccessTAG = {
|
|
|
41
47
|
fetchingLiquidTags: false,
|
|
42
48
|
fetchingSchema: false,
|
|
43
49
|
fetchingSchemaError: false,
|
|
50
|
+
liquidTags: [],
|
|
44
51
|
messages: [],
|
|
45
52
|
metaEntities: {
|
|
46
53
|
layouts: undefined,
|
|
54
|
+
tagLookupMap: { undefined: { definition: {} } },
|
|
47
55
|
tags: { standard: { random: "32" } },
|
|
48
56
|
},
|
|
49
57
|
orgID: "",
|
|
@@ -54,9 +62,11 @@ export const expectedStateGetSchemaForEntitySuccess = {
|
|
|
54
62
|
fetchingLiquidTags: false,
|
|
55
63
|
fetchingSchema: false,
|
|
56
64
|
fetchingSchemaError: false,
|
|
65
|
+
liquidTags: [],
|
|
57
66
|
messages: [],
|
|
58
67
|
metaEntities: {
|
|
59
68
|
layouts: undefined,
|
|
69
|
+
tagLookupMap: undefined,
|
|
60
70
|
tags: undefined,
|
|
61
71
|
},
|
|
62
72
|
orgID: "",
|
|
@@ -68,9 +78,13 @@ export const expectedForwardedTags = {
|
|
|
68
78
|
fetchingSchema: false,
|
|
69
79
|
fetchingSchemaError: '',
|
|
70
80
|
injectedTags: undefined,
|
|
81
|
+
liquidTags: [],
|
|
71
82
|
messages: [],
|
|
72
83
|
metaEntities: {
|
|
73
84
|
layouts: [],
|
|
85
|
+
tagLookupMap: {
|
|
86
|
+
|
|
87
|
+
},
|
|
74
88
|
tags: [],
|
|
75
89
|
},
|
|
76
90
|
orgID: "",
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Created by vivek on 22/5/17.
|
|
3
3
|
*/
|
|
4
|
-
import { fromJS } from 'immutable';
|
|
4
|
+
import { fromJS, Map as ImmutableMap } from 'immutable';
|
|
5
5
|
import _ from 'lodash';
|
|
6
6
|
import * as types from './constants';
|
|
7
7
|
import initialState from '../../initialState';
|
|
8
8
|
import { FAILURE } from '../App/constants';
|
|
9
|
+
import { TAG } from '../Whatsapp/constants';
|
|
10
|
+
import { getTagMapValue, getForwardedMapValues, getLoyaltyTagsMapValue } from '../../utils/tagValidations';
|
|
9
11
|
|
|
10
12
|
function capReducer(state = fromJS(initialState.cap), action) {
|
|
11
13
|
switch (action.type) {
|
|
@@ -96,6 +98,39 @@ function capReducer(state = fromJS(initialState.cap), action) {
|
|
|
96
98
|
return state
|
|
97
99
|
.set('fetchingLiquidTags', false);
|
|
98
100
|
case types.GET_SCHEMA_FOR_ENTITY_SUCCESS: {
|
|
101
|
+
//Process standard tags
|
|
102
|
+
const standardTagMapInitial = _.keyBy(
|
|
103
|
+
action?.data?.metaEntities?.standard,
|
|
104
|
+
item => item?.definition?.value
|
|
105
|
+
);
|
|
106
|
+
// Mapping only the `definition` object instead of the entire item, to reduce space used
|
|
107
|
+
const standardTagMap = _.mapValues(standardTagMapInitial, item => ({
|
|
108
|
+
definition: item?.definition ?? {},
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// Process custom tags
|
|
112
|
+
const customSubtags = getTagMapValue(action?.data?.metaEntities?.custom)
|
|
113
|
+
// Process extended tags
|
|
114
|
+
const extendedSubtags = getTagMapValue(action?.data?.metaEntities?.extended);
|
|
115
|
+
|
|
116
|
+
const loyaltySubTagsData = getLoyaltyTagsMapValue(action?.data?.metaEntities?.loyaltyTags);
|
|
117
|
+
|
|
118
|
+
const getExistingTagLookupMap = (state) => {
|
|
119
|
+
if (!state || !state.get) return {};
|
|
120
|
+
const tagLookupMap = state.getIn(['metaEntities', 'tagLookupMap']);
|
|
121
|
+
return state.get('metaEntities') && ImmutableMap.isMap(tagLookupMap)
|
|
122
|
+
? tagLookupMap.toJS()
|
|
123
|
+
: {};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Combine all maps
|
|
127
|
+
const combinedTagMap = {
|
|
128
|
+
...standardTagMap,
|
|
129
|
+
...customSubtags,
|
|
130
|
+
...extendedSubtags,
|
|
131
|
+
...loyaltySubTagsData,
|
|
132
|
+
...getExistingTagLookupMap(state),
|
|
133
|
+
};
|
|
99
134
|
const stateMeta = state.get("metaEntities");
|
|
100
135
|
return state
|
|
101
136
|
.set('fetchingSchema', false)
|
|
@@ -103,6 +138,7 @@ function capReducer(state = fromJS(initialState.cap), action) {
|
|
|
103
138
|
.set('metaEntities', {
|
|
104
139
|
layouts: action.data && action.entityType === 'LAYOUT' ? action.data.metaEntities : stateMeta?.layouts,
|
|
105
140
|
tags: action.data && action.entityType === 'TAG' ? action.data.metaEntities : stateMeta?.tags,
|
|
141
|
+
tagLookupMap: action?.data && action?.entityType === TAG ? combinedTagMap : stateMeta?.tagLookupMap,
|
|
106
142
|
})
|
|
107
143
|
.set('fetchingSchemaError', false);
|
|
108
144
|
}
|
|
@@ -110,6 +146,7 @@ function capReducer(state = fromJS(initialState.cap), action) {
|
|
|
110
146
|
return state.set('metaEntities', {
|
|
111
147
|
layouts: [],
|
|
112
148
|
tags: [],
|
|
149
|
+
tagLookupMap: {},
|
|
113
150
|
});
|
|
114
151
|
// eslint-disable-next-line no-case-declarations
|
|
115
152
|
case types.HIDE_TAGS:
|
|
@@ -117,8 +154,23 @@ function capReducer(state = fromJS(initialState.cap), action) {
|
|
|
117
154
|
metaEntities.tags.standard = _.filter(state.get('metaEntities').tags.standard, (tag) => action.tagList.indexOf(tag.definition.value) === -1);
|
|
118
155
|
metaEntities.tags.custom = _.filter(state.get('metaEntities').tags.custom, (tag) => action.tagList.indexOf(tag.name) === -1);
|
|
119
156
|
return state.setIn(['metaEntities'], metaEntities);
|
|
120
|
-
case types.SET_INJECTED_TAGS:
|
|
121
|
-
|
|
157
|
+
case types.SET_INJECTED_TAGS:
|
|
158
|
+
|
|
159
|
+
// Deep clone the tagLookupMap to avoid direct mutations
|
|
160
|
+
let updatedMetaEntitiesTagLookUp = _.cloneDeep(state.getIn(['metaEntities', 'tagLookupMap']));
|
|
161
|
+
const formattedInjectedTags = getForwardedMapValues(action?.injectedTags);
|
|
162
|
+
// Merge the injectedTags with the existing tagLookupMap
|
|
163
|
+
updatedMetaEntitiesTagLookUp = {
|
|
164
|
+
...formattedInjectedTags || {},
|
|
165
|
+
...updatedMetaEntitiesTagLookUp || {},
|
|
166
|
+
};
|
|
167
|
+
return state.set("injectedTags", action.injectedTags).setIn(
|
|
168
|
+
["metaEntities"],
|
|
169
|
+
fromJS({
|
|
170
|
+
...state.get("metaEntities"),
|
|
171
|
+
tagLookupMap: updatedMetaEntitiesTagLookUp
|
|
172
|
+
})
|
|
173
|
+
);
|
|
122
174
|
case types.GET_TOPBAR_MENU_DATA_REQUEST:
|
|
123
175
|
return state.set('topbarMenuData', fromJS({ status: 'request' }));
|
|
124
176
|
case types.GET_TOPBAR_MENU_DATA_SUCCESS:
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
expectedStateGetSchemaForEntitySuccessTAG,
|
|
22
22
|
expectedStateGetSchemaForEntitySuccess,
|
|
23
23
|
} from '../mockData';
|
|
24
|
+
import { TAG } from '../../Whatsapp/constants';
|
|
24
25
|
import { loadItem } from '../../../services/localStorageApi';
|
|
25
26
|
|
|
26
27
|
|
|
@@ -117,3 +118,104 @@ describe('should handle GET_SUPPORT_VIDEOS_CONFIG', () => {
|
|
|
117
118
|
expect(reducer(mockedInitialState, action).toJS())?.fetchingSchema?.toEqual(true);
|
|
118
119
|
});
|
|
119
120
|
});
|
|
121
|
+
|
|
122
|
+
describe('GET_SCHEMA_FOR_ENTITY_SUCCESS handler', () => {
|
|
123
|
+
it.concurrent('should handle existing tagLookupMap correctly when metaEntities and tagLookupMap exist', () => {
|
|
124
|
+
const initialStateTest = fromJS({
|
|
125
|
+
token: loadItem('token') || '',
|
|
126
|
+
orgID: loadItem('orgID') || '',
|
|
127
|
+
messages: [],
|
|
128
|
+
metaEntities: {
|
|
129
|
+
tags: [{ id: 1, name: 'tag1' }],
|
|
130
|
+
layouts: ['layout1', 'layout2'],
|
|
131
|
+
tagLookupMap: {
|
|
132
|
+
existingTag: { definition: { value: 'existing' } },
|
|
133
|
+
anotherTag: { definition: { value: 'another' } },
|
|
134
|
+
},
|
|
135
|
+
standard: [],
|
|
136
|
+
},
|
|
137
|
+
liquidTags: [],
|
|
138
|
+
fetchingLiquidTags: false,
|
|
139
|
+
fetchingSchema: false,
|
|
140
|
+
fetchingSchemaError: '',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
let action = {
|
|
144
|
+
type: GET_SCHEMA_FOR_ENTITY_SUCCESS,
|
|
145
|
+
entityType: TAG,
|
|
146
|
+
data: {
|
|
147
|
+
metaEntities: {
|
|
148
|
+
standard: [],
|
|
149
|
+
custom: [],
|
|
150
|
+
extended: [],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
let newState = reducer(initialStateTest, action);
|
|
156
|
+
let metaEntities = newState.get('metaEntities');
|
|
157
|
+
|
|
158
|
+
expect(metaEntities).toEqual(expect.objectContaining({
|
|
159
|
+
tagLookupMap: expect.objectContaining({
|
|
160
|
+
existingTag: expect.objectContaining({
|
|
161
|
+
definition: expect.objectContaining({
|
|
162
|
+
value: 'existing',
|
|
163
|
+
}),
|
|
164
|
+
}),
|
|
165
|
+
}),
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
action = {
|
|
169
|
+
type: GET_SCHEMA_FOR_ENTITY_SUCCESS,
|
|
170
|
+
entityType: 'LAYOUT',
|
|
171
|
+
data: {
|
|
172
|
+
metaEntities: {
|
|
173
|
+
layouts: ['layout1', 'layout2'],
|
|
174
|
+
standard: [],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
newState = reducer(initialStateTest, action);
|
|
179
|
+
metaEntities = newState.get('metaEntities');
|
|
180
|
+
expect(metaEntities).toEqual(expect.objectContaining({
|
|
181
|
+
layouts: expect.objectContaining({
|
|
182
|
+
layouts: expect.arrayContaining(['layout1', 'layout2']),
|
|
183
|
+
}),
|
|
184
|
+
}));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it.concurrent('should handle non-existent tagLookupMap by returning empty object', () => {
|
|
188
|
+
const initialStateTest = fromJS({
|
|
189
|
+
token: loadItem('token') || '',
|
|
190
|
+
orgID: loadItem('orgID') || '',
|
|
191
|
+
messages: [],
|
|
192
|
+
metaEntities: {
|
|
193
|
+
tagLookupMap: {},
|
|
194
|
+
},
|
|
195
|
+
liquidTags: [],
|
|
196
|
+
fetchingLiquidTags: false,
|
|
197
|
+
fetchingSchema: false,
|
|
198
|
+
fetchingSchemaError: '',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const action = {
|
|
202
|
+
type: GET_SCHEMA_FOR_ENTITY_SUCCESS,
|
|
203
|
+
entityType: TAG,
|
|
204
|
+
data: {
|
|
205
|
+
metaEntities: {
|
|
206
|
+
standard: [],
|
|
207
|
+
custom: [],
|
|
208
|
+
extended: [],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const newState = reducer(initialStateTest, action);
|
|
214
|
+
const metaEntities = newState.get('metaEntities');
|
|
215
|
+
|
|
216
|
+
// Updated assertions to handle plain object
|
|
217
|
+
expect(metaEntities).toBeDefined();
|
|
218
|
+
expect(metaEntities.tagLookupMap).toBeDefined();
|
|
219
|
+
expect(metaEntities.tagLookupMap).toEqual({});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -566,7 +566,6 @@ export function SlideBoxContent(props) {
|
|
|
566
566
|
handleTestAndPreview={handleTestAndPreview}
|
|
567
567
|
handleCloseTestAndPreview={handleCloseTestAndPreview}
|
|
568
568
|
isTestAndPreviewMode={isTestAndPreviewMode}
|
|
569
|
-
onValidationFail={onValidationFail}
|
|
570
569
|
/>
|
|
571
570
|
)}
|
|
572
571
|
{isEditFTP && (
|
|
@@ -633,7 +632,6 @@ export function SlideBoxContent(props) {
|
|
|
633
632
|
getLiquidTags={getLiquidTags}
|
|
634
633
|
getDefaultTags={type}
|
|
635
634
|
isFullMode={isFullMode}
|
|
636
|
-
onValidationFail={onValidationFail}
|
|
637
635
|
forwardedTags={forwardedTags}
|
|
638
636
|
selectedOfferDetails={selectedOfferDetails}
|
|
639
637
|
onPreviewContentClicked={onPreviewContentClicked}
|
|
@@ -782,7 +780,6 @@ export function SlideBoxContent(props) {
|
|
|
782
780
|
<MobliPushEdit
|
|
783
781
|
getFormLibraryData={getFormData}
|
|
784
782
|
setIsLoadingContent={setIsLoadingContent}
|
|
785
|
-
getLiquidTags={getLiquidTags}
|
|
786
783
|
location={{
|
|
787
784
|
pathname: `/mobilepush/edit/`,
|
|
788
785
|
query,
|
|
@@ -855,7 +852,6 @@ export function SlideBoxContent(props) {
|
|
|
855
852
|
mobilePushCreateMode={mobilePushCreateMode}
|
|
856
853
|
isGetFormData={isGetFormData}
|
|
857
854
|
getFormData={getFormData}
|
|
858
|
-
getLiquidTags={getLiquidTags}
|
|
859
855
|
templateData={templateData}
|
|
860
856
|
type={type}
|
|
861
857
|
step={templateStep}
|
|
@@ -884,7 +880,7 @@ export function SlideBoxContent(props) {
|
|
|
884
880
|
/>
|
|
885
881
|
) : (
|
|
886
882
|
<MobilePushNew
|
|
887
|
-
key="creatives-mobilepush-
|
|
883
|
+
key="creatives-mobilepush-wrapper"
|
|
888
884
|
date={new Date().getMilliseconds()}
|
|
889
885
|
setIsLoadingContent={setIsLoadingContent}
|
|
890
886
|
onMobilepushModeChange={onMobilepushModeChange}
|
|
@@ -1137,6 +1133,7 @@ export function SlideBoxContent(props) {
|
|
|
1137
1133
|
query,
|
|
1138
1134
|
search: '',
|
|
1139
1135
|
}}
|
|
1136
|
+
showTemplateName={showTemplateName}
|
|
1140
1137
|
showLiquidErrorInFooter={showLiquidErrorInFooter}
|
|
1141
1138
|
showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
|
|
1142
1139
|
handleTestAndPreview={handleTestAndPreview}
|