@cccsaurora/howler-ui 2.19.0-dev.1000 → 2.19.0-dev.1023

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 (56) hide show
  1. package/api/action/index.d.ts +5 -4
  2. package/api/action/index.js +8 -8
  3. package/api/analytic/index.d.ts +3 -2
  4. package/api/analytic/index.js +4 -4
  5. package/api/analytic/owner.d.ts +2 -1
  6. package/api/analytic/owner.js +2 -2
  7. package/api/analytic/rules.d.ts +2 -1
  8. package/api/analytic/rules.js +2 -2
  9. package/api/dossier/index.d.ts +4 -3
  10. package/api/dossier/index.js +6 -6
  11. package/api/hit/index.d.ts +3 -2
  12. package/api/hit/index.js +4 -4
  13. package/api/hit/labels.d.ts +3 -2
  14. package/api/hit/labels.js +4 -4
  15. package/api/hit/overwrite.d.ts +2 -1
  16. package/api/hit/overwrite.js +2 -2
  17. package/api/hit/transition.d.ts +2 -1
  18. package/api/hit/transition.js +2 -2
  19. package/api/index.d.ts +5 -4
  20. package/api/index.js +8 -8
  21. package/api/overview/index.d.ts +4 -3
  22. package/api/overview/index.js +6 -6
  23. package/api/search/overview.d.ts +2 -2
  24. package/api/template/index.d.ts +4 -3
  25. package/api/template/index.js +6 -6
  26. package/api/user/index.d.ts +2 -1
  27. package/api/user/index.js +2 -2
  28. package/api/view/index.d.ts +4 -3
  29. package/api/view/index.js +6 -6
  30. package/components/app/providers/SearchResponseProvider.d.ts +21 -0
  31. package/components/app/providers/SearchResponseProvider.js +81 -0
  32. package/components/app/providers/SearchResponseProvider.test.d.ts +1 -0
  33. package/components/app/providers/SearchResponseProvider.test.js +406 -0
  34. package/components/app/providers/ViewProvider.test.js +4 -1
  35. package/components/elements/addons/search/SearchPagination.d.ts +2 -1
  36. package/components/elements/addons/search/SearchPagination.js +4 -4
  37. package/components/elements/addons/search/SearchTotal.test.js +3 -1
  38. package/components/elements/display/ItemManager.d.ts +2 -2
  39. package/components/elements/display/ItemManager.js +1 -1
  40. package/components/hooks/useMyApi.d.ts +1 -1
  41. package/components/routes/action/useMyActionFunctions.js +5 -2
  42. package/components/routes/action/view/ActionDetails.js +7 -1
  43. package/components/routes/action/view/ActionSearch.js +20 -14
  44. package/components/routes/analytics/AnalyticDetails.js +1 -1
  45. package/components/routes/analytics/AnalyticSearch.js +6 -6
  46. package/components/routes/dossiers/Dossiers.js +13 -11
  47. package/components/routes/home/AddNewCard.test.js +20 -2
  48. package/components/routes/home/ViewRefresh.test.js +1 -1
  49. package/components/routes/overviews/OverviewViewer.js +6 -2
  50. package/components/routes/overviews/Overviews.js +13 -11
  51. package/components/routes/templates/TemplateViewer.js +7 -2
  52. package/components/routes/templates/Templates.js +16 -11
  53. package/components/routes/views/Views.js +15 -12
  54. package/locales/en/translation.json +4 -1
  55. package/locales/fr/translation.json +4 -1
  56. package/package.json +103 -103
