ding-react-admin 2.0.0 → 2.0.5

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 (169) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +254 -195
  3. package/dist/index.css +1 -0
  4. package/dist/index.js +1773 -1170
  5. package/dist/src/components/NavMenuLabel.d.ts +8 -0
  6. package/dist/src/components/NavMenuLabel.d.ts.map +1 -0
  7. package/dist/src/components/NavMenuSearch.d.ts +9 -0
  8. package/dist/src/components/NavMenuSearch.d.ts.map +1 -0
  9. package/dist/src/components/ScrollableArea.d.ts +10 -0
  10. package/dist/src/components/ScrollableArea.d.ts.map +1 -0
  11. package/dist/src/components/index.d.ts +3 -0
  12. package/dist/src/components/index.d.ts.map +1 -1
  13. package/dist/src/context/AuthProvider.d.ts +2 -0
  14. package/dist/src/context/AuthProvider.d.ts.map +1 -1
  15. package/dist/src/crud/FormGlobalErrorsAlert.d.ts +5 -0
  16. package/dist/src/crud/FormGlobalErrorsAlert.d.ts.map +1 -0
  17. package/dist/src/crud/ResourceForm.d.ts +1 -4
  18. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  19. package/dist/src/crud/ResourceFormModal.d.ts +7 -1
  20. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
  21. package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
  22. package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
  23. package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
  24. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  25. package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
  26. package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
  27. package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
  28. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  29. package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
  30. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
  31. package/dist/src/crud/index.d.ts +5 -0
  32. package/dist/src/crud/index.d.ts.map +1 -1
  33. package/dist/src/crud/types.d.ts +12 -0
  34. package/dist/src/crud/types.d.ts.map +1 -1
  35. package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
  36. package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
  37. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
  38. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
  39. package/dist/src/crud/utils/formErrors.d.ts +18 -7
  40. package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
  41. package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
  42. package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
  43. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +1 -0
  44. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -1
  45. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
  46. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
  47. package/dist/src/crud/utils/toFormData.d.ts +1 -1
  48. package/dist/src/crud/utils/toFormData.d.ts.map +1 -1
  49. package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
  50. package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
  51. package/dist/src/crud/utils/useChoices.d.ts +27 -1
  52. package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
  53. package/dist/src/crud/utils/useFormRecord.d.ts +9 -7
  54. package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -1
  55. package/dist/src/data/parseFormErrorHelpers.d.ts +6 -27
  56. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
  57. package/dist/src/index.d.ts +2 -2
  58. package/dist/src/index.d.ts.map +1 -1
  59. package/dist/src/layouts/AdminLayout.d.ts +1 -1
  60. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  61. package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
  62. package/dist/src/layouts/navFilter.d.ts +5 -0
  63. package/dist/src/layouts/navFilter.d.ts.map +1 -0
  64. package/dist/src/layouts/navMenuItems.d.ts +6 -0
  65. package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
  66. package/dist/src/types.d.ts +12 -0
  67. package/dist/src/types.d.ts.map +1 -1
  68. package/package.json +85 -59
  69. package/src/app/AdminApp.tsx +34 -34
  70. package/src/components/AuthAlternateLink.tsx +23 -23
  71. package/src/components/DensitySwitch.tsx +31 -31
  72. package/src/components/NavMenuLabel.tsx +26 -0
  73. package/src/components/NavMenuSearch.tsx +76 -0
  74. package/src/components/ScrollableArea.tsx +46 -0
  75. package/src/components/ThemeSwitch.tsx +28 -28
  76. package/src/components/ThemeToolbar.tsx +34 -34
  77. package/src/components/index.ts +7 -4
  78. package/src/components/navMenu.css +21 -0
  79. package/src/components/scrollable.css +28 -0
  80. package/src/context/AppThemeProvider.tsx +128 -128
  81. package/src/context/AuthProvider.tsx +89 -77
  82. package/src/context/DataProvider.tsx +30 -30
  83. package/src/context/PermissionsProvider.tsx +57 -57
  84. package/src/crud/FilterBar.tsx +36 -36
  85. package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
  86. package/src/crud/FormSteps.tsx +135 -135
  87. package/src/crud/FormTabs.tsx +126 -126
  88. package/src/crud/InlineFormSet.tsx +162 -162
  89. package/src/crud/ListActionsBar.tsx +106 -106
  90. package/src/crud/ResourceForm.tsx +83 -160
  91. package/src/crud/ResourceFormModal.tsx +75 -133
  92. package/src/crud/ResourceRecordForm.tsx +166 -0
  93. package/src/crud/columns/BooleanColumn.tsx +32 -32
  94. package/src/crud/columns/CustomColumn.tsx +33 -33
  95. package/src/crud/columns/DateColumn.tsx +28 -28
  96. package/src/crud/columns/ImageColumn.tsx +58 -58
  97. package/src/crud/columns/NumberColumn.tsx +31 -31
  98. package/src/crud/columns/ReferenceColumn.tsx +85 -85
  99. package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
  100. package/src/crud/columns/TextColumn.tsx +38 -38
  101. package/src/crud/context/FilterContext.tsx +71 -71
  102. package/src/crud/context/FormContext.tsx +39 -39
  103. package/src/crud/context/FormSectionContext.tsx +22 -22
  104. package/src/crud/context/InlineFieldsRegistry.tsx +61 -61
  105. package/src/crud/context/ListContext.tsx +93 -93
  106. package/src/crud/fields/BooleanField.tsx +38 -38
  107. package/src/crud/fields/DateField.tsx +53 -53
  108. package/src/crud/fields/FileField.tsx +123 -123
  109. package/src/crud/fields/ImageField.tsx +120 -120
  110. package/src/crud/fields/NumberField.tsx +52 -52
  111. package/src/crud/fields/PasswordField.tsx +99 -99
  112. package/src/crud/fields/ReferenceField.tsx +237 -96
  113. package/src/crud/fields/ReferenceInputActions.tsx +106 -0
  114. package/src/crud/fields/ReferenceManyField.tsx +200 -76
  115. package/src/crud/fields/SelectField.tsx +48 -48
  116. package/src/crud/fields/TextField.tsx +46 -46
  117. package/src/crud/fields/uploadFieldUtils.ts +21 -21
  118. package/src/crud/fields/useUploadPreviewUrl.ts +20 -20
  119. package/src/crud/filters/BooleanFilter.tsx +38 -38
  120. package/src/crud/filters/DateFilter.tsx +35 -35
  121. package/src/crud/filters/NumberFilter.tsx +33 -33
  122. package/src/crud/filters/ReferenceFilter.tsx +192 -173
  123. package/src/crud/filters/SelectFilter.tsx +44 -44
  124. package/src/crud/filters/TextFilter.tsx +36 -36
  125. package/src/crud/index.ts +99 -94
  126. package/src/crud/types.ts +146 -134
  127. package/src/crud/utils/buildFormPayload.ts +24 -24
  128. package/src/crud/utils/buildInlineRowsPayload.ts +37 -37
  129. package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -29
  130. package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
  131. package/src/crud/utils/choiceSelectionUtils.ts +116 -0
  132. package/src/crud/utils/formErrors.test.ts +168 -0
  133. package/src/crud/utils/formErrors.ts +138 -42
  134. package/src/crud/utils/formSectionErrors.ts +85 -85
  135. package/src/crud/utils/getFormValue.ts +13 -13
  136. package/src/crud/utils/hasUploadValues.ts +9 -9
  137. package/src/crud/utils/nestedFieldPath.ts +8 -8
  138. package/src/crud/utils/prepareFormSubmitBody.ts +22 -17
  139. package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
  140. package/src/crud/utils/setFormValue.ts +18 -18
  141. package/src/crud/utils/sortQueryParam.ts +37 -37
  142. package/src/crud/utils/toFormData.ts +79 -81
  143. package/src/crud/utils/uploadReferenceUtils.ts +44 -0
  144. package/src/crud/utils/useAbortableEffect.ts +17 -17
  145. package/src/crud/utils/useChoices.ts +330 -182
  146. package/src/crud/utils/useFormRecord.ts +179 -156
  147. package/src/crud/utils/useListQueryState.ts +191 -191
  148. package/src/data/abortError.test.ts +19 -19
  149. package/src/data/abortError.ts +10 -10
  150. package/src/data/createMemoryResourceHandlers.ts +78 -78
  151. package/src/data/createRestResourceHandlers.ts +63 -63
  152. package/src/data/dataUtils.test.ts +228 -220
  153. package/src/data/inMemoryList.ts +97 -97
  154. package/src/data/parseFormErrorHelpers.test.ts +201 -138
  155. package/src/data/parseFormErrorHelpers.ts +376 -311
  156. package/src/data/sortHelpers.ts +48 -48
  157. package/src/index.ts +211 -207
  158. package/src/layouts/AdminLayout.tsx +530 -382
  159. package/src/layouts/AuthPageLayout.tsx +87 -81
  160. package/src/layouts/navFilter.ts +49 -0
  161. package/src/layouts/navMenuItems.tsx +44 -0
  162. package/src/pages/LoginPage.tsx +80 -80
  163. package/src/pages/PlaceholderPage.tsx +9 -9
  164. package/src/permissions/resourcePermissions.ts +47 -47
  165. package/src/router/createAdminRouter.tsx +64 -64
  166. package/src/router/guards.tsx +63 -63
  167. package/src/router/routeAccess.ts +89 -89
  168. package/src/types.ts +10 -0
  169. package/src/vite-env.d.ts +1 -1
