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.
- package/LICENSE +21 -21
- package/README.md +254 -195
- package/dist/index.css +1 -0
- package/dist/index.js +1773 -1170
- package/dist/src/components/NavMenuLabel.d.ts +8 -0
- package/dist/src/components/NavMenuLabel.d.ts.map +1 -0
- package/dist/src/components/NavMenuSearch.d.ts +9 -0
- package/dist/src/components/NavMenuSearch.d.ts.map +1 -0
- package/dist/src/components/ScrollableArea.d.ts +10 -0
- package/dist/src/components/ScrollableArea.d.ts.map +1 -0
- package/dist/src/components/index.d.ts +3 -0
- package/dist/src/components/index.d.ts.map +1 -1
- package/dist/src/context/AuthProvider.d.ts +2 -0
- package/dist/src/context/AuthProvider.d.ts.map +1 -1
- package/dist/src/crud/FormGlobalErrorsAlert.d.ts +5 -0
- package/dist/src/crud/FormGlobalErrorsAlert.d.ts.map +1 -0
- package/dist/src/crud/ResourceForm.d.ts +1 -4
- package/dist/src/crud/ResourceForm.d.ts.map +1 -1
- package/dist/src/crud/ResourceFormModal.d.ts +7 -1
- package/dist/src/crud/ResourceFormModal.d.ts.map +1 -1
- package/dist/src/crud/ResourceRecordForm.d.ts +27 -0
- package/dist/src/crud/ResourceRecordForm.d.ts.map +1 -0
- package/dist/src/crud/fields/ReferenceField.d.ts +12 -2
- package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
- package/dist/src/crud/fields/ReferenceInputActions.d.ts +16 -0
- package/dist/src/crud/fields/ReferenceInputActions.d.ts.map +1 -0
- package/dist/src/crud/fields/ReferenceManyField.d.ts +10 -1
- package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
- package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
- package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
- package/dist/src/crud/index.d.ts +5 -0
- package/dist/src/crud/index.d.ts.map +1 -1
- package/dist/src/crud/types.d.ts +12 -0
- package/dist/src/crud/types.d.ts.map +1 -1
- package/dist/src/crud/utils/choiceSelectionUtils.d.ts +10 -0
- package/dist/src/crud/utils/choiceSelectionUtils.d.ts.map +1 -0
- package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts +2 -0
- package/dist/src/crud/utils/choiceSelectionUtils.test.d.ts.map +1 -0
- package/dist/src/crud/utils/formErrors.d.ts +18 -7
- package/dist/src/crud/utils/formErrors.d.ts.map +1 -1
- package/dist/src/crud/utils/formErrors.test.d.ts +2 -0
- package/dist/src/crud/utils/formErrors.test.d.ts.map +1 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts +1 -0
- package/dist/src/crud/utils/prepareFormSubmitBody.d.ts.map +1 -1
- package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts +4 -0
- package/dist/src/crud/utils/referenceSelectNotFoundContent.d.ts.map +1 -0
- package/dist/src/crud/utils/toFormData.d.ts +1 -1
- package/dist/src/crud/utils/toFormData.d.ts.map +1 -1
- package/dist/src/crud/utils/uploadReferenceUtils.d.ts +5 -0
- package/dist/src/crud/utils/uploadReferenceUtils.d.ts.map +1 -0
- package/dist/src/crud/utils/useChoices.d.ts +27 -1
- package/dist/src/crud/utils/useChoices.d.ts.map +1 -1
- package/dist/src/crud/utils/useFormRecord.d.ts +9 -7
- package/dist/src/crud/utils/useFormRecord.d.ts.map +1 -1
- package/dist/src/data/parseFormErrorHelpers.d.ts +6 -27
- package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/layouts/AdminLayout.d.ts +1 -1
- package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
- package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -1
- package/dist/src/layouts/navFilter.d.ts +5 -0
- package/dist/src/layouts/navFilter.d.ts.map +1 -0
- package/dist/src/layouts/navMenuItems.d.ts +6 -0
- package/dist/src/layouts/navMenuItems.d.ts.map +1 -0
- package/dist/src/types.d.ts +12 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +85 -59
- package/src/app/AdminApp.tsx +34 -34
- package/src/components/AuthAlternateLink.tsx +23 -23
- package/src/components/DensitySwitch.tsx +31 -31
- package/src/components/NavMenuLabel.tsx +26 -0
- package/src/components/NavMenuSearch.tsx +76 -0
- package/src/components/ScrollableArea.tsx +46 -0
- package/src/components/ThemeSwitch.tsx +28 -28
- package/src/components/ThemeToolbar.tsx +34 -34
- package/src/components/index.ts +7 -4
- package/src/components/navMenu.css +21 -0
- package/src/components/scrollable.css +28 -0
- package/src/context/AppThemeProvider.tsx +128 -128
- package/src/context/AuthProvider.tsx +89 -77
- package/src/context/DataProvider.tsx +30 -30
- package/src/context/PermissionsProvider.tsx +57 -57
- package/src/crud/FilterBar.tsx +36 -36
- package/src/crud/FormGlobalErrorsAlert.tsx +36 -0
- package/src/crud/FormSteps.tsx +135 -135
- package/src/crud/FormTabs.tsx +126 -126
- package/src/crud/InlineFormSet.tsx +162 -162
- package/src/crud/ListActionsBar.tsx +106 -106
- package/src/crud/ResourceForm.tsx +83 -160
- package/src/crud/ResourceFormModal.tsx +75 -133
- package/src/crud/ResourceRecordForm.tsx +166 -0
- package/src/crud/columns/BooleanColumn.tsx +32 -32
- package/src/crud/columns/CustomColumn.tsx +33 -33
- package/src/crud/columns/DateColumn.tsx +28 -28
- package/src/crud/columns/ImageColumn.tsx +58 -58
- package/src/crud/columns/NumberColumn.tsx +31 -31
- package/src/crud/columns/ReferenceColumn.tsx +85 -85
- package/src/crud/columns/ReferenceManyColumn.tsx +67 -67
- package/src/crud/columns/TextColumn.tsx +38 -38
- package/src/crud/context/FilterContext.tsx +71 -71
- package/src/crud/context/FormContext.tsx +39 -39
- package/src/crud/context/FormSectionContext.tsx +22 -22
- package/src/crud/context/InlineFieldsRegistry.tsx +61 -61
- package/src/crud/context/ListContext.tsx +93 -93
- package/src/crud/fields/BooleanField.tsx +38 -38
- package/src/crud/fields/DateField.tsx +53 -53
- package/src/crud/fields/FileField.tsx +123 -123
- package/src/crud/fields/ImageField.tsx +120 -120
- package/src/crud/fields/NumberField.tsx +52 -52
- package/src/crud/fields/PasswordField.tsx +99 -99
- package/src/crud/fields/ReferenceField.tsx +237 -96
- package/src/crud/fields/ReferenceInputActions.tsx +106 -0
- package/src/crud/fields/ReferenceManyField.tsx +200 -76
- package/src/crud/fields/SelectField.tsx +48 -48
- package/src/crud/fields/TextField.tsx +46 -46
- package/src/crud/fields/uploadFieldUtils.ts +21 -21
- package/src/crud/fields/useUploadPreviewUrl.ts +20 -20
- package/src/crud/filters/BooleanFilter.tsx +38 -38
- package/src/crud/filters/DateFilter.tsx +35 -35
- package/src/crud/filters/NumberFilter.tsx +33 -33
- package/src/crud/filters/ReferenceFilter.tsx +192 -173
- package/src/crud/filters/SelectFilter.tsx +44 -44
- package/src/crud/filters/TextFilter.tsx +36 -36
- package/src/crud/index.ts +99 -94
- package/src/crud/types.ts +146 -134
- package/src/crud/utils/buildFormPayload.ts +24 -24
- package/src/crud/utils/buildInlineRowsPayload.ts +37 -37
- package/src/crud/utils/buildResourceFormSubmitBody.ts +29 -29
- package/src/crud/utils/choiceSelectionUtils.test.ts +45 -0
- package/src/crud/utils/choiceSelectionUtils.ts +116 -0
- package/src/crud/utils/formErrors.test.ts +168 -0
- package/src/crud/utils/formErrors.ts +138 -42
- package/src/crud/utils/formSectionErrors.ts +85 -85
- package/src/crud/utils/getFormValue.ts +13 -13
- package/src/crud/utils/hasUploadValues.ts +9 -9
- package/src/crud/utils/nestedFieldPath.ts +8 -8
- package/src/crud/utils/prepareFormSubmitBody.ts +22 -17
- package/src/crud/utils/referenceSelectNotFoundContent.tsx +7 -0
- package/src/crud/utils/setFormValue.ts +18 -18
- package/src/crud/utils/sortQueryParam.ts +37 -37
- package/src/crud/utils/toFormData.ts +79 -81
- package/src/crud/utils/uploadReferenceUtils.ts +44 -0
- package/src/crud/utils/useAbortableEffect.ts +17 -17
- package/src/crud/utils/useChoices.ts +330 -182
- package/src/crud/utils/useFormRecord.ts +179 -156
- package/src/crud/utils/useListQueryState.ts +191 -191
- package/src/data/abortError.test.ts +19 -19
- package/src/data/abortError.ts +10 -10
- package/src/data/createMemoryResourceHandlers.ts +78 -78
- package/src/data/createRestResourceHandlers.ts +63 -63
- package/src/data/dataUtils.test.ts +228 -220
- package/src/data/inMemoryList.ts +97 -97
- package/src/data/parseFormErrorHelpers.test.ts +201 -138
- package/src/data/parseFormErrorHelpers.ts +376 -311
- package/src/data/sortHelpers.ts +48 -48
- package/src/index.ts +211 -207
- package/src/layouts/AdminLayout.tsx +530 -382
- package/src/layouts/AuthPageLayout.tsx +87 -81
- package/src/layouts/navFilter.ts +49 -0
- package/src/layouts/navMenuItems.tsx +44 -0
- package/src/pages/LoginPage.tsx +80 -80
- package/src/pages/PlaceholderPage.tsx +9 -9
- package/src/permissions/resourcePermissions.ts +47 -47
- package/src/router/createAdminRouter.tsx +64 -64
- package/src/router/guards.tsx +63 -63
- package/src/router/routeAccess.ts +89 -89
- package/src/types.ts +10 -0
- 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
|
+
});
|
package/src/data/abortError.ts
CHANGED
|
@@ -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
|
+
}
|