@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.
Files changed (55) hide show
  1. package/constants/unified.js +1 -3
  2. package/initialState.js +2 -0
  3. package/package.json +1 -1
  4. package/utils/common.js +8 -5
  5. package/utils/commonUtils.js +85 -4
  6. package/utils/tagValidations.js +223 -83
  7. package/utils/tests/commonUtil.test.js +124 -147
  8. package/utils/tests/tagValidations.test.js +358 -441
  9. package/v2Components/ErrorInfoNote/index.js +5 -2
  10. package/v2Components/FormBuilder/index.js +203 -137
  11. package/v2Components/FormBuilder/messages.js +8 -0
  12. package/v2Components/HtmlEditor/HTMLEditor.js +11 -2
  13. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  14. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +15 -0
  15. package/v2Components/HtmlEditor/_htmlEditor.scss +6 -1
  16. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +10 -10
  17. package/v2Components/HtmlEditor/components/DeviceToggle/FLOW_AND_CLICK_BEHAVIOUR.md +70 -0
  18. package/v2Components/HtmlEditor/hooks/useInAppContent.js +15 -8
  19. package/v2Containers/Cap/mockData.js +14 -0
  20. package/v2Containers/Cap/reducer.js +55 -3
  21. package/v2Containers/Cap/tests/reducer.test.js +102 -0
  22. package/v2Containers/CreativesContainer/SlideBoxContent.js +2 -5
  23. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -13
  24. package/v2Containers/CreativesContainer/index.js +10 -30
  25. package/v2Containers/Email/index.js +5 -1
  26. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +70 -23
  27. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +137 -29
  28. package/v2Containers/FTP/index.js +51 -2
  29. package/v2Containers/FTP/messages.js +4 -0
  30. package/v2Containers/InApp/index.js +139 -22
  31. package/v2Containers/InApp/tests/index.test.js +6 -17
  32. package/v2Containers/InappAdvance/index.js +118 -8
  33. package/v2Containers/InappAdvance/tests/index.test.js +2 -3
  34. package/v2Containers/Line/Container/Text/index.js +1 -0
  35. package/v2Containers/MobilePush/Create/index.js +19 -42
  36. package/v2Containers/MobilePush/Edit/index.js +19 -42
  37. package/v2Containers/MobilePushNew/index.js +32 -12
  38. package/v2Containers/MobilepushWrapper/index.js +1 -3
  39. package/v2Containers/Rcs/index.js +37 -12
  40. package/v2Containers/Sms/Create/index.js +3 -39
  41. package/v2Containers/Sms/Create/messages.js +0 -4
  42. package/v2Containers/Sms/Edit/index.js +3 -35
  43. package/v2Containers/Sms/commonMethods.js +6 -3
  44. package/v2Containers/SmsTrai/Edit/index.js +47 -11
  45. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  46. package/v2Containers/SmsWrapper/index.js +0 -2
  47. package/v2Containers/Viber/index.js +1 -0
  48. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +3 -1
  49. package/v2Containers/WebPush/Create/hooks/useTagManagement.test.js +7 -0
  50. package/v2Containers/WebPush/Create/index.js +2 -2
  51. package/v2Containers/WebPush/Create/utils/validation.js +2 -17
  52. package/v2Containers/WebPush/Create/utils/validation.test.js +24 -59
  53. package/v2Containers/Whatsapp/index.js +17 -9
  54. package/v2Containers/Zalo/index.js +11 -3
  55. 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 currentContent = inAppContent.getDeviceContent?.(inAppContent.activeDevice);
178
- const newContent = contentToUpdate[inAppContent.activeDevice] || contentToUpdate[DEVICE_TYPES.ANDROID] || '';
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
@@ -229,6 +229,7 @@ const defaultProps = {
229
229
  channel: 'EMAIL',
230
230
  userLocale: 'en',
231
231
  moduleFilterEnabled: true,
232
+ isLiquidEnabled: false,
232
233
  isFullMode: true,
233
234
  onErrorAcknowledged: jest.fn(),
234
235
  onValidationChange: jest.fn(),
@@ -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
- // Using higher specificity to override default position: relative
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 && contentValue && contentValue !== viewRef.current.state.doc.toString()) {
256
- const { current: view } = viewRef;
257
- const { state: { doc: { length } } } = view;
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 (keepContentSame) {
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, keepContentSame, autoSave, autoSaveInterval, onChange]);
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 (keepContentSame) {
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
- }, [keepContentSame, ANDROID, IOS]);
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
- return state.set("injectedTags", action.injectedTags);
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-create-new"
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}