@capillarytech/creatives-library 9.0.15-alpha.3 → 9.0.16-beta.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.
Files changed (146) hide show
  1. package/AppRoot.js +80 -0
  2. package/app.js +5 -3
  3. package/constants/unified.js +0 -29
  4. package/entry.js +67 -1
  5. package/global-styles.js +1 -1
  6. package/mfe-exposed-components.js +2 -4
  7. package/package.json +2 -2
  8. package/services/api.js +1 -1
  9. package/services/tests/api.test.js +20 -35
  10. package/styles/containers/layout/_layoutPage.scss +8 -6
  11. package/utils/commonUtils.js +1 -19
  12. package/utils/getDataLayer.js +15 -0
  13. package/utils/gtmTrackers/gtmEvents/creativeDetails.js +2 -1
  14. package/utils/mfeDetect.js +1 -0
  15. package/v2Components/CapActionButton/constants.js +1 -8
  16. package/v2Components/CapActionButton/index.js +144 -202
  17. package/v2Components/CapActionButton/index.scss +14 -232
  18. package/v2Components/CapActionButton/messages.js +3 -19
  19. package/v2Components/CapActionButton/tests/index.test.js +18 -42
  20. package/v2Components/CapImageUpload/index.js +2 -2
  21. package/v2Components/CapTagList/index.js +0 -10
  22. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +48 -70
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +2 -8
  24. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +21 -214
  25. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +0 -16
  26. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +9 -83
  27. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +0 -30
  28. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +11 -60
  29. package/v2Components/CommonTestAndPreview/SendTestMessage.js +5 -10
  30. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +22 -202
  31. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +80 -381
  32. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +8 -155
  33. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +0 -11
  34. package/v2Components/CommonTestAndPreview/constants.js +2 -38
  35. package/v2Components/CommonTestAndPreview/index.js +222 -761
  36. package/v2Components/CommonTestAndPreview/messages.js +3 -45
  37. package/v2Components/CommonTestAndPreview/sagas.js +6 -25
  38. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +0 -1
  39. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +267 -284
  40. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +65 -231
  41. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +5 -118
  42. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +0 -341
  43. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +1 -8
  44. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +13 -34
  45. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +283 -281
  46. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +1 -199
  47. package/v2Components/CommonTestAndPreview/tests/index.test.js +4 -133
  48. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +24 -31
  49. package/v2Components/NavigationBar/index.js +9 -7
  50. package/v2Components/NavigationBar/mfeModuleHeader.config.js +16 -0
  51. package/v2Components/Pagination/_pagination.scss +2 -0
  52. package/v2Components/TemplatePreview/_templatePreview.scss +25 -45
  53. package/v2Components/TemplatePreview/index.js +32 -147
  54. package/v2Components/TemplatePreview/tests/index.test.js +0 -142
  55. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  56. package/v2Components/TestAndPreviewSlidebox/index.js +1 -13
  57. package/v2Components/TestAndPreviewSlidebox/sagas.js +4 -11
  58. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +1 -3
  59. package/v2Containers/Cap/constants.js +1 -0
  60. package/v2Containers/Cap/index.js +47 -21
  61. package/v2Containers/Cap/sagas.js +19 -0
  62. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +1 -4
  63. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +0 -4
  64. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  65. package/v2Containers/CreativesContainer/SlideBoxContent.js +7 -37
  66. package/v2Containers/CreativesContainer/SlideBoxFooter.js +5 -14
  67. package/v2Containers/CreativesContainer/SlideBoxHeader.js +5 -36
  68. package/v2Containers/CreativesContainer/constants.js +0 -11
  69. package/v2Containers/CreativesContainer/index.js +104 -323
  70. package/v2Containers/CreativesContainer/index.scss +1 -83
  71. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +34 -79
  72. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +16 -79
  73. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +0 -8
  74. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +90 -333
  75. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +15 -20
  76. package/v2Containers/CreativesContainer/tests/index.test.js +9 -71
  77. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  78. package/v2Containers/Rcs/constants.js +11 -131
  79. package/v2Containers/Rcs/index.js +829 -2682
  80. package/v2Containers/Rcs/index.scss +8 -443
  81. package/v2Containers/Rcs/messages.js +6 -45
  82. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +39409 -74176
  83. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +74 -10
  84. package/v2Containers/Rcs/tests/index.test.js +40 -151
  85. package/v2Containers/Rcs/tests/mockData.js +0 -38
  86. package/v2Containers/Rcs/tests/utils.test.js +30 -643
  87. package/v2Containers/Rcs/utils.js +12 -466
  88. package/v2Containers/Sms/Create/index.js +48 -115
  89. package/v2Containers/SmsTrai/Create/index.js +4 -9
  90. package/v2Containers/SmsTrai/Edit/constants.js +0 -2
  91. package/v2Containers/SmsTrai/Edit/index.js +169 -678
  92. package/v2Containers/SmsTrai/Edit/messages.js +4 -14
  93. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +2854 -5455
  94. package/v2Containers/SmsWrapper/index.js +8 -37
  95. package/v2Containers/TagList/index.js +0 -6
  96. package/v2Containers/Templates/_templates.scss +11 -196
  97. package/v2Containers/Templates/actions.js +0 -11
  98. package/v2Containers/Templates/constants.js +0 -2
  99. package/v2Containers/Templates/index.js +60 -131
  100. package/v2Containers/Templates/sagas.js +13 -57
  101. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1015 -1060
  102. package/v2Containers/Templates/tests/sagas.test.js +16 -199
  103. package/v2Containers/TemplatesV2/TemplatesV2.style.js +10 -75
  104. package/v2Containers/TemplatesV2/index.js +43 -86
  105. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  106. package/v2Containers/Whatsapp/index.js +25 -4
  107. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -578
  108. package/utils/rcsPayloadUtils.js +0 -92
  109. package/utils/templateVarUtils.js +0 -201
  110. package/utils/tests/rcsPayloadUtils.test.js +0 -226
  111. package/utils/tests/templateVarUtils.test.js +0 -204
  112. package/v2Components/CommonTestAndPreview/previewApiUtils.js +0 -59
  113. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +0 -67
  114. package/v2Components/CommonTestAndPreview/utils.js +0 -84
  115. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +0 -91
  116. package/v2Components/SmsFallback/constants.js +0 -94
  117. package/v2Components/SmsFallback/index.js +0 -958
  118. package/v2Components/SmsFallback/index.scss +0 -266
  119. package/v2Components/SmsFallback/messages.js +0 -78
  120. package/v2Components/SmsFallback/smsFallbackUtils.js +0 -120
  121. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +0 -50
  122. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +0 -147
  123. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +0 -304
  124. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +0 -208
  125. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +0 -309
  126. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +0 -422
  127. package/v2Components/SmsFallback/useLocalTemplateList.js +0 -92
  128. package/v2Components/TemplatePreview/constants.js +0 -2
  129. package/v2Components/VarSegmentMessageEditor/constants.js +0 -2
  130. package/v2Components/VarSegmentMessageEditor/index.js +0 -125
  131. package/v2Components/VarSegmentMessageEditor/index.scss +0 -46
  132. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +0 -17
  133. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +0 -79
  134. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +0 -90
  135. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +0 -258
  136. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +0 -125
  137. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +0 -227
  138. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +0 -318
  139. package/v2Containers/Sms/smsFormDataHelpers.js +0 -67
  140. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +0 -253
  141. package/v2Containers/SmsTrai/Edit/index.scss +0 -126
  142. package/v2Containers/Templates/TemplatesActionBar.js +0 -101
  143. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +0 -120
  144. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +0 -180
  145. package/v2Containers/Templates/utils/smsTemplatesListApi.js +0 -79
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +0 -131
@@ -1,422 +0,0 @@
1
- import { renderHook, act } from '@testing-library/react-hooks';
2
- import { waitFor } from '@testing-library/react';
3
- import { useLocalTemplateList } from '../useLocalTemplateList';
4
-
5
- const makeDeferred = () => {
6
- let resolve;
7
- let reject;
8
- const promise = new Promise((res, rej) => {
9
- resolve = res;
10
- reject = rej;
11
- });
12
- return { promise, resolve, reject };
13
- };
14
-
15
- describe('useLocalTemplateList', () => {
16
- it('drops stale response when a newer reset starts', async () => {
17
- const first = makeDeferred();
18
- const second = makeDeferred();
19
- const fetchTemplates = jest
20
- .fn()
21
- .mockImplementationOnce(() => first.promise)
22
- .mockImplementationOnce(() => second.promise);
23
-
24
- const { result } = renderHook(() =>
25
- useLocalTemplateList({ fetchTemplates, perPage: 2 })
26
- );
27
-
28
- act(() => {
29
- result.current.reset('old-search');
30
- });
31
-
32
- act(() => {
33
- result.current.reset('new-search');
34
- });
35
-
36
- await act(async () => {
37
- second.resolve({
38
- templates: [{ _id: 'new-1' }],
39
- totalCount: 1,
40
- });
41
- await Promise.resolve();
42
- });
43
-
44
- expect(result.current.templates).toEqual([{ _id: 'new-1' }]);
45
- expect(result.current.totalCount).toBe(1);
46
-
47
- await act(async () => {
48
- first.resolve({
49
- templates: [{ _id: 'old-1' }],
50
- totalCount: 1,
51
- });
52
- await Promise.resolve();
53
- });
54
-
55
- // Older response should not overwrite latest state.
56
- expect(result.current.templates).toEqual([{ _id: 'new-1' }]);
57
- expect(result.current.search).toBe('new-search');
58
- });
59
-
60
- it('keeps existing templates when loadMore fails', async () => {
61
- const fetchTemplates = jest.fn(({ page }) => {
62
- if (page === 1) {
63
- return Promise.resolve({
64
- templates: [{ _id: 't1' }, { _id: 't2' }],
65
- totalCount: 4,
66
- });
67
- }
68
- return Promise.reject(new Error('load-more-failed'));
69
- });
70
-
71
- const { result } = renderHook(() =>
72
- useLocalTemplateList({ fetchTemplates, perPage: 2 })
73
- );
74
-
75
- act(() => {
76
- result.current.reset('');
77
- });
78
-
79
- await waitFor(() => {
80
- expect(result.current.loading).toBe(false);
81
- expect(result.current.templates).toHaveLength(2);
82
- expect(result.current.page).toBe(1);
83
- });
84
-
85
- act(() => {
86
- result.current.loadMore();
87
- });
88
-
89
- await waitFor(() => {
90
- expect(result.current.loading).toBe(false);
91
- });
92
-
93
- // Non-reset failure should not wipe already loaded data.
94
- expect(result.current.templates).toEqual([{ _id: 't1' }, { _id: 't2' }]);
95
- expect(result.current.totalCount).toBe(4);
96
- expect(result.current.page).toBe(1);
97
- });
98
-
99
- it('clears list on reset failure', async () => {
100
- const fetchTemplates = jest
101
- .fn()
102
- .mockResolvedValueOnce({
103
- templates: [{ _id: 'seed-1' }, { _id: 'seed-2' }],
104
- totalCount: 2,
105
- })
106
- .mockRejectedValueOnce(new Error('reset-failed'));
107
-
108
- const { result } = renderHook(() =>
109
- useLocalTemplateList({ fetchTemplates, perPage: 2 })
110
- );
111
-
112
- act(() => {
113
- result.current.reset('');
114
- });
115
-
116
- await waitFor(() => {
117
- expect(result.current.loading).toBe(false);
118
- expect(result.current.templates).toHaveLength(2);
119
- });
120
-
121
- act(() => {
122
- result.current.reset('fresh-search');
123
- });
124
-
125
- await waitFor(() => {
126
- expect(result.current.loading).toBe(false);
127
- });
128
-
129
- expect(result.current.templates).toEqual([]);
130
- expect(result.current.totalCount).toBe(0);
131
- expect(result.current.page).toBe(1);
132
- expect(result.current.search).toBe('fresh-search');
133
- });
134
-
135
- it('keeps previous totalCount when loadMore returns zero totalCount', async () => {
136
- const fetchTemplates = jest.fn(({ page }) => {
137
- if (page === 1) {
138
- return Promise.resolve({
139
- templates: [{ _id: 't1' }],
140
- totalCount: 10,
141
- });
142
- }
143
- return Promise.resolve({
144
- templates: [{ _id: 't2' }],
145
- totalCount: 0,
146
- });
147
- });
148
-
149
- const { result } = renderHook(() =>
150
- useLocalTemplateList({ fetchTemplates, perPage: 1 })
151
- );
152
-
153
- act(() => {
154
- result.current.reset('');
155
- });
156
-
157
- await waitFor(() => {
158
- expect(result.current.loading).toBe(false);
159
- expect(result.current.templates).toHaveLength(1);
160
- expect(result.current.totalCount).toBe(10);
161
- });
162
-
163
- act(() => {
164
- result.current.loadMore();
165
- });
166
-
167
- await waitFor(() => {
168
- expect(result.current.loading).toBe(false);
169
- });
170
-
171
- expect(result.current.templates.map((t) => t._id)).toEqual(['t1', 't2']);
172
- expect(result.current.totalCount).toBe(10);
173
- });
174
-
175
- it('ignores rejection from a superseded fetch (stale generation)', async () => {
176
- const slowFirst = makeDeferred();
177
- const fetchTemplates = jest
178
- .fn()
179
- .mockImplementationOnce(() => slowFirst.promise)
180
- .mockImplementationOnce(() =>
181
- Promise.resolve({ templates: [{ _id: 'from-second' }], totalCount: 1 }),
182
- );
183
-
184
- const { result } = renderHook(() =>
185
- useLocalTemplateList({ fetchTemplates, perPage: 25 })
186
- );
187
-
188
- act(() => {
189
- result.current.reset('first-search');
190
- });
191
- act(() => {
192
- result.current.reset('second-search');
193
- });
194
-
195
- await act(async () => {
196
- await Promise.resolve();
197
- });
198
-
199
- await waitFor(() => {
200
- expect(result.current.loading).toBe(false);
201
- expect(result.current.templates).toEqual([{ _id: 'from-second' }]);
202
- });
203
-
204
- await act(async () => {
205
- slowFirst.reject(new Error('stale'));
206
- await Promise.resolve();
207
- });
208
-
209
- expect(result.current.templates).toEqual([{ _id: 'from-second' }]);
210
- expect(result.current.search).toBe('second-search');
211
- });
212
-
213
- it('does not leave loading stuck when stale resolve completes after newer fetch (finally gen check)', async () => {
214
- const d1 = makeDeferred();
215
- const fetchTemplates = jest
216
- .fn()
217
- .mockImplementationOnce(() => d1.promise)
218
- .mockResolvedValueOnce({ templates: [{ _id: 'new' }], totalCount: 1 });
219
-
220
- const { result } = renderHook(() =>
221
- useLocalTemplateList({ fetchTemplates, perPage: 2 }),
222
- );
223
-
224
- act(() => {
225
- result.current.reset('a');
226
- });
227
- act(() => {
228
- result.current.reset('b');
229
- });
230
-
231
- await act(async () => {
232
- d1.resolve({ templates: [{ _id: 'old' }], totalCount: 1 });
233
- await Promise.resolve();
234
- });
235
-
236
- await waitFor(() => {
237
- expect(result.current.loading).toBe(false);
238
- expect(result.current.templates).toEqual([{ _id: 'new' }]);
239
- });
240
- });
241
-
242
- it('coerces setSearch non-string values to empty search term', () => {
243
- const fetchTemplates = jest.fn().mockResolvedValue({ templates: [], totalCount: 0 });
244
- const { result } = renderHook(() =>
245
- useLocalTemplateList({ fetchTemplates, perPage: 25 }),
246
- );
247
-
248
- act(() => {
249
- result.current.setSearch(null);
250
- });
251
- expect(result.current.search).toBe('');
252
-
253
- act(() => {
254
- result.current.setSearch(123);
255
- });
256
- expect(result.current.search).toBe('');
257
- });
258
-
259
- it('does not call fetch when loadMore runs but canLoadMore is false', async () => {
260
- const fetchTemplates = jest.fn().mockResolvedValue({
261
- templates: [{ _id: 'only' }],
262
- totalCount: 1,
263
- });
264
-
265
- const { result } = renderHook(() =>
266
- useLocalTemplateList({ fetchTemplates, perPage: 1 }),
267
- );
268
-
269
- act(() => {
270
- result.current.reset('');
271
- });
272
-
273
- await waitFor(() => {
274
- expect(result.current.loading).toBe(false);
275
- expect(result.current.canLoadMore).toBe(false);
276
- });
277
-
278
- const callsAfterFirst = fetchTemplates.mock.calls.length;
279
-
280
- act(() => {
281
- result.current.loadMore();
282
- });
283
-
284
- expect(fetchTemplates.mock.calls.length).toBe(callsAfterFirst);
285
- });
286
-
287
- it('passes explicit search term from reset() through to fetchTemplates', async () => {
288
- const fetchTemplates = jest.fn().mockResolvedValue({
289
- templates: [{ _id: 'r1' }],
290
- totalCount: 5,
291
- });
292
-
293
- const { result } = renderHook(() =>
294
- useLocalTemplateList({ fetchTemplates, perPage: 25 }),
295
- );
296
-
297
- act(() => {
298
- result.current.reset('explicit-search');
299
- });
300
-
301
- await waitFor(() => {
302
- expect(result.current.loading).toBe(false);
303
- });
304
-
305
- expect(fetchTemplates).toHaveBeenCalledWith(
306
- expect.objectContaining({ search: 'explicit-search', page: 1, reset: true }),
307
- );
308
- expect(result.current.search).toBe('explicit-search');
309
- });
310
-
311
- it('stores a positive totalCount returned by the API', async () => {
312
- const fetchTemplates = jest.fn().mockResolvedValue({
313
- templates: [{ _id: 'a' }, { _id: 'b' }],
314
- totalCount: 42,
315
- });
316
-
317
- const { result } = renderHook(() =>
318
- useLocalTemplateList({ fetchTemplates, perPage: 25 }),
319
- );
320
-
321
- act(() => {
322
- result.current.reset('');
323
- });
324
-
325
- await waitFor(() => {
326
- expect(result.current.loading).toBe(false);
327
- });
328
-
329
- expect(result.current.totalCount).toBe(42);
330
- expect(result.current.templates).toHaveLength(2);
331
- });
332
-
333
- it('canLoadMore is false while a fetch is in progress even when more items exist', async () => {
334
- let resolveFirst;
335
- const fetchTemplates = jest.fn(
336
- () => new Promise((res) => { resolveFirst = res; }),
337
- );
338
-
339
- const { result } = renderHook(() =>
340
- useLocalTemplateList({ fetchTemplates, perPage: 2 }),
341
- );
342
-
343
- act(() => {
344
- result.current.reset('');
345
- });
346
-
347
- // Loading is true → canLoadMore must be false regardless of hasMoreByTotal
348
- expect(result.current.loading).toBe(true);
349
- expect(result.current.canLoadMore).toBe(false);
350
-
351
- await act(async () => {
352
- resolveFirst({ templates: [{ _id: 'x' }, { _id: 'y' }], totalCount: 10 });
353
- await Promise.resolve();
354
- });
355
-
356
- expect(result.current.loading).toBe(false);
357
- expect(result.current.canLoadMore).toBe(true);
358
- });
359
-
360
- it('canLoadMore is false when templates is empty (hasMoreByFullPage requires length > 0)', async () => {
361
- const fetchTemplates = jest.fn().mockResolvedValue({
362
- templates: [],
363
- totalCount: 0,
364
- });
365
-
366
- const { result } = renderHook(() =>
367
- useLocalTemplateList({ fetchTemplates, perPage: 0 }),
368
- );
369
-
370
- act(() => {
371
- result.current.reset('');
372
- });
373
-
374
- await waitFor(() => {
375
- expect(result.current.loading).toBe(false);
376
- });
377
-
378
- // templates.length === 0 means hasMoreByFullPage is false even if lastFetchFullPage would be true
379
- expect(result.current.canLoadMore).toBe(false);
380
- });
381
-
382
- it('allows loadMore when totalCount is unknown but first page is full (hasMoreByFullPage)', async () => {
383
- const fetchTemplates = jest.fn(({ page }) => {
384
- if (page === 1) {
385
- return Promise.resolve({
386
- templates: [{ _id: 'a' }, { _id: 'b' }],
387
- totalCount: 0,
388
- });
389
- }
390
- return Promise.resolve({
391
- templates: [{ _id: 'c' }],
392
- totalCount: 0,
393
- });
394
- });
395
-
396
- const { result } = renderHook(() =>
397
- useLocalTemplateList({ fetchTemplates, perPage: 2 }),
398
- );
399
-
400
- act(() => {
401
- result.current.reset('');
402
- });
403
-
404
- await waitFor(() => {
405
- expect(result.current.loading).toBe(false);
406
- expect(result.current.canLoadMore).toBe(true);
407
- });
408
-
409
- act(() => {
410
- result.current.loadMore();
411
- });
412
-
413
- await waitFor(() => {
414
- expect(result.current.loading).toBe(false);
415
- });
416
-
417
- expect(result.current.templates.map((t) => t._id)).toEqual(['a', 'b', 'c']);
418
- expect(fetchTemplates).toHaveBeenCalledWith(
419
- expect.objectContaining({ page: 2, reset: false }),
420
- );
421
- });
422
- });
@@ -1,92 +0,0 @@
1
- import { useState, useCallback, useRef } from 'react';
2
-
3
- /**
4
- * @param {Object} options
5
- * @param {(params: { page: number, search: string, reset: boolean }) => Promise<{ templates: Array, totalCount: number }>} options.fetchTemplates
6
- * @param {number} [options.perPage=25]
7
- */
8
- export function useLocalTemplateList({ fetchTemplates, perPage = 25 }) {
9
- const [listData, setListData] = useState({ templates: [], totalCount: 0 });
10
- const [loading, setLoading] = useState(false);
11
- const [page, setPage] = useState(1);
12
- const [search, setSearchState] = useState('');
13
- const searchRef = useRef('');
14
- /** Drops stale responses when a newer fetch starts (search / reset while a request is in flight). */
15
- const fetchGenerationRef = useRef(0);
16
- const setSearch = useCallback((value) => {
17
- const term = typeof value === 'string' ? value : '';
18
- searchRef.current = term;
19
- setSearchState(term);
20
- }, []);
21
- const lastFetchFullPageRef = useRef(false);
22
-
23
- const { templates = [], totalCount = 0 } = listData ?? {};
24
- const hasKnownTotal = (totalCount ?? 0) > 0;
25
- const hasMoreByTotal = (totalCount ?? 0) > (templates?.length ?? 0);
26
- const hasMoreByFullPage =
27
- !hasKnownTotal && lastFetchFullPageRef.current && (templates?.length ?? 0) > 0;
28
- const canLoadMore = (hasMoreByTotal || hasMoreByFullPage) && !loading;
29
-
30
- const runFetch = useCallback(
31
- async ({ page: p = 1, reset = true, search: searchTerm } = {}) => {
32
- const term = searchTerm !== undefined ? searchTerm : searchRef.current;
33
- const gen = ++fetchGenerationRef.current;
34
- setLoading(true);
35
- try {
36
- const result = await fetchTemplates({ page: p, search: term, reset });
37
- if (gen !== fetchGenerationRef.current) {
38
- return;
39
- }
40
- const nextTemplates = result?.templates ?? [];
41
- const nextTotalCount = result?.totalCount ?? 0;
42
- lastFetchFullPageRef.current = nextTemplates.length >= perPage;
43
- setListData((prev) => ({
44
- templates: reset ? nextTemplates : [...(prev.templates || []), ...nextTemplates],
45
- totalCount: nextTotalCount > 0 ? nextTotalCount : (reset ? 0 : prev.totalCount),
46
- }));
47
- setPage(p);
48
- } catch (e) {
49
- if (gen !== fetchGenerationRef.current) {
50
- return;
51
- }
52
- lastFetchFullPageRef.current = false;
53
- if (reset) {
54
- setListData({ templates: [], totalCount: 0 });
55
- setPage(1);
56
- }
57
- } finally {
58
- if (gen === fetchGenerationRef.current) {
59
- setLoading(false);
60
- }
61
- }
62
- },
63
- [fetchTemplates, perPage]
64
- );
65
-
66
- const loadMore = useCallback(() => {
67
- if (!canLoadMore) return;
68
- runFetch({ page: page + 1, reset: false, search: searchRef.current });
69
- }, [canLoadMore, page, runFetch]);
70
-
71
- const reset = useCallback(
72
- (searchTerm) => {
73
- const term = String(searchTerm !== undefined ? searchTerm : searchRef.current);
74
- setSearch(term);
75
- lastFetchFullPageRef.current = false;
76
- runFetch({ page: 1, reset: true, search: term });
77
- },
78
- [runFetch, setSearch]
79
- );
80
-
81
- return {
82
- templates,
83
- totalCount,
84
- loading,
85
- page,
86
- search,
87
- setSearch,
88
- loadMore,
89
- reset,
90
- canLoadMore,
91
- };
92
- }
@@ -1,2 +0,0 @@
1
- /** Matches {{ varName }} placeholders (supports dot notation like user.firstName) */
2
- export const TEMPLATE_VAR_REGEX = /\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g;
@@ -1,2 +0,0 @@
1
- /** Default prefix before variable name in variable-slot placeholders. */
2
- export const VAR_SEGMENT_PLACEHOLDER_PREFIX = 'enter the value for ';
@@ -1,125 +0,0 @@
1
- /**
2
- * Shared message editor that renders template text with {{var}} and/or DLT `{#var#}` segments as
3
- * variable inputs and static text as headings.
4
- * Reused by RCS (title/description), SmsTrai Edit (SMS fallback), and WhatsApp (edit message/header).
5
- */
6
- import React from 'react';
7
- import PropTypes from 'prop-types';
8
- import CapRow from '@capillarytech/cap-ui-library/CapRow';
9
- import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
10
- import CapInput from '@capillarytech/cap-ui-library/CapInput';
11
- import {
12
- splitTemplateVarString,
13
- DEFAULT_MUSTACHE_VAR_REGEX,
14
- isAnyTemplateVarToken,
15
- } from '../../utils/templateVarUtils';
16
-
17
- import './index.scss';
18
- import { VAR_SEGMENT_PLACEHOLDER_PREFIX } from './constants';
19
-
20
- const { TextArea } = CapInput;
21
-
22
- export function VarSegmentMessageEditor({
23
- templateString = '',
24
- valueMap = {},
25
- onChange,
26
- onFocus,
27
- placeholderPrefix = VAR_SEGMENT_PLACEHOLDER_PREFIX,
28
- getPlaceholder,
29
- wrapperClassName = 'rcs_text_area_wrapper',
30
- rowClassName = 'rcs-edit-template-message-input',
31
- headingClassName = 'rcs-edit-template-message-split',
32
- varRegex,
33
- readOnly = false,
34
- disabled = false,
35
- footerContent,
36
- renderVarFooter,
37
- }) {
38
- const segments = splitTemplateVarString(templateString, varRegex || DEFAULT_MUSTACHE_VAR_REGEX);
39
- if (!segments?.length) return null;
40
-
41
- return (
42
- <div className={wrapperClassName}>
43
- <CapRow className={rowClassName}>
44
- {segments.map((segmentToken, segmentIndex) => {
45
- const isVar =
46
- typeof segmentToken === 'string' && isAnyTemplateVarToken(segmentToken);
47
- if (isVar) {
48
- const varSegmentFieldId = `${segmentToken}_${segmentIndex}`;
49
- const slotValueFromMap = valueMap?.[varSegmentFieldId];
50
- // Missing key: show empty (not the raw {{…}} token) so cleared slots and incomplete maps
51
- // cannot resurrect the token; placeholder still guides the user.
52
- const value =
53
- slotValueFromMap !== undefined && slotValueFromMap !== null ? slotValueFromMap : '';
54
- if (readOnly) {
55
- return (
56
- <CapHeading
57
- key={varSegmentFieldId}
58
- type="h4"
59
- className={`${headingClassName} var-segment-message-editor__read-only-value`.trim()}
60
- >
61
- {value}
62
- </CapHeading>
63
- );
64
- }
65
- const fromGet = getPlaceholder && getPlaceholder(segmentToken, segmentIndex);
66
- const placeholder =
67
- fromGet !== undefined && fromGet !== null && fromGet !== ''
68
- ? fromGet
69
- : `${placeholderPrefix}${segmentToken}`;
70
- return (
71
- <div key={varSegmentFieldId} className="var-segment-message-editor__var-slot">
72
- <TextArea
73
- id={varSegmentFieldId}
74
- placeholder={placeholder}
75
- autosize={{ minRows: 1, maxRows: 3 }}
76
- value={value}
77
- onFocus={() => onFocus && onFocus(varSegmentFieldId)}
78
- onChange={(e) =>
79
- onChange && onChange(varSegmentFieldId, e?.target?.value ?? '')}
80
- disabled={disabled}
81
- />
82
- {renderVarFooter
83
- ? renderVarFooter(segmentToken, segmentIndex, varSegmentFieldId)
84
- : null}
85
- </div>
86
- );
87
- }
88
- if (segmentToken) {
89
- return (
90
- <CapHeading
91
- key={`static_${segmentIndex}_${segmentToken}`}
92
- type="h4"
93
- className={headingClassName}
94
- >
95
- {segmentToken}
96
- </CapHeading>
97
- );
98
- }
99
- return null;
100
- })}
101
- </CapRow>
102
- {footerContent}
103
- </div>
104
- );
105
- }
106
-
107
- VarSegmentMessageEditor.propTypes = {
108
- templateString: PropTypes.string,
109
- valueMap: PropTypes.object,
110
- onChange: PropTypes.func,
111
- onFocus: PropTypes.func,
112
- placeholderPrefix: PropTypes.string,
113
- getPlaceholder: PropTypes.func,
114
- wrapperClassName: PropTypes.string,
115
- rowClassName: PropTypes.string,
116
- headingClassName: PropTypes.string,
117
- varRegex: PropTypes.object,
118
- readOnly: PropTypes.bool,
119
- disabled: PropTypes.bool,
120
- footerContent: PropTypes.node,
121
- /** Optional hint below a variable field (e.g. DLT `{#var#}` max length). */
122
- renderVarFooter: PropTypes.func,
123
- };
124
-
125
- export default VarSegmentMessageEditor;
@@ -1,46 +0,0 @@
1
- @import '~@capillarytech/cap-ui-library/styles/_variables';
2
-
3
- /* Same look as RCS edit message block: background, spacing, text color */
4
- .rcs_text_area_wrapper {
5
- .rcs-edit-template-message-input {
6
- background-color: $CAP_G10;
7
- padding: $CAP_SPACE_12 $CAP_SPACE_16 $CAP_SPACE_16;
8
- }
9
-
10
- .rcs-edit-template-message-split {
11
- margin: 0 0 $CAP_SPACE_04 0;
12
- overflow: hidden;
13
- text-overflow: ellipsis;
14
- color: $FONT_COLOR_04;
15
- font-weight: 500;
16
- }
17
-
18
- /* Variable chips: match RCS edit (white field, light border, 4px radius) */
19
- .rcs-edit-template-message-input .ant-input,
20
- .rcs-edit-template-message-input textarea.ant-input {
21
- margin: 0 0 0.125rem 0;
22
- border-radius: 0.25rem;
23
- border: 0.0625rem solid $CAP_G07;
24
- background-color: $CAP_WHITE;
25
- overflow: hidden;
26
- }
27
-
28
- /* Small gap between tag border and the next line (static text) */
29
- .rcs-edit-template-message-input :not(:first-child) {
30
- margin-top: $CAP_SPACE_08;
31
- }
32
-
33
- .rcs-edit-template-message-input > *:last-child {
34
- margin-bottom: 0;
35
- }
36
-
37
- .var-segment-message-editor__var-slot {
38
- display: flex;
39
- flex-direction: column;
40
- width: 100%;
41
- }
42
- }
43
-
44
- .var-segment-message-editor__read-only-value {
45
- margin: 0;
46
- }