@@ -0,0 +1,406 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { useContext } from 'react';
4
+ import SearchResponseProvider, { SearchResponseContext } from './SearchResponseProvider';
5
+ const TEST_PAGE_SIZE = 25;
6
+ const TEST_TOTAL_COUNT = 100;
7
+ const makeWrapper = (initialResponse) => {
8
+ const Wrapper = ({ children }) => (_jsx(SearchResponseProvider, { idField: "id", initialResponse: initialResponse, children: children }));
9
+ return Wrapper;
10
+ };
11
+ const renderProvider = (initialResponse) => {
12
+ return renderHook(() => useContext(SearchResponseContext), {
13
+ wrapper: makeWrapper(initialResponse)
14
+ });
15
+ };
16
+ describe('push', () => {
17
+ it("should add an item to the response if it doesn't exist", () => {
18
+ const hook = renderProvider({
19
+ items: [{ id: '0', name: 'existing' }],
20
+ offset: 0,
21
+ rows: TEST_PAGE_SIZE,
22
+ total: TEST_TOTAL_COUNT,
23
+ removeCount: 0
24
+ });
25
+ act(() => {
26
+ hook.result.current.push({ id: '1', name: 'test' });
27
+ });
28
+ expect(hook.result.current.response).toEqual({
29
+ items: [
30
+ { id: '0', name: 'existing' },
31
+ { id: '1', name: 'test' }
32
+ ],
33
+ offset: 0,
34
+ rows: TEST_PAGE_SIZE,
35
+ total: TEST_TOTAL_COUNT + 1,
36
+ removeCount: 0
37
+ });
38
+ });
39
+ it('should replace an item in the response if it already exists', () => {
40
+ const hook = renderProvider({
41
+ items: [{ id: '0', name: 'old' }],
42
+ offset: 0,
43
+ rows: TEST_PAGE_SIZE,
44
+ total: TEST_TOTAL_COUNT,
45
+ removeCount: 0
46
+ });
47
+ act(() => {
48
+ hook.result.current.push({ id: '0', name: 'new' });
49
+ });
50
+ expect(hook.result.current.response).toEqual({
51
+ items: [{ id: '0', name: 'new' }],
52
+ offset: 0,
53
+ rows: TEST_PAGE_SIZE,
54
+ total: TEST_TOTAL_COUNT,
55
+ removeCount: 0
56
+ });
57
+ });
58
+ it('should update total but not add item if the response has the maximum number of items', () => {
59
+ const items = Array.from({ length: TEST_PAGE_SIZE }, (_, i) => ({ id: i.toString(), name: `item${i}` }));
60
+ const newItem = { id: TEST_PAGE_SIZE.toString(), name: 'test' };
61
+ const hook = renderProvider({
62
+ items,
63
+ offset: 0,
64
+ rows: TEST_PAGE_SIZE,
65
+ total: TEST_TOTAL_COUNT,
66
+ removeCount: 0
67
+ });
68
+ act(() => {
69
+ hook.result.current.push(newItem);
70
+ });
71
+ expect(hook.result.current.response?.items).not.toContainEqual(newItem);
72
+ expect(hook.result.current.response).toEqual({
73
+ items,
74
+ offset: 0,
75
+ rows: TEST_PAGE_SIZE,
76
+ total: TEST_TOTAL_COUNT + 1,
77
+ removeCount: 0
78
+ });
79
+ });
80
+ it('should decrement removeCount if the item does not exist already', () => {
81
+ const hook = renderProvider({
82
+ items: [{ id: '0', name: 'existing' }],
83
+ offset: 0,
84
+ rows: TEST_PAGE_SIZE,
85
+ total: TEST_TOTAL_COUNT,
86
+ removeCount: 1
87
+ });
88
+ act(() => {
89
+ hook.result.current.push({ id: '1', name: 'test' });
90
+ });
91
+ expect(hook.result.current.response).not.toBeNull();
92
+ expect(hook.result.current.response.removeCount).toBe(0);
93
+ });
94
+ it('should not decrement removeCount if the item already exists', () => {
95
+ const hook = renderProvider({
96
+ items: [{ id: '0', name: 'existing' }],
97
+ offset: 0,
98
+ rows: TEST_PAGE_SIZE,
99
+ total: TEST_TOTAL_COUNT,
100
+ removeCount: 1
101
+ });
102
+ act(() => {
103
+ hook.result.current.push({ id: '0', name: 'new' });
104
+ });
105
+ expect(hook.result.current.response).not.toBeNull();
106
+ expect(hook.result.current.response.removeCount).toBe(1);
107
+ });
108
+ it('should keep response null if it is uninitiated', () => {
109
+ const hook = renderProvider();
110
+ act(() => {
111
+ hook.result.current.push({ id: '0', name: 'test' });
112
+ });
113
+ expect(hook.result.current.response).toBeNull();
114
+ });
115
+ });
116
+ describe('remove', () => {
117
+ it('should remove an item from the response if it exists', () => {
118
+ const hook = renderProvider({
119
+ items: [
120
+ { id: '0', name: 'item0' },
121
+ { id: '1', name: 'item1' }
122
+ ],
123
+ offset: 0,
124
+ rows: TEST_PAGE_SIZE,
125
+ total: TEST_TOTAL_COUNT,
126
+ removeCount: 0
127
+ });
128
+ act(() => {
129
+ hook.result.current.remove('0');
130
+ });
131
+ expect(hook.result.current.response).toEqual({
132
+ items: [{ id: '1', name: 'item1' }],
133
+ offset: 0,
134
+ rows: TEST_PAGE_SIZE,
135
+ total: TEST_TOTAL_COUNT - 1,
136
+ removeCount: 1
137
+ });
138
+ });
139
+ it('should ignore the remove if the item does not exist', () => {
140
+ const hook = renderProvider({
141
+ items: [{ id: '0', name: 'item' }],
142
+ offset: 0,
143
+ rows: TEST_PAGE_SIZE,
144
+ total: TEST_TOTAL_COUNT,
145
+ removeCount: 0
146
+ });
147
+ act(() => {
148
+ hook.result.current.remove('1');
149
+ });
150
+ expect(hook.result.current.response).toEqual({
151
+ items: [{ id: '0', name: 'item' }],
152
+ offset: 0,
153
+ rows: TEST_PAGE_SIZE,
154
+ total: TEST_TOTAL_COUNT,
155
+ removeCount: 0
156
+ });
157
+ });
158
+ it('should keep response null if it is uninitiated', () => {
159
+ const hook = renderProvider();
160
+ act(() => {
161
+ hook.result.current.remove('0');
162
+ });
163
+ expect(hook.result.current.response).toBeNull();
164
+ });
165
+ });
166
+ describe('replace', () => {
167
+ it('should replace an item in the response if it exists', () => {
168
+ const hook = renderProvider({
169
+ items: [{ id: '0', name: 'old' }],
170
+ offset: 0,
171
+ rows: TEST_PAGE_SIZE,
172
+ total: TEST_TOTAL_COUNT,
173
+ removeCount: 0
174
+ });
175
+ act(() => {
176
+ hook.result.current.replace('0', { id: undefined, name: 'new' });
177
+ });
178
+ expect(hook.result.current.response).toEqual({
179
+ items: [{ id: '0', name: 'new' }],
180
+ offset: 0,
181
+ rows: TEST_PAGE_SIZE,
182
+ total: TEST_TOTAL_COUNT,
183
+ removeCount: 0
184
+ });
185
+ });
186
+ it('should ignore the replace if the item does not exist', () => {
187
+ const hook = renderProvider({
188
+ items: [{ id: '0', name: 'item' }],
189
+ offset: 0,
190
+ rows: TEST_PAGE_SIZE,
191
+ total: TEST_TOTAL_COUNT,
192
+ removeCount: 0
193
+ });
194
+ act(() => {
195
+ hook.result.current.replace('1', { id: undefined, name: 'new' });
196
+ });
197
+ expect(hook.result.current.response).not.toContainEqual({ id: '1', name: 'new' });
198
+ expect(hook.result.current.response).toEqual({
199
+ items: [{ id: '0', name: 'item' }],
200
+ offset: 0,
201
+ rows: TEST_PAGE_SIZE,
202
+ total: TEST_TOTAL_COUNT,
203
+ removeCount: 0
204
+ });
205
+ });
206
+ it('should keep response null if it is uninitiated', () => {
207
+ const hook = renderProvider();
208
+ act(() => {
209
+ hook.result.current.replace('0', { id: '0', name: 'new' });
210
+ });
211
+ expect(hook.result.current.response).toBeNull();
212
+ });
213
+ it('should throw error if item id does not match the id provided', () => {
214
+ const hook = renderProvider({
215
+ items: [{ id: '0', name: 'old' }],
216
+ offset: 0,
217
+ rows: TEST_PAGE_SIZE,
218
+ total: TEST_TOTAL_COUNT,
219
+ removeCount: 0
220
+ });
221
+ expect(() => act(() => {
222
+ hook.result.current.replace('0', { id: '1', name: 'new' });
223
+ })).toThrow(/id does not match/);
224
+ });
225
+ });
226
+ describe('request', () => {
227
+ const apiSearchMock = vi.fn();
228
+ beforeEach(() => {
229
+ apiSearchMock.mockReset();
230
+ });
231
+ it('should update the response with the result of the request', async () => {
232
+ const hook = renderProvider({
233
+ items: [{ id: '0', name: 'item' }],
234
+ offset: 0,
235
+ rows: TEST_PAGE_SIZE,
236
+ total: TEST_TOTAL_COUNT,
237
+ removeCount: 0
238
+ });
239
+ const request = {
240
+ query: 'test',
241
+ rows: TEST_PAGE_SIZE,
242
+ offset: 0
243
+ };
244
+ apiSearchMock.mockResolvedValue({
245
+ items: [{ id: '1', name: 'new' }],
246
+ offset: request.offset,
247
+ rows: TEST_PAGE_SIZE,
248
+ total: TEST_TOTAL_COUNT
249
+ });
250
+ await act(async () => {
251
+ await hook.result.current.request(apiSearchMock, request);
252
+ });
253
+ expect(apiSearchMock).toHaveBeenCalledWith(request);
254
+ expect(hook.result.current.response).toEqual({
255
+ items: [{ id: '1', name: 'new' }],
256
+ offset: request.offset,
257
+ rows: TEST_PAGE_SIZE,
258
+ total: TEST_TOTAL_COUNT,
259
+ removeCount: 0
260
+ });
261
+ });
262
+ it.for([
263
+ { description: 'before', offset: 0 },
264
+ { description: 'the same as', offset: TEST_PAGE_SIZE }
265
+ ])('should reset removeCount if the offset is $description the current offset', async ({ offset }) => {
266
+ const hook = renderProvider({
267
+ items: [{ id: '0', name: 'item' }],
268
+ offset: TEST_PAGE_SIZE,
269
+ rows: TEST_PAGE_SIZE,
270
+ total: TEST_TOTAL_COUNT,
271
+ removeCount: 5
272
+ });
273
+ const request = {
274
+ query: 'test',
275
+ rows: TEST_PAGE_SIZE,
276
+ offset: offset
277
+ };
278
+ apiSearchMock.mockResolvedValue({
279
+ items: [{ id: '1', name: 'new' }],
280
+ offset: request.offset,
281
+ rows: TEST_PAGE_SIZE,
282
+ total: TEST_TOTAL_COUNT
283
+ });
284
+ await act(async () => {
285
+ await hook.result.current.request(apiSearchMock, request);
286
+ });
287
+ expect(apiSearchMock).toHaveBeenCalledWith(request);
288
+ expect(hook.result.current.response).toEqual({
289
+ items: [{ id: '1', name: 'new' }],
290
+ offset: request.offset,
291
+ rows: TEST_PAGE_SIZE,
292
+ total: TEST_TOTAL_COUNT,
293
+ removeCount: 0
294
+ });
295
+ });
296
+ it('should not reset removeCount if the offset is after the current offset', async () => {
297
+ const hook = renderProvider({
298
+ items: [{ id: '0', name: 'item' }],
299
+ offset: TEST_PAGE_SIZE,
300
+ rows: TEST_PAGE_SIZE,
301
+ total: TEST_TOTAL_COUNT,
302
+ removeCount: 5
303
+ });
304
+ const request = {
305
+ query: 'test',
306
+ rows: TEST_PAGE_SIZE,
307
+ offset: TEST_PAGE_SIZE * 2
308
+ };
309
+ apiSearchMock.mockResolvedValue({
310
+ items: [{ id: '1', name: 'new' }],
311
+ offset: request.offset,
312
+ rows: TEST_PAGE_SIZE,
313
+ total: TEST_TOTAL_COUNT
314
+ });
315
+ await act(async () => {
316
+ await hook.result.current.request(apiSearchMock, request);
317
+ });
318
+ expect(apiSearchMock).toHaveBeenCalledWith(request);
319
+ expect(hook.result.current.response).toEqual({
320
+ items: [{ id: '1', name: 'new' }],
321
+ offset: request.offset,
322
+ rows: TEST_PAGE_SIZE,
323
+ total: TEST_TOTAL_COUNT,
324
+ removeCount: 5
325
+ });
326
+ });
327
+ it('should keep response undefined if the request fails', async () => {
328
+ const hook = renderProvider();
329
+ const request = {
330
+ query: 'test',
331
+ rows: TEST_PAGE_SIZE,
332
+ offset: 0
333
+ };
334
+ apiSearchMock.mockRejectedValue(new Error('Request failed'));
335
+ await act(async () => {
336
+ await hook.result.current.request(apiSearchMock, request).catch(() => { });
337
+ });
338
+ expect(apiSearchMock).toHaveBeenCalledWith(request);
339
+ expect(hook.result.current.response).toBeNull();
340
+ });
341
+ it('should keep response unchanged if the request fails', async () => {
342
+ const initialResponse = {
343
+ items: [{ id: '0', name: 'item' }],
344
+ offset: 0,
345
+ rows: TEST_PAGE_SIZE,
346
+ total: TEST_TOTAL_COUNT,
347
+ removeCount: 5
348
+ };
349
+ const hook = renderProvider(initialResponse);
350
+ const request = {
351
+ query: 'test',
352
+ rows: TEST_PAGE_SIZE,
353
+ offset: 0
354
+ };
355
+ apiSearchMock.mockRejectedValue(new Error('Request failed'));
356
+ await act(async () => {
357
+ await hook.result.current.request(apiSearchMock, request).catch(() => { });
358
+ });
359
+ expect(apiSearchMock).toHaveBeenCalledWith(request);
360
+ expect(hook.result.current.response).toEqual(initialResponse);
361
+ });
362
+ it('should throw an error if the request throws an error', async () => {
363
+ const hook = renderProvider();
364
+ const request = {
365
+ query: 'test',
366
+ rows: TEST_PAGE_SIZE,
367
+ offset: 0
368
+ };
369
+ apiSearchMock.mockRejectedValue(new Error('Request failed'));
370
+ await expect(act(async () => {
371
+ await hook.result.current.request(apiSearchMock, request);
372
+ })).rejects.toThrow('Request failed');
373
+ });
374
+ });
375
+ describe('getSearchRequestData', () => {
376
+ it.for([
377
+ { description: 'no remove count', removeCount: 0 },
378
+ { description: 'with remove count', removeCount: 5 }
379
+ ])('should modify offset if it is provided - $description', ({ removeCount }) => {
380
+ const hook = renderProvider({
381
+ items: [],
382
+ offset: 0,
383
+ rows: TEST_PAGE_SIZE,
384
+ total: TEST_TOTAL_COUNT,
385
+ removeCount: removeCount
386
+ });
387
+ const modifiedRequest = hook.result.current.getSearchRequestData({ offset: TEST_PAGE_SIZE });
388
+ expect(modifiedRequest.offset).toBe(TEST_PAGE_SIZE - removeCount);
389
+ });
390
+ it('should not modify offset if it is not provided', () => {
391
+ const hook = renderProvider({
392
+ items: [],
393
+ offset: 0,
394
+ rows: TEST_PAGE_SIZE,
395
+ total: TEST_TOTAL_COUNT,
396
+ removeCount: 5
397
+ });
398
+ const modifiedRequest = hook.result.current.getSearchRequestData({ query: 'value' });
399
+ expect(modifiedRequest.offset).not.toBeDefined();
400
+ });
401
+ it('should not modify offset if response is uninitialised', () => {
402
+ const hook = renderProvider();
403
+ const modifiedRequest = hook.result.current.getSearchRequestData({ offset: TEST_PAGE_SIZE });
404
+ expect(modifiedRequest.offset).toBe(TEST_PAGE_SIZE);
405
+ });
406
+ });
@@ -151,7 +151,10 @@ describe('ViewContext', () => {
151
151
  it('should allow users to edit views', async () => {
152
152
  const result = await act(async () => hook.result.current('example_view_id', { query: DEFAULT_QUERY }));
153
153
  expect(hput).toHaveBeenCalledOnce();
154
- expect(hput).toBeCalledWith('/api/v1/view/example_view_id', { query: DEFAULT_QUERY });
154
+ const [url, body, ...rest] = vi.mocked(hput).mock.calls[0];
155
+ expect(url).toBe('/api/v1/view/example_view_id');
156
+ expect(body).toEqual({ query: DEFAULT_QUERY });
157
+ expect(rest.every(v => v === undefined)).toBe(true);
155
158
  expect(result).toEqual(MOCK_RESPONSES['/api/v1/view/example_view_id']);
156
159
  });
157
160
  });
