ding-react-admin 1.0.4 → 2.0.4

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 (230) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +254 -201
  3. package/dist/index.css +2 -0
  4. package/dist/index.js +2073 -1202
  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/InlineFormSet.d.ts +2 -17
  18. package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
  19. package/dist/src/crud/ResourceForm.d.ts +2 -10
  20. package/dist/src/crud/ResourceForm.d.ts.map +1 -1
  21. package/dist/src/crud/ResourceFormModal.d.ts +7 -1
  22. package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
  23. package/dist/src/crud/ResourceList.d.ts.map +1 -1
  24. package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
  25. package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
  26. package/dist/src/crud/columns/ImageColumn.d.ts +14 -0
  27. package/dist/src/crud/columns/ImageColumn.d.ts.map +1 -0
  28. package/dist/src/crud/context/InlineFieldsRegistry.d.ts +15 -0
  29. package/dist/src/crud/context/InlineFieldsRegistry.d.ts.map +1 -0
  30. package/dist/src/crud/context/PayloadFieldsContext.d.ts +18 -0
  31. package/dist/src/crud/context/PayloadFieldsContext.d.ts.map +1 -0
  32. package/dist/src/crud/fields/FileField.d.ts +12 -0
  33. package/dist/src/crud/fields/FileField.d.ts.map +1 -0
  34. package/dist/src/crud/fields/ImageField.d.ts +14 -0
  35. package/dist/src/crud/fields/ImageField.d.ts.map +1 -0
  36. package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
  37. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  38. package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
  39. package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
  40. package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
  41. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  42. package/dist/src/crud/fields/uploadFieldUtils.d.ts +5 -0
  43. package/dist/src/crud/fields/uploadFieldUtils.d.ts.map +1 -0
  44. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts +4 -0
  45. package/dist/src/crud/fields/useUploadPreviewUrl.d.ts.map +1 -0
  46. package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
  47. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
  48. package/dist/src/crud/index.d.ts +27 -9
  49. package/dist/src/crud/index.d.ts.map +1 -1
  50. package/dist/src/crud/types.d.ts +21 -7
  51. package/dist/src/crud/types.d.ts.map +1 -1
  52. package/dist/src/crud/utils/buildFormPayload.d.ts +6 -0
  53. package/dist/src/crud/utils/buildFormPayload.d.ts.map +1 -0
  54. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts +6 -0
  55. package/dist/src/crud/utils/buildInlineRowsPayload.d.ts.map +1 -0
  56. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts +5 -0
  57. package/dist/src/crud/utils/buildResourceFormSubmitBody.d.ts.map +1 -0
  58. package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
  59. package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
  60. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
  61. package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
  62. package/dist/src/crud/utils/formErrors.d.ts +18 -7
  63. package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
  64. package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
  65. package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
  66. package/dist/src/crud/utils/getFormValue.d.ts +3 -0
  67. package/dist/src/crud/utils/getFormValue.d.ts.map +1 -0
  68. package/dist/src/crud/utils/hasUploadValues.d.ts +3 -0
  69. package/dist/src/crud/utils/hasUploadValues.d.ts.map +1 -0
  70. package/dist/src/crud/utils/nestedFieldPath.d.ts +3 -0
  71. package/dist/src/crud/utils/nestedFieldPath.d.ts.map +1 -0
  72. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +8 -0
  73. package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -0
  74. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
  75. package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
  76. package/dist/src/crud/utils/setFormValue.d.ts +3 -0
  77. package/dist/src/crud/utils/setFormValue.d.ts.map +1 -0
  78. package/dist/src/crud/utils/toFormData.d.ts +13 -0
  79. package/dist/src/crud/utils/toFormData.d.ts.map +1 -0
  80. package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
  81. package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
  82. package/dist/src/crud/utils/useChoices.d.ts +27 -1
  83. package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
  84. package/dist/src/crud/utils/useFormRecord.d.ts +44 -0
  85. package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -0
  86. package/dist/src/data/createRestResourceHandlers.d.ts +5 -5
  87. package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -1
  88. package/dist/src/data/dataProviderTypes.d.ts +6 -5
  89. package/dist/src/data/dataProviderTypes.d.ts.map +1 -1
  90. package/dist/src/data/parseFormErrorHelpers.d.ts +23 -6
  91. package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
  92. package/dist/src/data/resourceHandlers.d.ts +1 -1
  93. package/dist/src/data/resourceHandlers.d.ts.map +1 -1
  94. package/dist/src/index.d.ts +4 -4
  95. package/dist/src/index.d.ts.map +1 -1
  96. package/dist/src/layouts/AdminLayout.d.ts +1 -1
  97. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  98. package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
  99. package/dist/src/layouts/navFilter.d.ts +5 -0
  100. package/dist/src/layouts/navFilter.d.ts.map +1 -0
  101. package/dist/src/layouts/navMenuItems.d.ts +6 -0
  102. package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
  103. package/dist/src/types.d.ts +12 -0
  104. package/dist/src/types.d.ts.map +1 -1
  105. package/package.json +79 -59
  106. package/src/app/AdminApp.tsx +34 -34
  107. package/src/components/AuthAlternateLink.tsx +23 -23
  108. package/src/components/DensitySwitch.tsx +31 -31
  109. package/src/components/NavMenuLabel.tsx +26 -0
  110. package/src/components/NavMenuSearch.tsx +76 -0
  111. package/src/components/ScrollableArea.tsx +46 -0
  112. package/src/components/ThemeSwitch.tsx +28 -28
  113. package/src/components/ThemeToolbar.tsx +34 -34
  114. package/src/components/index.ts +7 -4
  115. package/src/components/navMenu.css +21 -0
  116. package/src/components/scrollable.css +28 -0
  117. package/src/context/AppThemeProvider.tsx +128 -128
  118. package/src/context/AuthProvider.tsx +89 -77
  119. package/src/context/DataProvider.tsx +30 -30
  120. package/src/context/PermissionsProvider.tsx +57 -57
  121. package/src/crud/FilterBar.tsx +36 -36
  122. package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
  123. package/src/crud/FormSteps.tsx +135 -135
  124. package/src/crud/FormTabs.tsx +126 -126
  125. package/src/crud/InlineFormSet.tsx +162 -219
  126. package/src/crud/ListActionsBar.tsx +106 -106
  127. package/src/crud/ResourceForm.tsx +83 -166
  128. package/src/crud/ResourceFormModal.tsx +75 -133
  129. package/src/crud/ResourceList.tsx +1 -0
  130. package/src/crud/ResourceRecordForm.tsx +166 -0
  131. package/src/crud/columns/BooleanColumn.tsx +32 -32
  132. package/src/crud/columns/CustomColumn.tsx +33 -33
  133. package/src/crud/columns/DateColumn.tsx +28 -28
  134. package/src/crud/columns/ImageColumn.tsx +58 -0
  135. package/src/crud/columns/NumberColumn.tsx +31 -31
  136. package/src/crud/columns/ReferenceColumn.tsx +85 -85
  137. package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
  138. package/src/crud/columns/TextColumn.tsx +38 -38
  139. package/src/crud/context/FilterContext.tsx +71 -71
  140. package/src/crud/context/FormContext.tsx +39 -39
  141. package/src/crud/context/FormSectionContext.tsx +22 -22
  142. package/src/crud/context/InlineFieldsRegistry.tsx +61 -0
  143. package/src/crud/context/ListContext.tsx +93 -93
  144. package/src/crud/context/PayloadFieldsContext.tsx +62 -0
  145. package/src/crud/fields/BooleanField.tsx +38 -38
  146. package/src/crud/fields/DateField.tsx +53 -53
  147. package/src/crud/fields/FieldWrapper.tsx +87 -87
  148. package/src/crud/fields/FileField.tsx +123 -0
  149. package/src/crud/fields/ImageField.tsx +120 -0
  150. package/src/crud/fields/NumberField.tsx +52 -52
  151. package/src/crud/fields/PasswordField.tsx +99 -99
  152. package/src/crud/fields/ReferenceField.tsx +237 -96
  153. package/src/crud/fields/ReferenceInputActions.tsx +106 -0
  154. package/src/crud/fields/ReferenceManyField.tsx +200 -76
  155. package/src/crud/fields/SelectField.tsx +48 -48
  156. package/src/crud/fields/TextField.tsx +46 -46
  157. package/src/crud/fields/uploadFieldUtils.ts +21 -0
  158. package/src/crud/fields/useUploadPreviewUrl.ts +20 -0
  159. package/src/crud/filters/BooleanFilter.tsx +38 -38
  160. package/src/crud/filters/DateFilter.tsx +35 -35
  161. package/src/crud/filters/NumberFilter.tsx +33 -33
  162. package/src/crud/filters/ReferenceFilter.tsx +192 -173
  163. package/src/crud/filters/SelectFilter.tsx +44 -44
  164. package/src/crud/filters/TextFilter.tsx +36 -36
  165. package/src/crud/index.ts +99 -90
  166. package/src/crud/types.ts +146 -132
  167. package/src/crud/utils/buildFormPayload.ts +24 -0
  168. package/src/crud/utils/buildInlineRowsPayload.ts +37 -0
  169. package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -0
  170. package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
  171. package/src/crud/utils/choiceSelectionUtils.ts +116 -0
  172. package/src/crud/utils/formErrors.test.ts +168 -0
  173. package/src/crud/utils/formErrors.ts +138 -38
  174. package/src/crud/utils/formSectionErrors.ts +85 -85
  175. package/src/crud/utils/{getByPath.ts → getFormValue.ts} +13 -13
  176. package/src/crud/utils/hasUploadValues.ts +9 -0
  177. package/src/crud/utils/nestedFieldPath.ts +8 -0
  178. package/src/crud/utils/prepareFormSubmitBody.ts +22 -0
  179. package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
  180. package/src/crud/utils/{setByPath.ts → setFormValue.ts} +18 -18
  181. package/src/crud/utils/sortQueryParam.ts +37 -37
  182. package/src/crud/utils/toFormData.ts +79 -0
  183. package/src/crud/utils/uploadReferenceUtils.ts +44 -0
  184. package/src/crud/utils/useAbortableEffect.ts +17 -17
  185. package/src/crud/utils/useChoices.ts +330 -182
  186. package/src/crud/utils/useFormRecord.ts +179 -0
  187. package/src/crud/utils/useListQueryState.ts +191 -191
  188. package/src/data/abortError.test.ts +19 -19
  189. package/src/data/abortError.ts +10 -10
  190. package/src/data/createMemoryResourceHandlers.ts +78 -78
  191. package/src/data/createRestResourceHandlers.ts +63 -57
  192. package/src/data/dataProviderTypes.ts +7 -5
  193. package/src/data/dataUtils.test.ts +228 -108
  194. package/src/data/inMemoryList.ts +97 -97
  195. package/src/data/parseFormErrorHelpers.test.ts +201 -84
  196. package/src/data/parseFormErrorHelpers.ts +376 -224
  197. package/src/data/resourceHandlers.ts +1 -1
  198. package/src/data/sortHelpers.ts +48 -48
  199. package/src/index.ts +211 -195
  200. package/src/layouts/AdminLayout.tsx +530 -381
  201. package/src/layouts/AuthPageLayout.tsx +87 -81
  202. package/src/layouts/navFilter.ts +49 -0
  203. package/src/layouts/navMenuItems.tsx +44 -0
  204. package/src/pages/LoginPage.tsx +80 -80
  205. package/src/pages/PlaceholderPage.tsx +9 -9
  206. package/src/permissions/resourcePermissions.ts +47 -47
  207. package/src/router/createAdminRouter.tsx +64 -64
  208. package/src/router/guards.tsx +63 -63
  209. package/src/router/routeAccess.ts +89 -89
  210. package/src/types.ts +10 -0
  211. package/src/vite-env.d.ts +1 -1
  212. package/dist/src/crud/context/SubmitFieldsContext.d.ts +0 -18
  213. package/dist/src/crud/context/SubmitFieldsContext.d.ts.map +0 -1
  214. package/dist/src/crud/utils/getByPath.d.ts +0 -3
  215. package/dist/src/crud/utils/getByPath.d.ts.map +0 -1
  216. package/dist/src/crud/utils/inlineArrayName.d.ts +0 -3
  217. package/dist/src/crud/utils/inlineArrayName.d.ts.map +0 -1
  218. package/dist/src/crud/utils/inlineFieldName.d.ts +0 -3
  219. package/dist/src/crud/utils/inlineFieldName.d.ts.map +0 -1
  220. package/dist/src/crud/utils/pickBySources.d.ts +0 -6
  221. package/dist/src/crud/utils/pickBySources.d.ts.map +0 -1
  222. package/dist/src/crud/utils/setByPath.d.ts +0 -3
  223. package/dist/src/crud/utils/setByPath.d.ts.map +0 -1
  224. package/dist/src/crud/utils/useResourceFormData.d.ts +0 -47
  225. package/dist/src/crud/utils/useResourceFormData.d.ts.map +0 -1
  226. package/src/crud/context/SubmitFieldsContext.tsx +0 -62
  227. package/src/crud/utils/inlineArrayName.ts +0 -4
  228. package/src/crud/utils/inlineFieldName.ts +0 -8
  229. package/src/crud/utils/pickBySources.ts +0 -24
  230. package/src/crud/utils/useResourceFormData.ts +0 -231