@@ -1,191 +1,191 @@
1
- import { useCallback, useMemo } from "react";
2
- import { useSearchParams } from "react-router-dom";
3
- import type { SortSpec } from "../../data/dataProviderTypes";
4
- import { LIST_QUERY_RESERVED } from "../types";
5
- import { parseSortParam, serializeSort } from "./sortQueryParam";
6
-
7
- const DEFAULT_PAGE = 1;
8
- const DEFAULT_PER_PAGE = 10;
9
-
10
- function parseFilterValue(raw: string): unknown {
11
- if (raw.includes(",")) {
12
- const parts = raw.split(",").map((p) => p.trim());
13
- const nums = parts.map(Number);
14
- if (nums.every((n) => Number.isFinite(n))) return nums;
15
- return parts;
16
- }
17
- const n = Number(raw);
18
- if (raw !== "" && Number.isFinite(n) && String(n) === raw) return n;
19
- if (raw === "true") return true;
20
- if (raw === "false") return false;
21
- return raw;
22
- }
23
-
24
- function serializeFilterValue(value: unknown): string | null {
25
- if (value === undefined || value === null || value === "") return null;
26
- if (Array.isArray(value)) {
27
- if (value.length === 0) return null;
28
- return value.map(String).join(",");
29
- }
30
- return String(value);
31
- }
32
-
33
- export type ListQueryState = {
34
- page: number;
35
- perPage: number;
36
- sort: SortSpec[];
37
- filter: Record<string, unknown>;
38
- createModal: boolean;
39
- editId: string | null;
40
- };
41
-
42
- export type ListQueryActions = {
43
- setPage: (page: number) => void;
44
- setPerPage: (perPage: number) => void;
45
- setSort: (sort: SortSpec[]) => void;
46
- toggleSort: (field: string) => void;
47
- setFilter: (source: string, value: unknown) => void;
48
- setFilters: (filter: Record<string, unknown>) => void;
49
- openCreateModal: () => void;
50
- openEditModal: (id: string | number) => void;
51
- closeModal: () => void;
52
- };
53
-
54
- export function useListQueryState(
55
- staticFilter?: Record<string, unknown>,
56
- ): [ListQueryState, ListQueryActions] {
57
- const [searchParams, setSearchParams] = useSearchParams();
58
-
59
- const state = useMemo((): ListQueryState => {
60
- const pageRaw = searchParams.get("page");
61
- const perPageRaw = searchParams.get("perPage");
62
- const page = pageRaw ? Math.max(1, Number(pageRaw) || DEFAULT_PAGE) : DEFAULT_PAGE;
63
- const perPage = perPageRaw
64
- ? Math.max(1, Number(perPageRaw) || DEFAULT_PER_PAGE)
65
- : DEFAULT_PER_PAGE;
66
-
67
- const sortEntries = searchParams.getAll("sort");
68
- const sort =
69
- sortEntries.length > 0
70
- ? sortEntries.flatMap((s) => parseSortParam(s))
71
- : parseSortParam(searchParams.get("sort"));
72
-
73
- const filter: Record<string, unknown> = { ...staticFilter };
74
- searchParams.forEach((value, key) => {
75
- if (LIST_QUERY_RESERVED.has(key)) return;
76
- const existing = filter[key];
77
- if (existing !== undefined) {
78
- const arr = Array.isArray(existing) ? existing : [existing];
79
- filter[key] = [...arr, parseFilterValue(value)];
80
- } else if (searchParams.getAll(key).length > 1) {
81
- filter[key] = searchParams.getAll(key).map(parseFilterValue);
82
- } else {
83
- filter[key] = parseFilterValue(value);
84
- }
85
- });
86
-
87
- const createModal = searchParams.has("create");
88
- const editId = searchParams.get("edit");
89
-
90
- return { page, perPage, sort, filter, createModal, editId };
91
- }, [searchParams, staticFilter]);
92
-
93
- const updateParams = useCallback(
94
- (mutate: (params: URLSearchParams) => void) => {
95
- setSearchParams(
96
- (prev) => {
97
- const next = new URLSearchParams(prev);
98
- mutate(next);
99
- return next;
100
- },
101
- { replace: true },
102
- );
103
- },
104
- [setSearchParams],
105
- );
106
-
107
- const actions: ListQueryActions = useMemo(
108
- () => ({
109
- setPage: (page) => {
110
- updateParams((p) => {
111
- if (page <= 1) p.delete("page");
112
- else p.set("page", String(page));
113
- });
114
- },
115
- setPerPage: (perPage) => {
116
- updateParams((p) => {
117
- if (perPage === DEFAULT_PER_PAGE) p.delete("perPage");
118
- else p.set("perPage", String(perPage));
119
- p.delete("page");
120
- });
121
- },
122
- setSort: (sort) => {
123
- updateParams((p) => {
124
- p.delete("sort");
125
- const serialized = serializeSort(sort);
126
- if (serialized) p.set("sort", serialized);
127
- });
128
- },
129
- toggleSort: (field) => {
130
- updateParams((p) => {
131
- const current = p.getAll("sort").flatMap((s) => parseSortParam(s));
132
- const idx = current.findIndex((s) => s.field === field);
133
- let next: SortSpec[];
134
- if (idx < 0) {
135
- next = [...current, { field, order: "ASC" }];
136
- } else if (current[idx].order === "ASC") {
137
- next = current.map((s, i) =>
138
- i === idx ? { ...s, order: "DESC" as const } : s,
139
- );
140
- } else {
141
- next = current.filter((_, i) => i !== idx);
142
- }
143
- p.delete("sort");
144
- const serialized = serializeSort(next);
145
- if (serialized) p.set("sort", serialized);
146
- });
147
- },
148
- setFilter: (source, value) => {
149
- updateParams((p) => {
150
- p.delete(source);
151
- const serialized = serializeFilterValue(value);
152
- if (serialized != null) p.set(source, serialized);
153
- p.delete("page");
154
- });
155
- },
156
- setFilters: (filter) => {
157
- updateParams((p) => {
158
- for (const key of [...p.keys()]) {
159
- if (!LIST_QUERY_RESERVED.has(key)) p.delete(key);
160
- }
161
- for (const [key, value] of Object.entries(filter)) {
162
- const serialized = serializeFilterValue(value);
163
- if (serialized != null) p.set(key, serialized);
164
- }
165
- p.delete("page");
166
- });
167
- },
168
- openCreateModal: () => {
169
- updateParams((p) => {
170
- p.set("create", "1");
171
- p.delete("edit");
172
- });
173
- },
174
- openEditModal: (id) => {
175
- updateParams((p) => {
176
- p.set("edit", String(id));
177
- p.delete("create");
178
- });
179
- },
180
- closeModal: () => {
181
- updateParams((p) => {
182
- p.delete("create");
183
- p.delete("edit");
184
- });
185
- },
186
- }),
187
- [updateParams],
188
- );
189
-
190
- return [state, actions];
191
- }
1
+ import { useCallback, useMemo } from "react";
2
+ import { useSearchParams } from "react-router-dom";
3
+ import type { SortSpec } from "../../data/dataProviderTypes";
4
+ import { LIST_QUERY_RESERVED } from "../types";
5
+ import { parseSortParam, serializeSort } from "./sortQueryParam";
6
+
7
+ const DEFAULT_PAGE = 1;
8
+ const DEFAULT_PER_PAGE = 10;
9
+
10
+ function parseFilterValue(raw: string): unknown {
11
+ if (raw.includes(",")) {
12
+ const parts = raw.split(",").map((p) => p.trim());
13
+ const nums = parts.map(Number);
14
+ if (nums.every((n) => Number.isFinite(n))) return nums;
15
+ return parts;
16
+ }
17
+ const n = Number(raw);
18
+ if (raw !== "" && Number.isFinite(n) && String(n) === raw) return n;
19
+ if (raw === "true") return true;
20
+ if (raw === "false") return false;
21
+ return raw;
22
+ }
23
+
24
+ function serializeFilterValue(value: unknown): string | null {
25
+ if (value === undefined || value === null || value === "") return null;
26
+ if (Array.isArray(value)) {
27
+ if (value.length === 0) return null;
28
+ return value.map(String).join(",");
29
+ }
30
+ return String(value);
31
+ }
32
+
33
+ export type ListQueryState = {
34
+ page: number;
35
+ perPage: number;
36
+ sort: SortSpec[];
37
+ filter: Record<string, unknown>;
38
+ createModal: boolean;
39
+ editId: string | null;
40
+ };
41
+
42
+ export type ListQueryActions = {
43
+ setPage: (page: number) => void;
44
+ setPerPage: (perPage: number) => void;
45
+ setSort: (sort: SortSpec[]) => void;
46
+ toggleSort: (field: string) => void;
47
+ setFilter: (source: string, value: unknown) => void;
48
+ setFilters: (filter: Record<string, unknown>) => void;
49
+ openCreateModal: () => void;
50
+ openEditModal: (id: string | number) => void;
51
+ closeModal: () => void;
52
+ };
53
+
54
+ export function useListQueryState(
55
+ staticFilter?: Record<string, unknown>,
56
+ ): [ListQueryState, ListQueryActions] {
57
+ const [searchParams, setSearchParams] = useSearchParams();
58
+
59
+ const state = useMemo((): ListQueryState => {
60
+ const pageRaw = searchParams.get("page");
61
+ const perPageRaw = searchParams.get("perPage");
62
+ const page = pageRaw ? Math.max(1, Number(pageRaw) || DEFAULT_PAGE) : DEFAULT_PAGE;
63
+ const perPage = perPageRaw
64
+ ? Math.max(1, Number(perPageRaw) || DEFAULT_PER_PAGE)
65
+ : DEFAULT_PER_PAGE;
66
+
67
+ const sortEntries = searchParams.getAll("sort");
68
+ const sort =
69
+ sortEntries.length > 0
70
+ ? sortEntries.flatMap((s) => parseSortParam(s))
71
+ : parseSortParam(searchParams.get("sort"));
72
+
73
+ const filter: Record<string, unknown> = { ...staticFilter };
74
+ searchParams.forEach((value, key) => {
75
+ if (LIST_QUERY_RESERVED.has(key)) return;
76
+ const existing = filter[key];
77
+ if (existing !== undefined) {
78
+ const arr = Array.isArray(existing) ? existing : [existing];
79
+ filter[key] = [...arr, parseFilterValue(value)];
80
+ } else if (searchParams.getAll(key).length > 1) {
81
+ filter[key] = searchParams.getAll(key).map(parseFilterValue);
82
+ } else {
83
+ filter[key] = parseFilterValue(value);
84
+ }
85
+ });
86
+
87
+ const createModal = searchParams.has("create");
88
+ const editId = searchParams.get("edit");
89
+
90
+ return { page, perPage, sort, filter, createModal, editId };
91
+ }, [searchParams, staticFilter]);
92
+
93
+ const updateParams = useCallback(
94
+ (mutate: (params: URLSearchParams) => void) => {
95
+ setSearchParams(
96
+ (prev) => {
97
+ const next = new URLSearchParams(prev);
98
+ mutate(next);
99
+ return next;
100
+ },
101
+ { replace: true },
102
+ );
103
+ },
104
+ [setSearchParams],
105
+ );
106
+
107
+ const actions: ListQueryActions = useMemo(
108
+ () => ({
109
+ setPage: (page) => {
110
+ updateParams((p) => {
111
+ if (page <= 1) p.delete("page");
112
+ else p.set("page", String(page));
113
+ });
114
+ },
115
+ setPerPage: (perPage) => {
116
+ updateParams((p) => {
117
+ if (perPage === DEFAULT_PER_PAGE) p.delete("perPage");
118
+ else p.set("perPage", String(perPage));
119
+ p.delete("page");
120
+ });
121
+ },
122
+ setSort: (sort) => {
123
+ updateParams((p) => {
124
+ p.delete("sort");
125
+ const serialized = serializeSort(sort);
126
+ if (serialized) p.set("sort", serialized);
127
+ });
128
+ },
129
+ toggleSort: (field) => {
130
+ updateParams((p) => {
131
+ const current = p.getAll("sort").flatMap((s) => parseSortParam(s));
132
+ const idx = current.findIndex((s) => s.field === field);
133
+ let next: SortSpec[];
134
+ if (idx < 0) {
135
+ next = [...current, { field, order: "ASC" }];
136
+ } else if (current[idx].order === "ASC") {
137
+ next = current.map((s, i) =>
138
+ i === idx ? { ...s, order: "DESC" as const } : s,
139
+ );
140
+ } else {
141
+ next = current.filter((_, i) => i !== idx);
142
+ }
143
+ p.delete("sort");
144
+ const serialized = serializeSort(next);
145
+ if (serialized) p.set("sort", serialized);
146
+ });
147
+ },
148
+ setFilter: (source, value) => {
149
+ updateParams((p) => {
150
+ p.delete(source);
151
+ const serialized = serializeFilterValue(value);
152
+ if (serialized != null) p.set(source, serialized);
153
+ p.delete("page");
154
+ });
155
+ },
156
+ setFilters: (filter) => {
157
+ updateParams((p) => {
158
+ for (const key of [...p.keys()]) {
159
+ if (!LIST_QUERY_RESERVED.has(key)) p.delete(key);
160
+ }
161
+ for (const [key, value] of Object.entries(filter)) {
162
+ const serialized = serializeFilterValue(value);
163
+ if (serialized != null) p.set(key, serialized);
164
+ }
165
+ p.delete("page");
166
+ });
167
+ },
168
+ openCreateModal: () => {
169
+ updateParams((p) => {
170
+ p.set("create", "1");
171
+ p.delete("edit");
172
+ });
173
+ },
174
+ openEditModal: (id) => {
175
+ updateParams((p) => {
176
+ p.set("edit", String(id));
177
+ p.delete("create");
178
+ });
179
+ },
180
+ closeModal: () => {
181
+ updateParams((p) => {
182
+ p.delete("create");
183
+ p.delete("edit");
184
+ });
185
+ },
186
+ }),
187
+ [updateParams],
188
+ );
189
+
190
+ return [state, actions];
191
+ }
@@ -1,19 +1,19 @@
1
- import { describe, expect, it } from "vitest";
2
- import { isAbortError } from "./abortError";
3
-
4
- describe("isAbortError", () => {
5
- it("detects DOM AbortError", () => {
6
- expect(isAbortError({ name: "AbortError" })).toBe(true);
7
- });
8
-
9
- it("detects axios CanceledError", () => {
10
- expect(isAbortError({ name: "CanceledError", code: "ERR_CANCELED" })).toBe(
11
- true,
12
- );
13
- });
14
-
15
- it("returns false for ordinary errors", () => {
16
- expect(isAbortError(new Error("Network Error"))).toBe(false);
17
- expect(isAbortError(null)).toBe(false);
18
- });
19
- });
1
+ import { describe, expect, it } from "vitest";
2
+ import { isAbortError } from "./abortError";
3
+
4
+ describe("isAbortError", () => {
5
+ it("detects DOM AbortError", () => {
6
+ expect(isAbortError({ name: "AbortError" })).toBe(true);
7
+ });
8
+
9
+ it("detects axios CanceledError", () => {
10
+ expect(isAbortError({ name: "CanceledError", code: "ERR_CANCELED" })).toBe(
11
+ true,
12
+ );
13
+ });
14
+
15
+ it("returns false for ordinary errors", () => {
16
+ expect(isAbortError(new Error("Network Error"))).toBe(false);
17
+ expect(isAbortError(null)).toBe(false);
18
+ });
19
+ });
@@ -1,10 +1,10 @@
1
- /** True when a fetch/axios call was aborted via `AbortSignal` (not a real failure). */
2
- export function isAbortError(error: unknown): boolean {
3
- if (error == null || typeof error !== "object") return false;
4
- const e = error as { name?: string; code?: string };
5
- return (
6
- e.name === "AbortError" ||
7
- e.name === "CanceledError" ||
8
- e.code === "ERR_CANCELED"
9
- );
10
- }
1
+ /** True when a fetch/axios call was aborted via `AbortSignal` (not a real failure). */
2
+ export function isAbortError(error: unknown): boolean {
3
+ if (error == null || typeof error !== "object") return false;
4
+ const e = error as { name?: string; code?: string };
5
+ return (
6
+ e.name === "AbortError" ||
7
+ e.name === "CanceledError" ||
8
+ e.code === "ERR_CANCELED"
9
+ );
10
+ }
@@ -1,78 +1,78 @@
1
- import { applyInMemoryListParams, getById, rowIdMatches } from "./inMemoryList";
2
- import type {
3
- CreateResult,
4
- DeleteResult,
5
- GetListParams,
6
- GetListResult,
7
- GetOneResult,
8
- UpdateResult,
9
- } from "./dataProviderTypes";
10
- import type { ResourceHandlers } from "./resourceHandlers";
11
-
12
- type Row = Record<string, unknown>;
13
-
14
- export type MemoryResourceHandlersConfig<
15
- T extends Record<string, unknown> & { id: string | number },
16
- > = {
17
- getRows: () => T[];
18
- nextId: () => string | number;
19
- /** Defaults to `{ ...data, id }` from form field sources. */
20
- mapCreate?: (data: Row, id: string | number) => T;
21
- /** Defaults to spread patch over current row. */
22
- applyUpdate?: (current: T, patch: Row) => T;
23
- scopeList?: (rows: T[], params: GetListParams) => T[];
24
- afterDelete?: (removed: T) => void;
25
- };
26
-
27
- /**
28
- * Generic in-memory CRUD for demos and tests. Override hooks for entity-specific rules.
29
- */
30
- export function createMemoryResourceHandlers<
31
- T extends Record<string, unknown> & { id: string | number },
32
- >(config: MemoryResourceHandlersConfig<T>): ResourceHandlers<T> {
33
- const asRows = (xs: T[]) => xs as unknown as Row[];
34
-
35
- const mapCreate =
36
- config.mapCreate ??
37
- ((data: Row, id: string | number) => ({ ...data, id }) as T);
38
-
39
- const applyUpdate =
40
- config.applyUpdate ??
41
- ((current: T, patch: Row) => ({ ...current, ...patch, id: current.id }) as T);
42
-
43
- return {
44
- async getList(params): Promise<GetListResult<T>> {
45
- const base = config.scopeList
46
- ? config.scopeList(config.getRows(), params)
47
- : config.getRows();
48
- return applyInMemoryListParams(asRows(base), params) as GetListResult<T>;
49
- },
50
-
51
- async getOne(id, _params?): Promise<GetOneResult<T>> {
52
- return { data: getById(config.getRows(), id) as T };
53
- },
54
-
55
- async create(data): Promise<CreateResult<T>> {
56
- const row = mapCreate(data as Row, config.nextId());
57
- config.getRows().push(row);
58
- return { data: row };
59
- },
60
-
61
- async update({ id, data }): Promise<UpdateResult<T>> {
62
- const current = getById(config.getRows(), id);
63
- const patch = data as Row;
64
- const next = applyUpdate(current, patch);
65
- Object.assign(current, next);
66
- return { data: current };
67
- },
68
-
69
- async delete(id): Promise<DeleteResult<T>> {
70
- const rows = config.getRows();
71
- const idx = rows.findIndex((r) => rowIdMatches(r.id, id));
72
- if (idx < 0) return { data: null };
73
- const [removed] = rows.splice(idx, 1);
74
- config.afterDelete?.(removed);
75
- return { data: removed };
76
- },
77
- };
78
- }
1
+ import { applyInMemoryListParams, getById, rowIdMatches } from "./inMemoryList";
2
+ import type {
3
+ CreateResult,
4
+ DeleteResult,
5
+ GetListParams,
6
+ GetListResult,
7
+ GetOneResult,
8
+ UpdateResult,
9
+ } from "./dataProviderTypes";
10
+ import type { ResourceHandlers } from "./resourceHandlers";
11
+
12
+ type Row = Record<string, unknown>;
13
+
14
+ export type MemoryResourceHandlersConfig<
15
+ T extends Record<string, unknown> & { id: string | number },
16
+ > = {
17
+ getRows: () => T[];
18
+ nextId: () => string | number;
19
+ /** Defaults to `{ ...data, id }` from form field sources. */
20
+ mapCreate?: (data: Row, id: string | number) => T;
21
+ /** Defaults to spread patch over current row. */
22
+ applyUpdate?: (current: T, patch: Row) => T;
23
+ scopeList?: (rows: T[], params: GetListParams) => T[];
24
+ afterDelete?: (removed: T) => void;
25
+ };
26
+
27
+ /**
28
+ * Generic in-memory CRUD for demos and tests. Override hooks for entity-specific rules.
29
+ */
30
+ export function createMemoryResourceHandlers<
31
+ T extends Record<string, unknown> & { id: string | number },
32
+ >(config: MemoryResourceHandlersConfig<T>): ResourceHandlers<T> {
33
+ const asRows = (xs: T[]) => xs as unknown as Row[];
34
+
35
+ const mapCreate =
36
+ config.mapCreate ??
37
+ ((data: Row, id: string | number) => ({ ...data, id }) as T);
38
+
39
+ const applyUpdate =
40
+ config.applyUpdate ??
41
+ ((current: T, patch: Row) => ({ ...current, ...patch, id: current.id }) as T);
42
+
43
+ return {
44
+ async getList(params): Promise<GetListResult<T>> {
45
+ const base = config.scopeList
46
+ ? config.scopeList(config.getRows(), params)
47
+ : config.getRows();
48
+ return applyInMemoryListParams(asRows(base), params) as GetListResult<T>;
49
+ },
50
+
51
+ async getOne(id, _params?): Promise<GetOneResult<T>> {
52
+ return { data: getById(config.getRows(), id) as T };
53
+ },
54
+
55
+ async create(data): Promise<CreateResult<T>> {
56
+ const row = mapCreate(data as Row, config.nextId());
57
+ config.getRows().push(row);
58
+ return { data: row };
59
+ },
60
+
61
+ async update({ id, data }): Promise<UpdateResult<T>> {
62
+ const current = getById(config.getRows(), id);
63
+ const patch = data as Row;
64
+ const next = applyUpdate(current, patch);
65
+ Object.assign(current, next);
66
+ return { data: current };
67
+ },
68
+
69
+ async delete(id): Promise<DeleteResult<T>> {
70
+ const rows = config.getRows();
71
+ const idx = rows.findIndex((r) => rowIdMatches(r.id, id));
72
+ if (idx < 0) return { data: null };
73
+ const [removed] = rows.splice(idx, 1);
74
+ config.afterDelete?.(removed);
75
+ return { data: removed };
76
+ },
77
+ };
78
+ }