@capillarytech/creatives-library 9.0.28 → 9.0.29-alpha.1
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 +29 -0
- package/global-styles.js +1 -1
- package/package.json +1 -1
- package/services/tests/api.test.js +35 -20
- package/utils/commonUtils.js +22 -1
- package/utils/rcsPayloadUtils.js +102 -0
- package/utils/templateVarUtils.js +198 -0
- package/utils/tests/rcsPayloadUtils.test.js +295 -0
- package/utils/tests/templateVarUtils.test.js +204 -0
- package/v2Components/CapActionButton/constants.js +21 -1
- package/v2Components/CapActionButton/index.js +260 -144
- package/v2Components/CapActionButton/index.scss +245 -14
- package/v2Components/CapActionButton/messages.js +32 -3
- package/v2Components/CapActionButton/tests/index.test.js +74 -19
- package/v2Components/CapImageUpload/index.js +2 -2
- package/v2Components/CapTagList/index.js +9 -0
- package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
- package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
- package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
- package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
- package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
- package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
- package/v2Components/CommonTestAndPreview/constants.js +35 -2
- package/v2Components/CommonTestAndPreview/index.js +770 -231
- package/v2Components/CommonTestAndPreview/messages.js +45 -3
- package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
- package/v2Components/CommonTestAndPreview/sagas.js +25 -6
- package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
- package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
- package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
- package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
- package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +108 -15
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
- package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
- package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
- package/v2Components/CommonTestAndPreview/utils.js +84 -0
- package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
- package/v2Components/SmsFallback/constants.js +94 -0
- package/v2Components/SmsFallback/index.js +958 -0
- package/v2Components/SmsFallback/index.scss +266 -0
- package/v2Components/SmsFallback/messages.js +78 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
- package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
- package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
- package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
- package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
- package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
- package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
- package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
- package/v2Components/TemplatePreview/constants.js +2 -0
- package/v2Components/TemplatePreview/index.js +147 -32
- package/v2Components/TemplatePreview/tests/index.test.js +142 -0
- package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
- package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
- package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
- package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
- package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
- package/v2Components/VarSegmentMessageEditor/index.js +125 -0
- package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
- package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
- package/v2Containers/CreativesContainer/constants.js +11 -0
- package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
- package/v2Containers/CreativesContainer/index.js +328 -106
- package/v2Containers/CreativesContainer/index.scss +102 -1
- package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
- package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
- package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
- package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
- package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
- package/v2Containers/Email/index.js +3 -63
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
- package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
- package/v2Containers/Rcs/carouselUtils.js +224 -0
- package/v2Containers/Rcs/components/CarouselCard.js +317 -0
- package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
- package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
- package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
- package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
- package/v2Containers/Rcs/constants.js +132 -16
- package/v2Containers/Rcs/index.js +1808 -949
- package/v2Containers/Rcs/index.scss +443 -8
- package/v2Containers/Rcs/messages.js +45 -22
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +484 -0
- package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
- package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
- package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
- package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
- package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
- package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
- package/v2Containers/Rcs/tests/index.test.js +219 -40
- package/v2Containers/Rcs/tests/mockData.js +38 -0
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
- package/v2Containers/Rcs/tests/utils.test.js +682 -30
- package/v2Containers/Rcs/utils.js +514 -12
- package/v2Containers/Sms/Create/index.js +115 -48
- package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
- package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
- package/v2Containers/SmsTrai/Create/index.js +9 -4
- package/v2Containers/SmsTrai/Edit/constants.js +2 -0
- package/v2Containers/SmsTrai/Edit/index.js +678 -169
- package/v2Containers/SmsTrai/Edit/index.scss +126 -0
- package/v2Containers/SmsTrai/Edit/messages.js +14 -4
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
- package/v2Containers/SmsWrapper/index.js +37 -8
- package/v2Containers/TagList/index.js +5 -0
- package/v2Containers/Templates/TemplatesActionBar.js +101 -0
- package/v2Containers/Templates/_templates.scss +196 -12
- package/v2Containers/Templates/actions.js +11 -0
- package/v2Containers/Templates/constants.js +2 -0
- package/v2Containers/Templates/index.js +131 -59
- package/v2Containers/Templates/sagas.js +57 -13
- package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
- package/v2Containers/Templates/tests/sagas.test.js +199 -16
- package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
- package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
- package/v2Containers/TemplatesV2/index.js +88 -25
- package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
- package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
- package/v2Containers/Whatsapp/index.js +3 -20
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
|
@@ -0,0 +1,471 @@
|
|
|
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
|
+
|
|
423
|
+
it('defaults perPage to 25 when not provided', async () => {
|
|
424
|
+
const fetchTemplates = jest.fn().mockResolvedValue({
|
|
425
|
+
templates: Array.from({ length: 25 }, (_, i) => ({ _id: `t${i}` })),
|
|
426
|
+
totalCount: 0,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const { result } = renderHook(() => useLocalTemplateList({ fetchTemplates }));
|
|
430
|
+
|
|
431
|
+
act(() => {
|
|
432
|
+
result.current.reset('');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
await waitFor(() => {
|
|
436
|
+
expect(result.current.loading).toBe(false);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// hasMoreByFullPage requires templates.length >= perPage; with the default
|
|
440
|
+
// perPage of 25 and exactly 25 templates returned, more pages are assumed available.
|
|
441
|
+
expect(result.current.canLoadMore).toBe(true);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('reset() with no argument reuses the last search term set via setSearch', async () => {
|
|
445
|
+
const fetchTemplates = jest.fn().mockResolvedValue({
|
|
446
|
+
templates: [{ _id: 'a' }],
|
|
447
|
+
totalCount: 1,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const { result } = renderHook(() =>
|
|
451
|
+
useLocalTemplateList({ fetchTemplates, perPage: 25 }),
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
act(() => {
|
|
455
|
+
result.current.setSearch('carried-over');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
act(() => {
|
|
459
|
+
result.current.reset();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
await waitFor(() => {
|
|
463
|
+
expect(result.current.loading).toBe(false);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
expect(fetchTemplates).toHaveBeenCalledWith(
|
|
467
|
+
expect.objectContaining({ search: 'carried-over' }),
|
|
468
|
+
);
|
|
469
|
+
expect(result.current.search).toBe('carried-over');
|
|
470
|
+
});
|
|
471
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
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: requestedPage = 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: requestedPage, 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(requestedPage);
|
|
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
|
+
}
|
|
@@ -13,9 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
.shell-v2 {
|
|
15
15
|
position: relative;
|
|
16
|
-
|
|
17
|
-
transform: translate(-50%);
|
|
18
|
-
left: 50%;
|
|
16
|
+
margin: 0 auto;
|
|
19
17
|
padding: 0 10px;
|
|
20
18
|
width: 450px;
|
|
21
19
|
|
|
@@ -537,20 +535,21 @@
|
|
|
537
535
|
.unicode-disabled{
|
|
538
536
|
font-size: 16px;
|
|
539
537
|
}
|
|
538
|
+
position: absolute;
|
|
539
|
+
overflow: auto;
|
|
540
|
+
top: 0;
|
|
541
|
+
left: 17%;
|
|
542
|
+
white-space: pre-wrap;
|
|
543
|
+
word-break: break-word;
|
|
544
|
+
max-height: 100%;
|
|
545
|
+
line-height: 0.875rem;
|
|
546
|
+
font-size: 0.625rem;
|
|
547
|
+
font-family: 'open-sans';
|
|
548
|
+
|
|
540
549
|
&.sms {
|
|
541
|
-
max-height: 100%;
|
|
542
|
-
position: absolute;
|
|
543
550
|
// width: initial;
|
|
544
|
-
overflow: auto;
|
|
545
|
-
top: 0;
|
|
546
|
-
left: 17%;
|
|
547
|
-
white-space: pre-wrap;
|
|
548
|
-
word-break: break-word;
|
|
549
|
-
line-height: 14px;
|
|
550
551
|
padding: 0 8px 8px 8px;
|
|
551
|
-
font-size: 10px;
|
|
552
552
|
width: 100%;
|
|
553
|
-
font-family: 'open-sans';
|
|
554
553
|
.rcs-image{
|
|
555
554
|
width: 100%;
|
|
556
555
|
height: 123px;
|
|
@@ -571,6 +570,9 @@
|
|
|
571
570
|
margin-top: 0;
|
|
572
571
|
padding: 4px 8px;
|
|
573
572
|
}
|
|
573
|
+
&.rcs-desc {
|
|
574
|
+
margin-top: $CAP_SPACE_08;
|
|
575
|
+
}
|
|
574
576
|
&.rcs-button-text{
|
|
575
577
|
color: $FONT_COLOR_05;
|
|
576
578
|
.rcs-icon{
|
|
@@ -582,22 +584,12 @@
|
|
|
582
584
|
&:not(.sms){
|
|
583
585
|
padding: 4px;
|
|
584
586
|
background: #3F51B5;
|
|
585
|
-
position: absolute;
|
|
586
587
|
// width: initial;
|
|
587
|
-
overflow: auto;
|
|
588
|
-
top: 0;
|
|
589
|
-
left: 17%;
|
|
590
|
-
white-space: pre-wrap;
|
|
591
|
-
word-break: break-word;
|
|
592
588
|
border-radius: 4px;
|
|
593
|
-
max-height: 100%;
|
|
594
589
|
color: #FFFFFF;
|
|
595
590
|
width: 70%;
|
|
596
591
|
min-height: 26px;
|
|
597
|
-
line-height: 14px;
|
|
598
592
|
padding: 8px;
|
|
599
|
-
font-size: 10px;
|
|
600
|
-
font-family: 'open-sans';
|
|
601
593
|
}
|
|
602
594
|
&.message-pop-carousel {
|
|
603
595
|
position: relative;
|
|
@@ -842,9 +834,13 @@
|
|
|
842
834
|
|
|
843
835
|
.msg-container-carousel {
|
|
844
836
|
display: flex;
|
|
837
|
+
width: 100%;
|
|
845
838
|
.scroll-container {
|
|
846
839
|
overflow-x: auto;
|
|
840
|
+
overflow-y: hidden;
|
|
847
841
|
display: flex;
|
|
842
|
+
flex-wrap: nowrap;
|
|
843
|
+
width: 100%;
|
|
848
844
|
padding-top: $CAP_SPACE_06;
|
|
849
845
|
padding-right: $CAP_SPACE_06;
|
|
850
846
|
white-space: nowrap;
|
|
@@ -865,6 +861,32 @@
|
|
|
865
861
|
}
|
|
866
862
|
}
|
|
867
863
|
|
|
864
|
+
.shell-v2.rcs-preview {
|
|
865
|
+
// Collapse the default 8px margin so the divider renders as a clean hairline, not a white gap.
|
|
866
|
+
.whatsapp-divider {
|
|
867
|
+
margin: 0;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Override `.message-pop:not(.sms)` (blue background) for RCS carousel cards.
|
|
871
|
+
.msg-container.sms {
|
|
872
|
+
.message-pop.sms {
|
|
873
|
+
.rcs-carousel-card {
|
|
874
|
+
background: $CAP_WHITE;
|
|
875
|
+
color: $CAP_G01;
|
|
876
|
+
width: 10.4rem;
|
|
877
|
+
left: 0;
|
|
878
|
+
flex-shrink: 0;
|
|
879
|
+
padding: $CAP_SPACE_04 0 $CAP_SPACE_08;
|
|
880
|
+
border-radius: $CAP_SPACE_04; // 4px (Figma RCS carousel card corner)
|
|
881
|
+
|
|
882
|
+
.carousel-title {
|
|
883
|
+
font-weight: 700 !important;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
868
890
|
.align-center {
|
|
869
891
|
text-align: center;
|
|
870
892
|
}
|
|
@@ -1063,8 +1085,6 @@
|
|
|
1063
1085
|
}
|
|
1064
1086
|
.video-icon {
|
|
1065
1087
|
position: absolute;
|
|
1066
|
-
right: -17px;
|
|
1067
|
-
bottom: -17px;
|
|
1068
1088
|
}
|
|
1069
1089
|
|
|
1070
1090
|
.zalo-preview-container {
|