ding-react-admin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3067 -0
- package/dist/src/app/AdminApp.d.ts +7 -0
- package/dist/src/app/AdminApp.d.ts.map +1 -0
- package/dist/src/components/AuthAlternateLink.d.ts +7 -0
- package/dist/src/components/AuthAlternateLink.d.ts.map +1 -0
- package/dist/src/components/DensitySwitch.d.ts +2 -0
- package/dist/src/components/DensitySwitch.d.ts.map +1 -0
- package/dist/src/components/ThemeSwitch.d.ts +2 -0
- package/dist/src/components/ThemeSwitch.d.ts.map +1 -0
- package/dist/src/components/ThemeToolbar.d.ts +2 -0
- package/dist/src/components/ThemeToolbar.d.ts.map +1 -0
- package/dist/src/components/index.d.ts +5 -0
- package/dist/src/components/index.d.ts.map +1 -0
- package/dist/src/context/AppThemeProvider.d.ts +13 -0
- package/dist/src/context/AppThemeProvider.d.ts.map +1 -0
- package/dist/src/context/AuthProvider.d.ts +18 -0
- package/dist/src/context/AuthProvider.d.ts.map +1 -0
- package/dist/src/context/DataProvider.d.ts +10 -0
- package/dist/src/context/DataProvider.d.ts.map +1 -0
- package/dist/src/context/PermissionsProvider.d.ts +18 -0
- package/dist/src/context/PermissionsProvider.d.ts.map +1 -0
- package/dist/src/crud/FilterBar.d.ts +6 -0
- package/dist/src/crud/FilterBar.d.ts.map +1 -0
- package/dist/src/crud/FormSteps.d.ts +20 -0
- package/dist/src/crud/FormSteps.d.ts.map +1 -0
- package/dist/src/crud/FormTabs.d.ts +13 -0
- package/dist/src/crud/FormTabs.d.ts.map +1 -0
- package/dist/src/crud/InlineFormSet.d.ts +29 -0
- package/dist/src/crud/InlineFormSet.d.ts.map +1 -0
- package/dist/src/crud/ListActionsBar.d.ts +15 -0
- package/dist/src/crud/ListActionsBar.d.ts.map +1 -0
- package/dist/src/crud/ResourceForm.d.ts +21 -0
- package/dist/src/crud/ResourceForm.d.ts.map +1 -0
- package/dist/src/crud/ResourceFormModal.d.ts +10 -0
- package/dist/src/crud/ResourceFormModal.d.ts.map +1 -0
- package/dist/src/crud/ResourceList.d.ts +38 -0
- package/dist/src/crud/ResourceList.d.ts.map +1 -0
- package/dist/src/crud/columns/BooleanColumn.d.ts +6 -0
- package/dist/src/crud/columns/BooleanColumn.d.ts.map +1 -0
- package/dist/src/crud/columns/CustomColumn.d.ts +9 -0
- package/dist/src/crud/columns/CustomColumn.d.ts.map +1 -0
- package/dist/src/crud/columns/DateColumn.d.ts +6 -0
- package/dist/src/crud/columns/DateColumn.d.ts.map +1 -0
- package/dist/src/crud/columns/NumberColumn.d.ts +6 -0
- package/dist/src/crud/columns/NumberColumn.d.ts.map +1 -0
- package/dist/src/crud/columns/ReferenceColumn.d.ts +6 -0
- package/dist/src/crud/columns/ReferenceColumn.d.ts.map +1 -0
- package/dist/src/crud/columns/ReferenceManyColumn.d.ts +6 -0
- package/dist/src/crud/columns/ReferenceManyColumn.d.ts.map +1 -0
- package/dist/src/crud/columns/TextColumn.d.ts +7 -0
- package/dist/src/crud/columns/TextColumn.d.ts.map +1 -0
- package/dist/src/crud/context/FilterContext.d.ts +17 -0
- package/dist/src/crud/context/FilterContext.d.ts.map +1 -0
- package/dist/src/crud/context/FormContext.d.ts +16 -0
- package/dist/src/crud/context/FormContext.d.ts.map +1 -0
- package/dist/src/crud/context/FormFieldsContext.d.ts +14 -0
- package/dist/src/crud/context/FormFieldsContext.d.ts.map +1 -0
- package/dist/src/crud/context/FormSectionContext.d.ts +7 -0
- package/dist/src/crud/context/FormSectionContext.d.ts.map +1 -0
- package/dist/src/crud/context/InlineFormContext.d.ts +16 -0
- package/dist/src/crud/context/InlineFormContext.d.ts.map +1 -0
- package/dist/src/crud/context/ListContext.d.ts +22 -0
- package/dist/src/crud/context/ListContext.d.ts.map +1 -0
- package/dist/src/crud/fields/BooleanField.d.ts +7 -0
- package/dist/src/crud/fields/BooleanField.d.ts.map +1 -0
- package/dist/src/crud/fields/DateField.d.ts +8 -0
- package/dist/src/crud/fields/DateField.d.ts.map +1 -0
- package/dist/src/crud/fields/FieldWrapper.d.ts +29 -0
- package/dist/src/crud/fields/FieldWrapper.d.ts.map +1 -0
- package/dist/src/crud/fields/NumberField.d.ts +12 -0
- package/dist/src/crud/fields/NumberField.d.ts.map +1 -0
- package/dist/src/crud/fields/PasswordField.d.ts +11 -0
- package/dist/src/crud/fields/PasswordField.d.ts.map +1 -0
- package/dist/src/crud/fields/ReferenceField.d.ts +18 -0
- package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -0
- package/dist/src/crud/fields/ReferenceManyField.d.ts +9 -0
- package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -0
- package/dist/src/crud/fields/SelectField.d.ts +10 -0
- package/dist/src/crud/fields/SelectField.d.ts.map +1 -0
- package/dist/src/crud/fields/TextField.d.ts +10 -0
- package/dist/src/crud/fields/TextField.d.ts.map +1 -0
- package/dist/src/crud/fields/useInlineOrFormField.d.ts +10 -0
- package/dist/src/crud/fields/useInlineOrFormField.d.ts.map +1 -0
- package/dist/src/crud/filters/BooleanFilter.d.ts +4 -0
- package/dist/src/crud/filters/BooleanFilter.d.ts.map +1 -0
- package/dist/src/crud/filters/DateFilter.d.ts +4 -0
- package/dist/src/crud/filters/DateFilter.d.ts.map +1 -0
- package/dist/src/crud/filters/NumberFilter.d.ts +4 -0
- package/dist/src/crud/filters/NumberFilter.d.ts.map +1 -0
- package/dist/src/crud/filters/ReferenceFilter.d.ts +13 -0
- package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -0
- package/dist/src/crud/filters/SelectFilter.d.ts +7 -0
- package/dist/src/crud/filters/SelectFilter.d.ts.map +1 -0
- package/dist/src/crud/filters/TextFilter.d.ts +6 -0
- package/dist/src/crud/filters/TextFilter.d.ts.map +1 -0
- package/dist/src/crud/index.d.ts +44 -0
- package/dist/src/crud/index.d.ts.map +1 -0
- package/dist/src/crud/types.d.ts +99 -0
- package/dist/src/crud/types.d.ts.map +1 -0
- package/dist/src/crud/utils/formErrors.d.ts +9 -0
- package/dist/src/crud/utils/formErrors.d.ts.map +1 -0
- package/dist/src/crud/utils/formSectionErrors.d.ts +6 -0
- package/dist/src/crud/utils/formSectionErrors.d.ts.map +1 -0
- package/dist/src/crud/utils/getByPath.d.ts +3 -0
- package/dist/src/crud/utils/getByPath.d.ts.map +1 -0
- package/dist/src/crud/utils/inlineArrayName.d.ts +3 -0
- package/dist/src/crud/utils/inlineArrayName.d.ts.map +1 -0
- package/dist/src/crud/utils/pickBySources.d.ts +6 -0
- package/dist/src/crud/utils/pickBySources.d.ts.map +1 -0
- package/dist/src/crud/utils/setByPath.d.ts +3 -0
- package/dist/src/crud/utils/setByPath.d.ts.map +1 -0
- package/dist/src/crud/utils/sortQueryParam.d.ts +7 -0
- package/dist/src/crud/utils/sortQueryParam.d.ts.map +1 -0
- package/dist/src/crud/utils/useChoices.d.ts +10 -0
- package/dist/src/crud/utils/useChoices.d.ts.map +1 -0
- package/dist/src/crud/utils/useListQueryState.d.ts +22 -0
- package/dist/src/crud/utils/useListQueryState.d.ts.map +1 -0
- package/dist/src/data/createMemoryResourceHandlers.d.ts +23 -0
- package/dist/src/data/createMemoryResourceHandlers.d.ts.map +1 -0
- package/dist/src/data/createRestResourceHandlers.d.ts +22 -0
- package/dist/src/data/createRestResourceHandlers.d.ts.map +1 -0
- package/dist/src/data/dataProviderTypes.d.ts +67 -0
- package/dist/src/data/dataProviderTypes.d.ts.map +1 -0
- package/dist/src/data/dataUtils.test.d.ts +2 -0
- package/dist/src/data/dataUtils.test.d.ts.map +1 -0
- package/dist/src/data/inMemoryList.d.ts +12 -0
- package/dist/src/data/inMemoryList.d.ts.map +1 -0
- package/dist/src/data/parseFormErrorHelpers.d.ts +35 -0
- package/dist/src/data/parseFormErrorHelpers.d.ts.map +1 -0
- package/dist/src/data/parseFormErrorHelpers.test.d.ts +2 -0
- package/dist/src/data/parseFormErrorHelpers.test.d.ts.map +1 -0
- package/dist/src/data/resourceHandlers.d.ts +33 -0
- package/dist/src/data/resourceHandlers.d.ts.map +1 -0
- package/dist/src/data/sortHelpers.d.ts +17 -0
- package/dist/src/data/sortHelpers.d.ts.map +1 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/layouts/AdminLayout.d.ts +3 -0
- package/dist/src/layouts/AdminLayout.d.ts.map +1 -0
- package/dist/src/layouts/AuthPageLayout.d.ts +11 -0
- package/dist/src/layouts/AuthPageLayout.d.ts.map +1 -0
- package/dist/src/pages/LoginPage.d.ts +3 -0
- package/dist/src/pages/LoginPage.d.ts.map +1 -0
- package/dist/src/pages/PlaceholderPage.d.ts +4 -0
- package/dist/src/pages/PlaceholderPage.d.ts.map +1 -0
- package/dist/src/permissions/resourcePermissions.d.ts +22 -0
- package/dist/src/permissions/resourcePermissions.d.ts.map +1 -0
- package/dist/src/router/createAdminRouter.d.ts +3 -0
- package/dist/src/router/createAdminRouter.d.ts.map +1 -0
- package/dist/src/router/guards.d.ts +20 -0
- package/dist/src/router/guards.d.ts.map +1 -0
- package/dist/src/router/routeAccess.d.ts +21 -0
- package/dist/src/router/routeAccess.d.ts.map +1 -0
- package/dist/src/types.d.ts +123 -0
- package/dist/src/types.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/app/AdminApp.tsx +34 -0
- package/src/components/AuthAlternateLink.tsx +23 -0
- package/src/components/DensitySwitch.tsx +31 -0
- package/src/components/ThemeSwitch.tsx +28 -0
- package/src/components/ThemeToolbar.tsx +34 -0
- package/src/components/index.ts +4 -0
- package/src/context/AppThemeProvider.tsx +128 -0
- package/src/context/AuthProvider.tsx +77 -0
- package/src/context/DataProvider.tsx +30 -0
- package/src/context/PermissionsProvider.tsx +57 -0
- package/src/crud/FilterBar.tsx +36 -0
- package/src/crud/FormSteps.tsx +135 -0
- package/src/crud/FormTabs.tsx +126 -0
- package/src/crud/InlineFormSet.tsx +231 -0
- package/src/crud/ListActionsBar.tsx +106 -0
- package/src/crud/ResourceForm.tsx +281 -0
- package/src/crud/ResourceFormModal.tsx +121 -0
- package/src/crud/ResourceList.tsx +633 -0
- package/src/crud/columns/BooleanColumn.tsx +32 -0
- package/src/crud/columns/CustomColumn.tsx +33 -0
- package/src/crud/columns/DateColumn.tsx +28 -0
- package/src/crud/columns/NumberColumn.tsx +31 -0
- package/src/crud/columns/ReferenceColumn.tsx +85 -0
- package/src/crud/columns/ReferenceManyColumn.tsx +67 -0
- package/src/crud/columns/TextColumn.tsx +38 -0
- package/src/crud/context/FilterContext.tsx +71 -0
- package/src/crud/context/FormContext.tsx +39 -0
- package/src/crud/context/FormFieldsContext.tsx +76 -0
- package/src/crud/context/FormSectionContext.tsx +22 -0
- package/src/crud/context/InlineFormContext.tsx +71 -0
- package/src/crud/context/ListContext.tsx +93 -0
- package/src/crud/fields/BooleanField.tsx +32 -0
- package/src/crud/fields/DateField.tsx +47 -0
- package/src/crud/fields/FieldWrapper.tsx +109 -0
- package/src/crud/fields/NumberField.tsx +50 -0
- package/src/crud/fields/PasswordField.tsx +98 -0
- package/src/crud/fields/ReferenceField.tsx +191 -0
- package/src/crud/fields/ReferenceManyField.tsx +70 -0
- package/src/crud/fields/SelectField.tsx +42 -0
- package/src/crud/fields/TextField.tsx +44 -0
- package/src/crud/fields/useInlineOrFormField.tsx +91 -0
- package/src/crud/filters/BooleanFilter.tsx +38 -0
- package/src/crud/filters/DateFilter.tsx +35 -0
- package/src/crud/filters/NumberFilter.tsx +33 -0
- package/src/crud/filters/ReferenceFilter.tsx +173 -0
- package/src/crud/filters/SelectFilter.tsx +44 -0
- package/src/crud/filters/TextFilter.tsx +36 -0
- package/src/crud/index.ts +73 -0
- package/src/crud/types.ts +130 -0
- package/src/crud/utils/formErrors.ts +38 -0
- package/src/crud/utils/formSectionErrors.ts +85 -0
- package/src/crud/utils/getByPath.ts +13 -0
- package/src/crud/utils/inlineArrayName.ts +4 -0
- package/src/crud/utils/pickBySources.ts +24 -0
- package/src/crud/utils/setByPath.ts +18 -0
- package/src/crud/utils/sortQueryParam.ts +37 -0
- package/src/crud/utils/useChoices.ts +182 -0
- package/src/crud/utils/useListQueryState.ts +191 -0
- package/src/data/createMemoryResourceHandlers.ts +78 -0
- package/src/data/createRestResourceHandlers.ts +57 -0
- package/src/data/dataProviderTypes.ts +101 -0
- package/src/data/dataUtils.test.ts +108 -0
- package/src/data/inMemoryList.ts +97 -0
- package/src/data/parseFormErrorHelpers.test.ts +84 -0
- package/src/data/parseFormErrorHelpers.ts +224 -0
- package/src/data/resourceHandlers.ts +124 -0
- package/src/data/sortHelpers.ts +48 -0
- package/src/index.ts +182 -0
- package/src/layouts/AdminLayout.tsx +381 -0
- package/src/layouts/AuthPageLayout.tsx +81 -0
- package/src/pages/LoginPage.tsx +80 -0
- package/src/pages/PlaceholderPage.tsx +9 -0
- package/src/permissions/resourcePermissions.ts +47 -0
- package/src/router/createAdminRouter.tsx +64 -0
- package/src/router/guards.tsx +63 -0
- package/src/router/routeAccess.ts +89 -0
- package/src/types.ts +135 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getByPath } from "./getByPath";
|
|
2
|
+
import { setByPath } from "./setByPath";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a submit payload from form values using declared field `source` paths.
|
|
6
|
+
* Flat `username` → `{ username }`; nested `invoiceLine.product` → `{ invoiceLine: { product } }`.
|
|
7
|
+
*/
|
|
8
|
+
export function pickBySources(
|
|
9
|
+
values: Record<string, unknown>,
|
|
10
|
+
sources: string[],
|
|
11
|
+
): Record<string, unknown> {
|
|
12
|
+
if (sources.length === 0) {
|
|
13
|
+
return { ...values };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result: Record<string, unknown> = {};
|
|
17
|
+
for (const source of sources) {
|
|
18
|
+
const value = getByPath(values, source);
|
|
19
|
+
if (value !== undefined) {
|
|
20
|
+
setByPath(result, source, value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Write a value into a record using dot notation, e.g. `invoiceLine.product`. */
|
|
2
|
+
export function setByPath(
|
|
3
|
+
record: Record<string, unknown>,
|
|
4
|
+
path: string,
|
|
5
|
+
value: unknown,
|
|
6
|
+
): void {
|
|
7
|
+
const parts = path.split(".");
|
|
8
|
+
let cur: Record<string, unknown> = record;
|
|
9
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
10
|
+
const part = parts[i];
|
|
11
|
+
const next = cur[part];
|
|
12
|
+
if (next == null || typeof next !== "object" || Array.isArray(next)) {
|
|
13
|
+
cur[part] = {};
|
|
14
|
+
}
|
|
15
|
+
cur = cur[part] as Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
cur[parts[parts.length - 1]!] = value;
|
|
18
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SortOrder, SortSpec } from "../../data/dataProviderTypes";
|
|
2
|
+
|
|
3
|
+
/** Parse `sort` query (Django-style: `sku,-price`; legacy: `sku:asc,price:desc`). */
|
|
4
|
+
export function parseSortParam(raw: string | null): SortSpec[] {
|
|
5
|
+
if (!raw) return [];
|
|
6
|
+
const specs: SortSpec[] = [];
|
|
7
|
+
for (const part of raw.split(",").map((p) => p.trim()).filter(Boolean)) {
|
|
8
|
+
if (part.includes(":")) {
|
|
9
|
+
const [field, orderRaw] = part.split(":");
|
|
10
|
+
if (!field) continue;
|
|
11
|
+
const order: SortOrder =
|
|
12
|
+
orderRaw?.toLowerCase() === "desc" ? "DESC" : "ASC";
|
|
13
|
+
specs.push({ field, order });
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (part.startsWith("-") && part.length > 1) {
|
|
17
|
+
specs.push({ field: part.slice(1), order: "DESC" });
|
|
18
|
+
} else {
|
|
19
|
+
specs.push({ field: part, order: "ASC" });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return specs;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Serialize sort specs to Django-style `sku,-price`. */
|
|
26
|
+
export function serializeSort(specs: SortSpec[]): string | null {
|
|
27
|
+
if (specs.length === 0) return null;
|
|
28
|
+
return specs
|
|
29
|
+
.map((s) => (s.order === "DESC" ? `-${s.field}` : s.field))
|
|
30
|
+
.join(",");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function sortPrioritiesFromSpecs(
|
|
34
|
+
specs: SortSpec[],
|
|
35
|
+
): Map<string, number> {
|
|
36
|
+
return new Map(specs.map((s, i) => [s.field, i + 1]));
|
|
37
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useDataProvider } from "../../context/DataProvider";
|
|
3
|
+
import type { ChoiceOption, ChoicesLoader } from "../types";
|
|
4
|
+
|
|
5
|
+
const choicesCache = new Map<string, ChoiceOption[]>();
|
|
6
|
+
const choicesInflight = new Map<string, Promise<ChoiceOption[]>>();
|
|
7
|
+
|
|
8
|
+
function loaderKey(loader: ChoicesLoader, search?: string): string {
|
|
9
|
+
if (typeof loader === "function") return `fn:${search ?? ""}`;
|
|
10
|
+
if (Array.isArray(loader)) return `static:${loader.length}`;
|
|
11
|
+
return `res:${loader.resource}:${JSON.stringify(loader.filter ?? {})}:${search ?? ""}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveOptionLabel(
|
|
15
|
+
record: Record<string, unknown>,
|
|
16
|
+
optionLabel: string | ((record: Record<string, unknown>) => string),
|
|
17
|
+
): string {
|
|
18
|
+
if (typeof optionLabel === "function") return optionLabel(record);
|
|
19
|
+
return String(record[optionLabel] ?? "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function fetchChoices(
|
|
23
|
+
effectiveLoader: ChoicesLoader,
|
|
24
|
+
dp: ReturnType<typeof useDataProvider>,
|
|
25
|
+
optionLabel: string | ((record: Record<string, unknown>) => string),
|
|
26
|
+
optionValue: string,
|
|
27
|
+
search?: string,
|
|
28
|
+
): Promise<ChoiceOption[]> {
|
|
29
|
+
if (typeof effectiveLoader === "function") {
|
|
30
|
+
return effectiveLoader({ dataProvider: dp, search });
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(effectiveLoader)) {
|
|
33
|
+
return effectiveLoader;
|
|
34
|
+
}
|
|
35
|
+
const res = await dp.getList(effectiveLoader.resource, {
|
|
36
|
+
filter: {
|
|
37
|
+
...effectiveLoader.filter,
|
|
38
|
+
...(search ? { q: search } : {}),
|
|
39
|
+
},
|
|
40
|
+
pagination: { page: 1, perPage: 500 },
|
|
41
|
+
});
|
|
42
|
+
return (res.data as Record<string, unknown>[]).map((row) => ({
|
|
43
|
+
label: resolveOptionLabel(row, optionLabel),
|
|
44
|
+
value: row[optionValue],
|
|
45
|
+
record: row,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadChoicesShared(
|
|
50
|
+
effectiveLoader: ChoicesLoader,
|
|
51
|
+
dp: ReturnType<typeof useDataProvider>,
|
|
52
|
+
optionLabel: string | ((record: Record<string, unknown>) => string),
|
|
53
|
+
optionValue: string,
|
|
54
|
+
search?: string,
|
|
55
|
+
): Promise<ChoiceOption[]> {
|
|
56
|
+
const key = loaderKey(effectiveLoader, search);
|
|
57
|
+
const cached = choicesCache.get(key);
|
|
58
|
+
if (cached && !search) {
|
|
59
|
+
return Promise.resolve(cached);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const inflight = choicesInflight.get(key);
|
|
63
|
+
if (inflight) return inflight;
|
|
64
|
+
|
|
65
|
+
const promise = fetchChoices(
|
|
66
|
+
effectiveLoader,
|
|
67
|
+
dp,
|
|
68
|
+
optionLabel,
|
|
69
|
+
optionValue,
|
|
70
|
+
search,
|
|
71
|
+
)
|
|
72
|
+
.then((result) => {
|
|
73
|
+
if (!search) choicesCache.set(key, result);
|
|
74
|
+
return result;
|
|
75
|
+
})
|
|
76
|
+
.finally(() => {
|
|
77
|
+
choicesInflight.delete(key);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
choicesInflight.set(key, promise);
|
|
81
|
+
return promise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function useChoices(
|
|
85
|
+
loader: ChoicesLoader | undefined,
|
|
86
|
+
reference: string | undefined,
|
|
87
|
+
optionLabel: string | ((record: Record<string, unknown>) => string) = "name",
|
|
88
|
+
optionValue = "id",
|
|
89
|
+
search?: string,
|
|
90
|
+
) {
|
|
91
|
+
const dp = useDataProvider();
|
|
92
|
+
|
|
93
|
+
const effectiveLoader = useMemo((): ChoicesLoader | undefined => {
|
|
94
|
+
if (loader) return loader;
|
|
95
|
+
if (!reference) return undefined;
|
|
96
|
+
return {
|
|
97
|
+
resource: reference,
|
|
98
|
+
filter: search ? { q: search } : undefined,
|
|
99
|
+
};
|
|
100
|
+
}, [loader, reference, search]);
|
|
101
|
+
|
|
102
|
+
const cacheKey = effectiveLoader
|
|
103
|
+
? loaderKey(effectiveLoader, search)
|
|
104
|
+
: undefined;
|
|
105
|
+
|
|
106
|
+
const [options, setOptions] = useState<ChoiceOption[]>(() => {
|
|
107
|
+
if (!cacheKey || search) return [];
|
|
108
|
+
return choicesCache.get(cacheKey) ?? [];
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const [loading, setLoading] = useState(() => {
|
|
112
|
+
if (!cacheKey || search) return Boolean(effectiveLoader);
|
|
113
|
+
return !choicesCache.has(cacheKey);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const load = useCallback(async () => {
|
|
117
|
+
if (!effectiveLoader) {
|
|
118
|
+
setOptions([]);
|
|
119
|
+
setLoading(false);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const key = loaderKey(effectiveLoader, search);
|
|
124
|
+
const cached = choicesCache.get(key);
|
|
125
|
+
if (cached && !search) {
|
|
126
|
+
setOptions(cached);
|
|
127
|
+
setLoading(false);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setLoading(true);
|
|
132
|
+
try {
|
|
133
|
+
const result = await loadChoicesShared(
|
|
134
|
+
effectiveLoader,
|
|
135
|
+
dp,
|
|
136
|
+
optionLabel,
|
|
137
|
+
optionValue,
|
|
138
|
+
search,
|
|
139
|
+
);
|
|
140
|
+
setOptions(result);
|
|
141
|
+
} catch {
|
|
142
|
+
setOptions([]);
|
|
143
|
+
} finally {
|
|
144
|
+
setLoading(false);
|
|
145
|
+
}
|
|
146
|
+
}, [effectiveLoader, dp, optionLabel, optionValue, search]);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
void load();
|
|
150
|
+
}, [load]);
|
|
151
|
+
|
|
152
|
+
const labelForValue = useCallback(
|
|
153
|
+
(value: unknown): string => {
|
|
154
|
+
const opt = options.find((o) => o.value === value);
|
|
155
|
+
return opt?.label ?? String(value ?? "—");
|
|
156
|
+
},
|
|
157
|
+
[options],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const labelsForValues = useCallback(
|
|
161
|
+
(values: unknown[]): string => {
|
|
162
|
+
if (!values?.length) return "—";
|
|
163
|
+
return values.map((v) => labelForValue(v)).join(", ");
|
|
164
|
+
},
|
|
165
|
+
[labelForValue],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const optionForValue = useCallback(
|
|
169
|
+
(value: unknown): ChoiceOption | undefined =>
|
|
170
|
+
options.find((o) => o.value === value),
|
|
171
|
+
[options],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
options,
|
|
176
|
+
loading,
|
|
177
|
+
labelForValue,
|
|
178
|
+
labelsForValues,
|
|
179
|
+
optionForValue,
|
|
180
|
+
reload: load,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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): 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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { GetListParams, Identifier } from "./dataProviderTypes";
|
|
2
|
+
import type { ResourceHandlers } from "./resourceHandlers";
|
|
3
|
+
|
|
4
|
+
export type RestResourceHandlersConfig<
|
|
5
|
+
T extends Record<string, unknown> = Record<string, unknown>,
|
|
6
|
+
> = {
|
|
7
|
+
list: (params: GetListParams) => Promise<{ data: T[]; total: number }>;
|
|
8
|
+
retrieve: (id: Identifier) => Promise<T>;
|
|
9
|
+
create: (data: Record<string, unknown>) => Promise<T>;
|
|
10
|
+
update: (id: Identifier, data: Record<string, unknown>) => Promise<T>;
|
|
11
|
+
destroy: (id: Identifier) => Promise<void>;
|
|
12
|
+
/** When API create body differs from form `source` paths. */
|
|
13
|
+
transformCreate?: (data: Record<string, unknown>) => unknown;
|
|
14
|
+
/** When API update body differs from form `source` paths. */
|
|
15
|
+
transformUpdate?: (data: Record<string, unknown>) => unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Thin CRUD glue for REST backends. Form payload comes from field `source` paths;
|
|
20
|
+
* wire your fetch or API client in the five functions below.
|
|
21
|
+
*/
|
|
22
|
+
export function createRestResourceHandlers<
|
|
23
|
+
T extends Record<string, unknown> = Record<string, unknown>,
|
|
24
|
+
>(config: RestResourceHandlersConfig<T>): ResourceHandlers<T> {
|
|
25
|
+
return {
|
|
26
|
+
async getList(params) {
|
|
27
|
+
return config.list(params);
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async getOne(id) {
|
|
31
|
+
return { data: await config.retrieve(id) };
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async create(data) {
|
|
35
|
+
const body = config.transformCreate
|
|
36
|
+
? config.transformCreate(data as Record<string, unknown>)
|
|
37
|
+
: data;
|
|
38
|
+
return {
|
|
39
|
+
data: await config.create(body as Record<string, unknown>),
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async update({ id, data }) {
|
|
44
|
+
const body = config.transformUpdate
|
|
45
|
+
? config.transformUpdate(data as Record<string, unknown>)
|
|
46
|
+
: data;
|
|
47
|
+
return {
|
|
48
|
+
data: await config.update(id, body as Record<string, unknown>),
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async delete(id) {
|
|
53
|
+
await config.destroy(id);
|
|
54
|
+
return { data: null };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** Resource primary key as accepted by `DataProvider` methods. */
|
|
2
|
+
export type Identifier = string | number;
|
|
3
|
+
|
|
4
|
+
export type SortOrder = "ASC" | "DESC";
|
|
5
|
+
|
|
6
|
+
export type SortSpec = {
|
|
7
|
+
field: string;
|
|
8
|
+
order: SortOrder;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type PaginationParams = {
|
|
12
|
+
page: number;
|
|
13
|
+
perPage: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type GetListParams = {
|
|
17
|
+
pagination?: PaginationParams;
|
|
18
|
+
/** Single or multi-column sort. */
|
|
19
|
+
sort?: SortSpec | SortSpec[];
|
|
20
|
+
/** Filter values; arrays mean "match any of" for that field. */
|
|
21
|
+
filter?: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type GetListResult<RecordType extends Record<string, unknown>> = {
|
|
25
|
+
data: RecordType[];
|
|
26
|
+
total: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type GetOneResult<RecordType extends Record<string, unknown>> = {
|
|
30
|
+
data: RecordType;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type CreateResult<RecordType extends Record<string, unknown>> = {
|
|
34
|
+
data: RecordType;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type UpdateParams<RecordType extends Record<string, unknown>> = {
|
|
38
|
+
id: Identifier;
|
|
39
|
+
data: Partial<RecordType>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type UpdateResult<RecordType extends Record<string, unknown>> = {
|
|
43
|
+
data: RecordType;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type DeleteResult<RecordType extends Record<string, unknown>> = {
|
|
47
|
+
data: RecordType | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type FormMutation = "create" | "update";
|
|
51
|
+
|
|
52
|
+
export type ParseFormErrorContext = {
|
|
53
|
+
resource: string;
|
|
54
|
+
mutation: FormMutation;
|
|
55
|
+
/** Set when saving an inline row — prefix field paths for that row. */
|
|
56
|
+
inlineArrayName?: string;
|
|
57
|
+
rowIndex?: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Field paths match react-hook-form `name` (form `source` or inline `arrayName.index.source`). */
|
|
61
|
+
export type FormValidationErrors = {
|
|
62
|
+
fields?: Record<string, string | string[]>;
|
|
63
|
+
/** Errors not tied to a single field (e.g. non_field_errors). */
|
|
64
|
+
global?: string | string[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type ParseFormError = (
|
|
68
|
+
error: unknown,
|
|
69
|
+
context: ParseFormErrorContext,
|
|
70
|
+
) => FormValidationErrors | null | undefined;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Small, framework-agnostic data layer (react-admin–style names).
|
|
74
|
+
* Implement with REST, GraphQL, or an in-memory mock.
|
|
75
|
+
*/
|
|
76
|
+
export type DataProvider<
|
|
77
|
+
RecordType extends Record<string, unknown> = Record<string, unknown>,
|
|
78
|
+
> = {
|
|
79
|
+
getList: (
|
|
80
|
+
resource: string,
|
|
81
|
+
params: GetListParams,
|
|
82
|
+
) => Promise<GetListResult<RecordType>>;
|
|
83
|
+
getOne: (
|
|
84
|
+
resource: string,
|
|
85
|
+
id: Identifier,
|
|
86
|
+
) => Promise<GetOneResult<RecordType>>;
|
|
87
|
+
create: (
|
|
88
|
+
resource: string,
|
|
89
|
+
data: Partial<RecordType>,
|
|
90
|
+
) => Promise<CreateResult<RecordType>>;
|
|
91
|
+
update: (
|
|
92
|
+
resource: string,
|
|
93
|
+
params: UpdateParams<RecordType>,
|
|
94
|
+
) => Promise<UpdateResult<RecordType>>;
|
|
95
|
+
delete: (
|
|
96
|
+
resource: string,
|
|
97
|
+
id: Identifier,
|
|
98
|
+
) => Promise<DeleteResult<RecordType>>;
|
|
99
|
+
/** Map API validation errors to form field paths. Optional. */
|
|
100
|
+
parseFormError?: ParseFormError;
|
|
101
|
+
};
|