@@ -0,0 +1,179 @@
1
+ import type { MessageInstance } from "antd/es/message/interface";
2
+ import {
3
+ useCallback,
4
+ useState,
5
+ type MutableRefObject,
6
+ } from "react";
7
+ import type { DataProvider } from "../../data/dataProviderTypes";
8
+ import { isAbortError } from "../../data/abortError";
9
+ import {
10
+ type DefaultValues,
11
+ type FieldValues,
12
+ type UseFormReturn,
13
+ } from "react-hook-form";
14
+ import type { InlineFieldRegistration } from "../context/InlineFieldsRegistry";
15
+ import { buildResourceFormSubmitBody } from "./buildResourceFormSubmitBody";
16
+ import { applyApiErrorsToForm } from "./formErrors";
17
+ import { useAbortableEffect } from "./useAbortableEffect";
18
+
19
+ type LoadOptions<T extends FieldValues> = {
20
+ dp: DataProvider;
21
+ resource: string;
22
+ id: string | undefined;
23
+ isNew: boolean;
24
+ form: UseFormReturn<T>;
25
+ message: MessageInstance;
26
+ defaultValues?: Partial<T>;
27
+ /** When false, skip loading (e.g. modal closed). Defaults to true. */
28
+ enabled?: boolean;
29
+ };
30
+
31
+ /** Edit mode: `getOne` → `form.reset` → bump `formVersion` so field arrays remount. */
32
+ export function useFormRecordLoad<T extends FieldValues>({
33
+ dp,
34
+ resource,
35
+ id,
36
+ isNew,
37
+ form,
38
+ message,
39
+ defaultValues,
40
+ enabled = true,
41
+ }: LoadOptions<T>) {
42
+ const [loading, setLoading] = useState(!isNew);
43
+ const [formVersion, setFormVersion] = useState(0);
44
+
45
+ const load = useCallback(
46
+ async (signal?: AbortSignal) => {
47
+ if (isNew || !id) {
48
+ if (defaultValues) {
49
+ form.reset({ ...defaultValues } as DefaultValues<T>);
50
+ } else {
51
+ form.reset({} as DefaultValues<T>);
52
+ }
53
+ setLoading(false);
54
+ return;
55
+ }
56
+ setLoading(true);
57
+ try {
58
+ const res = await dp.getOne(resource, id, { signal });
59
+ if (signal?.aborted) return;
60
+ form.reset(res.data as DefaultValues<T>);
61
+ setFormVersion((v) => v + 1);
62
+ } catch (e) {
63
+ if (!isAbortError(e)) {
64
+ message.error(e instanceof Error ? e.message : "Load failed");
65
+ }
66
+ } finally {
67
+ if (!signal?.aborted) setLoading(false);
68
+ }
69
+ },
70
+ [dp, resource, id, isNew, form, message, defaultValues],
71
+ );
72
+
73
+ useAbortableEffect(
74
+ (signal) => {
75
+ if (!enabled) return;
76
+ return load(signal);
77
+ },
78
+ [enabled, load],
79
+ );
80
+
81
+ return { loading, formVersion };
82
+ }
83
+
84
+ type SubmitOptions<T extends FieldValues & { id?: unknown }> = {
85
+ dp: DataProvider;
86
+ resource: string;
87
+ id: string | undefined;
88
+ isNew: boolean;
89
+ form: UseFormReturn<T>;
90
+ message: MessageInstance;
91
+ payloadFieldsRef: MutableRefObject<Set<string>>;
92
+ inlineRegistryRef: MutableRefObject<Map<string, InlineFieldRegistration>>;
93
+ setGlobalErrors: (errors: string[]) => void;
94
+ onSuccess?: (record: T) => void;
95
+ };
96
+
97
+ /** Save: `buildFormPayload` → clean inline rows → one `create` or `update`. */
98
+ export function useFormRecordSave<T extends FieldValues & { id?: unknown }>({
99
+ dp,
100
+ resource,
101
+ id,
102
+ isNew,
103
+ form,
104
+ message,
105
+ payloadFieldsRef,
106
+ inlineRegistryRef,
107
+ setGlobalErrors,
108
+ onSuccess,
109
+ }: SubmitOptions<T>) {
110
+ const [saving, setSaving] = useState(false);
111
+
112
+ const onSubmit = useCallback(
113
+ async (values: T) => {
114
+ setGlobalErrors([]);
115
+ setSaving(true);
116
+ try {
117
+ const raw = values as Record<string, unknown>;
118
+ const body = buildResourceFormSubmitBody(
119
+ raw,
120
+ Array.from(payloadFieldsRef.current),
121
+ inlineRegistryRef.current.values(),
122
+ );
123
+
124
+ let saved: T;
125
+ if (isNew) {
126
+ const res = await dp.create(resource, body);
127
+ saved = res.data as T;
128
+ message.success("Created");
129
+ } else if (id) {
130
+ const res = await dp.update(resource, { id, data: body });
131
+ saved = res.data as T;
132
+ message.success("Updated");
133
+ } else {
134
+ return;
135
+ }
136
+
137
+ onSuccess?.(saved);
138
+ } catch (e) {
139
+ const { handled, globalErrors } = await applyApiErrorsToForm(
140
+ dp,
141
+ form,
142
+ e,
143
+ {
144
+ resource,
145
+ mutation: isNew ? "create" : "update",
146
+ inlineFieldPaths: Array.from(inlineRegistryRef.current.keys()),
147
+ },
148
+ {
149
+ payloadFields: payloadFieldsRef.current,
150
+ inlineRegistry: inlineRegistryRef.current.values(),
151
+ },
152
+ );
153
+ if (handled) {
154
+ setGlobalErrors(globalErrors);
155
+ message.error(globalErrors[0] ?? "Save failed.");
156
+ } else {
157
+ setGlobalErrors([]);
158
+ message.error(e instanceof Error ? e.message : "Save failed");
159
+ }
160
+ } finally {
161
+ setSaving(false);
162
+ }
163
+ },
164
+ [
165
+ dp,
166
+ resource,
167
+ id,
168
+ isNew,
169
+ form,
170
+ message,
171
+ payloadFieldsRef,
172
+ inlineRegistryRef,
173
+ setGlobalErrors,
174
+ onSuccess,
175
+ ],
176
+ );
177
+
178
+ return { onSubmit, saving };
179
+ }
@@ -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
+ }