@codella-software/react 2.2.26 → 2.3.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.
package/dist/index.mjs CHANGED
@@ -1,77 +1,309 @@
1
- import { useState, useEffect, useMemo, createContext, useContext, useCallback, useRef } from "react";
2
- import { TableBuilder } from "@codella-software/utils";
1
+ import { FiltersAndSortService } from "@codella-software/utils";
2
+ import { useRef, useEffect, useCallback, useSyncExternalStore, useMemo, useState, createContext, useContext } from "react";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import { SSEService, WebsocketService, LiveUpdateService } from "@codella-software/utils/live-updates";
5
5
  import { RichContentService } from "@codella-software/utils/rich-content";
6
6
  import { TabsService } from "@codella-software/utils/tabs";
7
+ function defaultIsEqual(prev, next) {
8
+ if (prev === next) return true;
9
+ if (prev.query !== next.query) return false;
10
+ if (prev.pagination.page !== next.pagination.page) return false;
11
+ if (prev.pagination.limit !== next.pagination.limit) return false;
12
+ const prevSort = prev.sort;
13
+ const nextSort = next.sort;
14
+ if ((prevSort == null ? void 0 : prevSort.fieldName) !== (nextSort == null ? void 0 : nextSort.fieldName)) return false;
15
+ if ((prevSort == null ? void 0 : prevSort.direction) !== (nextSort == null ? void 0 : nextSort.direction)) return false;
16
+ const prevFilters = prev.filters;
17
+ const nextFilters = next.filters;
18
+ const prevKeys = Object.keys(prevFilters);
19
+ const nextKeys = Object.keys(nextFilters);
20
+ if (prevKeys.length !== nextKeys.length) return false;
21
+ for (const key of prevKeys) {
22
+ if (prevFilters[key] !== nextFilters[key]) return false;
23
+ }
24
+ return true;
25
+ }
26
+ function countActiveFilters(filters, defaultFilters) {
27
+ let count = 0;
28
+ const keys = Object.keys(filters);
29
+ for (const key of keys) {
30
+ const value = filters[key];
31
+ const defaultValue = defaultFilters == null ? void 0 : defaultFilters[key];
32
+ const isDefault = value === defaultValue;
33
+ const isEmpty = value === null || value === void 0 || value === "";
34
+ const isEmptyArray = Array.isArray(value) && value.length === 0;
35
+ if (!isEmpty && !isEmptyArray && !isDefault) {
36
+ count++;
37
+ }
38
+ }
39
+ return count;
40
+ }
7
41
  function useFiltersAndSort(options) {
8
- const { service } = options;
9
- if (!service) {
10
- throw new Error("useFiltersAndSort: service is required");
42
+ const { service: externalService, config, debug = false, isEqual = defaultIsEqual } = options;
43
+ if (!externalService && !config) {
44
+ throw new Error(
45
+ "useFiltersAndSort: either `service` or `config` must be provided. Pass an existing FiltersAndSortService instance via `service`, or provide a `config` object to create a new service."
46
+ );
11
47
  }
12
- const [state, setState] = useState(() => {
13
- return service.getState();
14
- });
48
+ if (externalService && config) {
49
+ if (debug) {
50
+ console.warn(
51
+ "useFiltersAndSort: both `service` and `config` were provided. The `service` will be used and `config` will be ignored."
52
+ );
53
+ }
54
+ }
55
+ const ownsServiceRef = useRef(false);
56
+ const serviceRef = useRef(null);
57
+ const isEqualRef = useRef(isEqual);
15
58
  useEffect(() => {
16
- if (!service || !service.state$) {
17
- return;
59
+ isEqualRef.current = isEqual;
60
+ }, [isEqual]);
61
+ if (!serviceRef.current) {
62
+ if (externalService) {
63
+ serviceRef.current = externalService;
64
+ ownsServiceRef.current = false;
65
+ } else if (config) {
66
+ serviceRef.current = new FiltersAndSortService({
67
+ storageKey: config.storageKey,
68
+ persistToStorage: config.persistToStorage,
69
+ initialState: config.initialState,
70
+ defaultFilters: config.defaultFilters,
71
+ skipStorageHydration: config.skipStorageHydration,
72
+ storageDebounceMs: config.storageDebounceMs,
73
+ storageKeyPrefix: config.storageKeyPrefix
74
+ });
75
+ ownsServiceRef.current = true;
76
+ if (debug) {
77
+ console.log(`useFiltersAndSort: created service with key "${config.storageKey}"`);
78
+ }
18
79
  }
19
- const subscription = service.state$.subscribe((newState) => {
20
- setState(newState);
80
+ }
81
+ const svc = serviceRef.current;
82
+ const defaultFiltersRef = useRef(config == null ? void 0 : config.defaultFilters);
83
+ const subscribe = useCallback(
84
+ (onStoreChange) => {
85
+ const subscription = svc.getState().subscribe(() => {
86
+ onStoreChange();
87
+ });
88
+ return () => subscription.unsubscribe();
89
+ },
90
+ [svc]
91
+ );
92
+ const getSnapshot = useCallback(() => svc.getCurrentState(), [svc]);
93
+ const getServerSnapshot = useCallback(() => svc.getDefaultState(), [svc]);
94
+ const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
95
+ useEffect(() => {
96
+ if (!debug) return;
97
+ const sub = svc.getState().subscribe((newState) => {
98
+ console.log(`useFiltersAndSort [${svc.getStorageKey()}]:`, newState);
21
99
  });
22
- return () => subscription.unsubscribe();
23
- }, [service]);
24
- const callbacks = useMemo(() => ({
25
- setFilter: (key, value) => {
26
- service.setFilter(key, value);
100
+ return () => sub.unsubscribe();
101
+ }, [svc, debug]);
102
+ useEffect(() => {
103
+ return () => {
104
+ if (ownsServiceRef.current && serviceRef.current) {
105
+ if (debug) {
106
+ console.log(`useFiltersAndSort: destroying owned service "${serviceRef.current.getStorageKey()}"`);
107
+ }
108
+ serviceRef.current.destroy();
109
+ serviceRef.current = null;
110
+ }
111
+ };
112
+ }, [debug]);
113
+ const setFilter = useCallback(
114
+ (key, value) => {
115
+ svc.setFilters({ [key]: value });
27
116
  },
28
- clearFilter: (key) => {
29
- service.clearFilter(key);
117
+ [svc]
118
+ );
119
+ const setFilters = useCallback(
120
+ (filters) => {
121
+ svc.setFilters(filters);
30
122
  },
31
- clearAllFilters: () => {
32
- service.clearAllFilters();
123
+ [svc]
124
+ );
125
+ const clearFilter = useCallback(
126
+ (key) => {
127
+ svc.removeFilter(key);
33
128
  },
34
- setSort: (field, direction) => {
35
- service.setSort(field, direction);
129
+ [svc]
130
+ );
131
+ const clearAllFilters = useCallback(() => {
132
+ svc.clearFilters();
133
+ }, [svc]);
134
+ const hasFilter = useCallback(
135
+ (key) => {
136
+ var _a;
137
+ const value = svc.getCurrentState().filters[key];
138
+ const defaultValue = (_a = defaultFiltersRef.current) == null ? void 0 : _a[key];
139
+ if (value === defaultValue) return false;
140
+ if (value === null || value === void 0 || value === "") return false;
141
+ if (Array.isArray(value) && value.length === 0) return false;
142
+ return true;
36
143
  },
37
- toggleSort: (field) => {
38
- service.toggleSort(field);
144
+ [svc]
145
+ );
146
+ const hasAnyFilters = useCallback(() => {
147
+ return countActiveFilters(svc.getCurrentState().filters, defaultFiltersRef.current) > 0;
148
+ }, [svc]);
149
+ const setSort = useCallback(
150
+ (field) => {
151
+ svc.setSort(field);
39
152
  },
40
- setPage: (page) => {
41
- service.setPage(page);
153
+ [svc]
154
+ );
155
+ const clearSort = useCallback(() => {
156
+ svc.setSort("");
157
+ }, [svc]);
158
+ const isSortedBy = useCallback(
159
+ (field) => {
160
+ var _a;
161
+ return ((_a = svc.getCurrentState().sort) == null ? void 0 : _a.fieldName) === field;
42
162
  },
43
- setPageSize: (size) => {
44
- service.setPageSize(size);
163
+ [svc]
164
+ );
165
+ const setPage = useCallback(
166
+ (page) => {
167
+ if (page < 0) {
168
+ if (debug) {
169
+ console.warn("useFiltersAndSort: setPage called with negative value, clamping to 0");
170
+ }
171
+ page = 0;
172
+ }
173
+ svc.setPagination(page);
45
174
  },
46
- nextPage: () => {
47
- service.nextPage();
175
+ [svc, debug]
176
+ );
177
+ const setPageSize = useCallback(
178
+ (size) => {
179
+ if (size < 1) {
180
+ if (debug) {
181
+ console.warn("useFiltersAndSort: setPageSize called with value < 1, clamping to 1");
182
+ }
183
+ size = 1;
184
+ }
185
+ const currentPage = svc.getCurrentState().pagination.page;
186
+ svc.setPagination(currentPage, size);
48
187
  },
49
- prevPage: () => {
50
- service.prevPage();
51
- }
52
- }), [service]);
53
- const memoizedReturn = useMemo(() => ({
54
- filters: state.filters,
55
- sortBy: state.sortBy,
56
- sortDirection: state.sortDirection,
57
- page: state.page,
58
- pageSize: state.pageSize,
59
- ...callbacks
60
- }), [state, callbacks]);
61
- return memoizedReturn;
188
+ [svc, debug]
189
+ );
190
+ const nextPage = useCallback(() => {
191
+ svc.setPagination(svc.getCurrentState().pagination.page + 1);
192
+ }, [svc]);
193
+ const prevPage = useCallback(() => {
194
+ const newPage = Math.max(0, svc.getCurrentState().pagination.page - 1);
195
+ svc.setPagination(newPage);
196
+ }, [svc]);
197
+ const firstPage = useCallback(() => {
198
+ svc.setPagination(0);
199
+ }, [svc]);
200
+ const setQuery = useCallback(
201
+ (query) => {
202
+ svc.setQuery(query);
203
+ },
204
+ [svc]
205
+ );
206
+ const clearQuery = useCallback(() => {
207
+ svc.setQuery("");
208
+ }, [svc]);
209
+ const reset = useCallback(() => {
210
+ svc.reset();
211
+ }, [svc]);
212
+ const clearStorageAndReset = useCallback(() => {
213
+ svc.clearStorage();
214
+ svc.reset();
215
+ }, [svc]);
216
+ const getService = useCallback(() => svc, [svc]);
217
+ const activeFilterCount = useMemo(
218
+ () => countActiveFilters(state.filters, defaultFiltersRef.current),
219
+ [state.filters]
220
+ );
221
+ const isFirstPage = state.pagination.page === 0;
222
+ const offset = state.pagination.page * state.pagination.limit;
223
+ return useMemo(
224
+ () => {
225
+ var _a, _b;
226
+ return {
227
+ // State
228
+ filters: state.filters,
229
+ sortBy: ((_a = state.sort) == null ? void 0 : _a.fieldName) ?? null,
230
+ sortDirection: ((_b = state.sort) == null ? void 0 : _b.direction) ?? "asc",
231
+ page: state.pagination.page,
232
+ pageSize: state.pagination.limit,
233
+ query: state.query,
234
+ state,
235
+ // Filter actions
236
+ setFilter,
237
+ setFilters,
238
+ clearFilter,
239
+ clearAllFilters,
240
+ hasFilter,
241
+ hasAnyFilters,
242
+ activeFilterCount,
243
+ // Sort actions
244
+ setSort,
245
+ toggleSort: setSort,
246
+ clearSort,
247
+ isSortedBy,
248
+ // Pagination actions
249
+ setPage,
250
+ setPageSize,
251
+ nextPage,
252
+ prevPage,
253
+ firstPage,
254
+ isFirstPage,
255
+ offset,
256
+ // Query actions
257
+ setQuery,
258
+ clearQuery,
259
+ // General actions
260
+ reset,
261
+ clearStorageAndReset,
262
+ storageKey: svc.getStorageKey(),
263
+ // Service access
264
+ getService
265
+ };
266
+ },
267
+ [
268
+ state,
269
+ setFilter,
270
+ setFilters,
271
+ clearFilter,
272
+ clearAllFilters,
273
+ hasFilter,
274
+ hasAnyFilters,
275
+ activeFilterCount,
276
+ setSort,
277
+ clearSort,
278
+ isSortedBy,
279
+ setPage,
280
+ setPageSize,
281
+ nextPage,
282
+ prevPage,
283
+ firstPage,
284
+ isFirstPage,
285
+ offset,
286
+ setQuery,
287
+ clearQuery,
288
+ reset,
289
+ clearStorageAndReset,
290
+ svc,
291
+ getService
292
+ ]
293
+ );
62
294
  }
63
295
  function useFormBuilder(options) {
64
296
  const { builder } = options;
65
297
  if (!builder) {
66
298
  throw new Error("useFormBuilder: builder is required");
67
299
  }
68
- const [state, setState] = useState(() => builder.getState());
300
+ const [state, setState] = useState(() => builder.currentState);
69
301
  const [isSubmitting, setIsSubmitting] = useState(false);
70
302
  useEffect(() => {
71
- if (!builder || !builder.stateChanged$) {
303
+ if (!builder || !builder.state$) {
72
304
  return;
73
305
  }
74
- const subscription = builder.stateChanged$.subscribe((newState) => {
306
+ const subscription = builder.state$.subscribe((newState) => {
75
307
  setState(newState);
76
308
  });
77
309
  return () => subscription.unsubscribe();
@@ -80,8 +312,10 @@ function useFormBuilder(options) {
80
312
  values: state.values,
81
313
  errors: state.errors,
82
314
  touched: state.touched,
83
- currentStep: state.currentStep,
84
- totalSteps: state.totalSteps,
315
+ currentStep: 0,
316
+ // Multi-step requires MultiStepFormBuilder
317
+ totalSteps: 1,
318
+ // Multi-step requires MultiStepFormBuilder
85
319
  isSubmitting,
86
320
  isValid: Object.keys(state.errors).length === 0,
87
321
  setFieldValue: async (field, value) => {
@@ -96,9 +330,9 @@ function useFormBuilder(options) {
96
330
  submit: async () => {
97
331
  setIsSubmitting(true);
98
332
  try {
99
- const result = await builder.submit();
333
+ await builder.submit();
100
334
  setIsSubmitting(false);
101
- return result;
335
+ return null;
102
336
  } catch (error) {
103
337
  setIsSubmitting(false);
104
338
  throw error;
@@ -108,14 +342,10 @@ function useFormBuilder(options) {
108
342
  builder.reset();
109
343
  },
110
344
  nextStep: () => {
111
- if (builder.nextStep) {
112
- builder.nextStep();
113
- }
345
+ console.warn("nextStep() is only available on MultiStepFormBuilder");
114
346
  },
115
347
  prevStep: () => {
116
- if (builder.prevStep) {
117
- builder.prevStep();
118
- }
348
+ console.warn("prevStep() is only available on MultiStepFormBuilder");
119
349
  }
120
350
  }), [builder, state, isSubmitting]);
121
351
  return memoizedReturn;
@@ -358,91 +588,10 @@ function useRichContent(options = {}) {
358
588
  setSelection
359
589
  };
360
590
  }
361
- function useTableService(options) {
362
- const { config, data } = options;
363
- const builder = useMemo(() => {
364
- const tb = new TableBuilder(config);
365
- tb.setData(data);
366
- return tb;
367
- }, [config]);
368
- const [state, setState] = useState(() => builder.getState());
369
- const [selectedRows, setSelectedRows] = useState([]);
370
- const [allSelected, setAllSelected] = useState(false);
371
- useEffect(() => {
372
- if (!builder || !builder.stateChanged$) {
373
- return;
374
- }
375
- const subscription = builder.stateChanged$.subscribe((newState) => {
376
- setState(newState);
377
- });
378
- return () => subscription.unsubscribe();
379
- }, [builder]);
380
- useEffect(() => {
381
- builder.setData(data);
382
- }, [builder, data]);
383
- const memoizedMethods = useMemo(() => ({
384
- toggleRowSelection: (rowId) => {
385
- setSelectedRows(
386
- (prev) => prev.includes(rowId) ? prev.filter((id) => id !== rowId) : [...prev, rowId]
387
- );
388
- },
389
- toggleAllSelection: () => {
390
- if (allSelected) {
391
- setSelectedRows([]);
392
- setAllSelected(false);
393
- } else {
394
- setSelectedRows(state.rows.map((row) => row.id || String(row)));
395
- setAllSelected(true);
396
- }
397
- },
398
- clearSelection: () => {
399
- setSelectedRows([]);
400
- setAllSelected(false);
401
- },
402
- setSort: (field, direction) => {
403
- builder.setSort(field, direction);
404
- },
405
- toggleSort: (field) => {
406
- builder.toggleSort(field);
407
- },
408
- setFilter: (field, value) => {
409
- builder.setFilter(field, value);
410
- },
411
- clearFilter: (field) => {
412
- builder.clearFilter(field);
413
- },
414
- clearAllFilters: () => {
415
- builder.clearAllFilters();
416
- },
417
- setPage: (page) => {
418
- builder.setPage(page);
419
- },
420
- setPageSize: (size) => {
421
- builder.setPageSize(size);
422
- },
423
- nextPage: () => {
424
- builder.nextPage();
425
- },
426
- prevPage: () => {
427
- builder.prevPage();
428
- }
429
- }), [builder, allSelected, state.rows]);
430
- const memoizedReturn = useMemo(() => ({
431
- rows: state.rows,
432
- selectedRows,
433
- allSelected,
434
- currentPage: state.currentPage,
435
- pageSize: state.pageSize,
436
- totalPages: state.totalPages,
437
- totalItems: state.totalItems,
438
- hasNextPage: state.hasNextPage,
439
- hasPrevPage: state.hasPrevPage,
440
- sortBy: state.sortBy,
441
- sortDirection: state.sortDirection,
442
- filters: state.filters,
443
- ...memoizedMethods
444
- }), [state, selectedRows, allSelected, memoizedMethods]);
445
- return memoizedReturn;
591
+ function useTableService() {
592
+ throw new Error(
593
+ "useTableService is deprecated. TableBuilder is a configuration builder, not a stateful service. Use FiltersAndSortService with useFiltersAndSort hook for reactive state management. See documentation: https://github.com/CodellaSoftware/codella-utils"
594
+ );
446
595
  }
447
596
  const TabsContext = createContext(void 0);
448
597
  function TabsProvider({ config, children }) {