@@ -3,7 +3,8 @@ type SearchPaginationProps = Omit<PaginationProps, 'onChange'> & {
3
3
  limit: number;
4
4
  offset: number;
5
5
  total: number;
6
+ removeCount?: number;
6
7
  onChange: (nextOffset: number) => void;
7
8
  };
8
- declare const SearchPagination: ({ limit, offset, total, onChange, ...paginationProps }: SearchPaginationProps) => import("react/jsx-runtime").JSX.Element;
9
+ declare const SearchPagination: ({ limit, offset, total, removeCount, onChange, ...paginationProps }: SearchPaginationProps) => import("react/jsx-runtime").JSX.Element;
9
10
  export default SearchPagination;
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Pagination } from '@mui/material';
3
3
  import { useCallback } from 'react';
4
- const SearchPagination = ({ limit, offset, total, onChange, ...paginationProps }) => {
4
+ const SearchPagination = ({ limit, offset, total, removeCount, onChange, ...paginationProps }) => {
5
5
  const onPageChange = useCallback((_event, nextPage) => {
6
6
  onChange(nextPage === 1 ? 0 : (nextPage - 1) * limit);
7
7
  }, [limit, onChange]);
8
- const count = Math.ceil(total / limit);
9
- const page = Math.floor((offset + 1) / limit) + 1;
10
- return limit && total && limit < total ? (_jsx(Pagination, { count: count, page: page, onChange: onPageChange, ...paginationProps })) : null;
8
+ const count = Math.ceil((total + (removeCount ?? 0)) / limit);
9
+ const page = Math.floor((offset + 1 + (removeCount ?? 0)) / limit) + 1;
10
+ return limit && total && count > 1 ? (_jsx(Pagination, { count: count, page: page, onChange: onPageChange, ...paginationProps })) : null;
11
11
  };
12
12
  export default SearchPagination;
@@ -38,7 +38,9 @@ describe('SearchTotal', () => {
38
38
  expect(container.firstChild).toHaveClass('custom');
39
39
  });
40
40
  it('passes variant to Typography', () => {
41
- const { container } = render(_jsx(SearchTotal, { total: 10, offset: 0, pageLength: 10, variant: "caption" }), { wrapper: Wrapper });
41
+ const { container } = render(_jsx(SearchTotal, { total: 10, offset: 0, pageLength: 10, variant: "caption" }), {
42
+ wrapper: Wrapper
43
+ });
42
44
  expect(container.firstChild).toHaveClass('MuiTypography-caption');
43
45
  });
44
46
  });
@@ -1,4 +1,4 @@
1
- import type { HowlerSearchResponse } from '@cccsaurora/howler-ui/api/search';
1
+ import type { SearchResponseState } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
2
2
  import type { TuiListItemOnSelect, TuiListItemRenderer } from '@cccsaurora/howler-ui/components/elements/addons/lists';
3
3
  import type { FC, ReactNode } from 'react';
4
4
  interface ItemManagerProps {
@@ -13,7 +13,7 @@ interface ItemManagerProps {
13
13
  onSelect?: TuiListItemOnSelect<unknown>;
14
14
  phrase: string;
15
15
  renderer: TuiListItemRenderer<unknown>;
16
- response: HowlerSearchResponse<unknown>;
16
+ response: SearchResponseState<unknown>;
17
17
  searchAdornment?: ReactNode;
18
18
  searching: boolean;
19
19
  createPrompt?: string;
@@ -24,7 +24,7 @@ const ItemManager = ({ aboveSearch, afterSearch, belowSearch, searchFilters, has
24
24
  mt: -0.5,
25
25
  borderBottomLeftRadius: theme.shape.borderRadius,
26
26
  borderBottomRightRadius: theme.shape.borderRadius
27
- }) }))] }), afterSearch] }), searchFilters, response && (_jsxs(Stack, { direction: "row", alignItems: "center", mt: 0.5, children: [_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) }), _jsx(Box, { flex: 1 }), _jsx(SearchPagination, { total: response.total, limit: response.rows, offset: response.offset, onChange: onPageChange })] })), belowSearch, _jsx(TuiList, { onSelection: onSelect, children: renderer }), onCreate && (_jsxs(Fab, { variant: "extended", size: "large", color: "primary", sx: theme => ({
27
+ }) }))] }), afterSearch] }), searchFilters, response && (_jsxs(Stack, { direction: "row", alignItems: "center", mt: 0.5, children: [_jsx(SearchTotal, { total: response.total, pageLength: response.items.length, offset: response.offset, sx: theme => ({ color: theme.palette.text.secondary, fontSize: '0.9em', fontStyle: 'italic' }) }), _jsx(Box, { flex: 1 }), _jsx(SearchPagination, { total: response.total, limit: response.rows, offset: response.offset, removeCount: response.removeCount, onChange: onPageChange })] })), belowSearch, _jsx(TuiList, { onSelection: onSelect, children: renderer }), onCreate && (_jsxs(Fab, { variant: "extended", size: "large", color: "primary", sx: theme => ({
28
28
  textTransform: 'none',
29
29
  position: isNarrow ? 'fixed' : 'absolute',
30
30
  right: isNarrow ? theme.spacing(2) : `calc(100% + ${theme.spacing(5)})`,
@@ -1,4 +1,4 @@
1
- type DispatchApiConfig = {
1
+ export type DispatchApiConfig = {
2
2
  throwError?: boolean;
3
3
  logError?: boolean;
4
4
  showError?: boolean;
@@ -163,8 +163,11 @@ const useMyActionFunctions = () => {
163
163
  deleteAction: useCallback(async (actionId) => {
164
164
  setLoading(true);
165
165
  try {
166
- await dispatchApi(api.action.del(actionId));
167
- if (location.pathname.endsWith(actionId)) {
166
+ const detailedActionView = location.pathname.endsWith(actionId);
167
+ await dispatchApi(api.action.del(actionId, detailedActionView ? 'wait_for' : undefined), {
168
+ throwError: true
169
+ });
170
+ if (detailedActionView) {
168
171
  navigate('/action');
169
172
  }
170
173
  }
@@ -9,6 +9,7 @@ import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/Fle
9
9
  import Phrase from '@cccsaurora/howler-ui/components/elements/addons/search/phrase/Phrase';
10
10
  import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
11
11
  import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
12
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
12
13
  import OperationEntry from '@cccsaurora/howler-ui/components/routes/action/shared/OperationEntry';
13
14
  import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
14
15
  import { useCallback, useContext, useEffect, useState } from 'react';
@@ -28,6 +29,7 @@ const ActionDetails = () => {
28
29
  const [operations, setOperations] = useState([]);
29
30
  const [action, setAction] = useState();
30
31
  const { withConfirmDeleteModal } = useContext(ModalContext);
32
+ const { showSuccessMessage } = useMySnackbar();
31
33
  const onTriggerChange = useCallback(async (e) => {
32
34
  let newTriggers = action.triggers ?? [];
33
35
  if (e.target.checked && !newTriggers.includes(e.target.name)) {
@@ -47,6 +49,10 @@ const ActionDetails = () => {
47
49
  setLoading(false);
48
50
  }
49
51
  }, [action, dispatchApi, setLoading]);
52
+ const onDelete = useCallback(() => withConfirmDeleteModal(async () => {
53
+ await deleteAction(action?.action_id);
54
+ showSuccessMessage(t('route.actions.manager.delete.success'));
55
+ }), [withConfirmDeleteModal, deleteAction, action?.action_id, showSuccessMessage, t]);
50
56
  useEffect(() => {
51
57
  setLoading(true);
52
58
  Promise.all([
@@ -65,7 +71,7 @@ const ActionDetails = () => {
65
71
  user.roles.includes('admin') ||
66
72
  user.roles.includes('actionrunner_basic') ||
67
73
  user.roles.includes('actionrunner_advanced');
68
- return (_jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", justifyContent: "space-between", children: [_jsx(Typography, { variant: "h5", children: action?.name }), action?.owner_id && _jsx(HowlerAvatar, { sx: { width: 32, height: 32 }, userId: action.owner_id })] }), _jsx(Phrase, { fullWidth: true, value: action?.query, disabled: true, size: "small", onChange: () => { }, startAdornment: _jsx(IconButton, { onClick: () => onSearch(action?.query), children: _jsx(Search, { fontSize: "small" }) }) }), _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [response && _jsx(QueryResultText, { count: response.total, query: action?.query }), _jsx(FlexOne, {}), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Delete, {}), size: "small", variant: "outlined", color: "error", onClick: () => withConfirmDeleteModal(() => deleteAction(action?.action_id)), children: t('button.delete') })), execRoles && (_jsx(Button, { startIcon: _jsx(PlayCircleOutline, {}), size: "small", variant: "outlined", color: "success", onClick: () => executeAction(action?.action_id), children: t('route.actions.execute') })), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Edit, {}), size: "small", variant: "outlined", component: Link, to: `/action/${params.id}/edit`, children: t('route.actions.edit') }))] }), user.roles.includes('automation_advanced') && (_jsx(FormGroup, { children: _jsx(Stack, { direction: "row", spacing: 1, children: action?.operations
74
+ return (_jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", justifyContent: "space-between", children: [_jsx(Typography, { variant: "h5", children: action?.name }), action?.owner_id && _jsx(HowlerAvatar, { sx: { width: 32, height: 32 }, userId: action.owner_id })] }), _jsx(Phrase, { fullWidth: true, value: action?.query, disabled: true, size: "small", onChange: () => { }, startAdornment: _jsx(IconButton, { onClick: () => onSearch(action?.query), children: _jsx(Search, { fontSize: "small" }) }) }), _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [response && _jsx(QueryResultText, { count: response.total, query: action?.query }), _jsx(FlexOne, {}), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Delete, {}), size: "small", variant: "outlined", color: "error", onClick: onDelete, children: t('button.delete') })), execRoles && (_jsx(Button, { startIcon: _jsx(PlayCircleOutline, {}), size: "small", variant: "outlined", color: "success", onClick: () => executeAction(action?.action_id), children: t('route.actions.execute') })), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Edit, {}), size: "small", variant: "outlined", component: Link, to: `/action/${params.id}/edit`, children: t('route.actions.edit') }))] }), user.roles.includes('automation_advanced') && (_jsx(FormGroup, { children: _jsx(Stack, { direction: "row", spacing: 1, children: action?.operations
69
75
  ?.map(a => (operations ?? []).find(_action => _action.id === a.operation_id)?.triggers ?? [])
70
76
  .reduce((acc, triggers) => acc.filter(_t => triggers.includes(_t)))
71
77
  .map(trigger => (_jsx(FormControlLabel, { control: _jsx(Checkbox, { name: trigger, onChange: onTriggerChange, checked: action?.triggers?.includes(trigger) ?? false }), label: t(`route.actions.trigger.${trigger}`) }, trigger))) }) })), loading &&
@@ -4,13 +4,14 @@ import { Autocomplete, Card, CardContent, CardHeader, Chip, Grid, IconButton, St
4
4
  import api from '@cccsaurora/howler-ui/api';
5
5
  import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
6
6
  import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
7
+ import SearchResponseProvider, { SearchResponseContext } from '@cccsaurora/howler-ui/components/app/providers/SearchResponseProvider';
7
8
  import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
8
9
  import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
9
10
  import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
10
11
  import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
11
12
  import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
12
- import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
13
13
  import { useMyLocalStorageItem } from '@cccsaurora/howler-ui/components/hooks/useMyLocalStorage';
14
+ import useMySnackbar from '@cccsaurora/howler-ui/components/hooks/useMySnackbar';
14
15
  import { useCallback, useContext, useEffect, useState } from 'react';
15
16
  import { Trans, useTranslation } from 'react-i18next';
16
17
  import { useNavigate, useSearchParams } from 'react-router-dom';
@@ -21,17 +22,17 @@ const ActionSearch = () => {
21
22
  const navigate = useNavigate();
22
23
  const { t } = useTranslation();
23
24
  const { user } = useAppUser();
24
- const { dispatchApi } = useMyApi();
25
25
  const { load } = useContext(TuiListMethodContext);
26
26
  const { withConfirmDeleteModal } = useContext(ModalContext);
27
+ const { showSuccessMessage } = useMySnackbar();
27
28
  const [searchParams, setSearchParams] = useSearchParams();
28
29
  const { deleteAction } = useMyActionFunctions();
29
30
  const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
31
+ const { response, request, remove, getSearchRequestData } = useContext(SearchResponseContext);
30
32
  const [searching, setSearching] = useState(false);
31
33
  const [hasError, setHasError] = useState(false);
32
34
  const [phrase, setPhrase] = useState(searchParams.get('phrase') || '');
33
35
  const [offset, setOffset] = useState(parseInt(searchParams.get('offset')) || 0);
34
- const [response, setResponse] = useState(null);
35
36
  const [searchModifiers, setSearchModifiers] = useState([]);
36
37
  // Search Handler.
37
38
  const onSearch = useCallback(async () => {
@@ -50,13 +51,11 @@ const ActionSearch = () => {
50
51
  if (searchModifiers.length > 0) {
51
52
  query = `(${query}) AND (triggers:(${searchModifiers.join(' OR ')}))`;
52
53
  }
53
- const _response = await dispatchApi(api.search.action.post({
54
+ await request(api.search.action.post, {
54
55
  query,
55
56
  rows: pageCount,
56
57
  offset
57
- }));
58
- setResponse(_response);
59
- load(_response.items.map(u => ({ id: u.action_id, item: u })));
58
+ });
60
59
  }
61
60
  catch (e) {
62
61
  setHasError(true);
@@ -64,22 +63,29 @@ const ActionSearch = () => {
64
63
  finally {
65
64
  setSearching(false);
66
65
  }
67
- }, [dispatchApi, load, offset, pageCount, phrase, searchModifiers, searchParams, setSearchParams]);
66
+ }, [phrase, setSearchParams, searchParams, searchModifiers, request, pageCount, offset]);
67
+ useEffect(() => {
68
+ if (response) {
69
+ load(response.items.map((item) => ({ id: item.action_id, item: item })));
70
+ }
71
+ }, [response, load]);
68
72
  const onPageChange = useCallback((_offset) => {
69
73
  if (_offset !== offset) {
70
- searchParams.set('offset', _offset.toString());
74
+ const modifiedRequest = getSearchRequestData({ offset: _offset });
75
+ searchParams.set('offset', modifiedRequest.offset.toString());
71
76
  setSearchParams(searchParams, { replace: true });
72
- setOffset(_offset);
77
+ setOffset(modifiedRequest.offset);
73
78
  }
74
- }, [offset, searchParams, setSearchParams]);
79
+ }, [offset, searchParams, setSearchParams, getSearchRequestData]);
75
80
  const onDelete = useCallback((e, actionId) => {
76
81
  e.preventDefault();
77
82
  e.stopPropagation();
78
83
  withConfirmDeleteModal(async () => {
79
84
  await deleteAction(actionId);
80
- onSearch();
85
+ remove(actionId);
86
+ showSuccessMessage(t('route.actions.manager.delete.success'));
81
87
  });
82
- }, [deleteAction, onSearch, withConfirmDeleteModal]);
88
+ }, [deleteAction, remove, withConfirmDeleteModal, showSuccessMessage, t]);
83
89
  // Effect to initialize list of users.
84
90
  useEffect(() => {
85
91
  onSearch();
@@ -120,6 +126,6 @@ const ActionSearch = () => {
120
126
  return (_jsx(ItemManager, { onSearch: onSearch, onCreate: editRoles ? () => navigate('/action/execute') : undefined, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.actions.search.prompt') }), searchFilters: _jsx(Autocomplete, { multiple: true, size: "small", value: searchModifiers, onChange: (__, values) => setSearchModifiers(values), getOptionLabel: trigger => t(`route.actions.trigger.${trigger}`), options: VALID_ACTION_TRIGGERS, renderInput: params => (_jsx(TextField, { ...params, sx: { maxWidth: '500px' }, label: t('route.actions.trigger') })) }), renderer: renderer, response: response, createPrompt: "route.actions.create", searchPrompt: "route.actions.search", createIcon: _jsx(Terminal, { sx: { mr: 1 } }) }));
121
127
  };
122
128
  const ActionSearchProvider = () => {
123
- return (_jsx(TuiListProvider, { children: _jsx(ActionSearch, {}) }));
129
+ return (_jsx(TuiListProvider, { children: _jsx(SearchResponseProvider, { idField: "action_id", children: _jsx(ActionSearch, {}) }) }));
124
130
  };
125
131
  export default ActionSearchProvider;
@@ -62,7 +62,7 @@ const AnalyticDetails = () => {
62
62
  }, [analytic?.analytic_id, dispatchApi]);
63
63
  const onDelete = useCallback(() => {
64
64
  withConfirmDeleteModal(async () => {
65
- await dispatchApi(api.analytic.del(analytic?.analytic_id));
65
+ await dispatchApi(api.analytic.del(analytic?.analytic_id, 'wait_for'));
66
66
  showSuccessMessage(t('route.analytics.deleted'));
67
67
  navigate('/analytics');
68
68
  });