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,38 @@
|
|
|
1
|
+
import { Select } from "antd";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import type { BaseSourceProps } from "../types";
|
|
4
|
+
import { useRegisterFilter } from "../context/FilterContext";
|
|
5
|
+
|
|
6
|
+
export type BooleanFilterProps = BaseSourceProps;
|
|
7
|
+
|
|
8
|
+
export function BooleanFilter({ source, label }: BooleanFilterProps) {
|
|
9
|
+
const def = useMemo(
|
|
10
|
+
() => ({
|
|
11
|
+
key: source,
|
|
12
|
+
source,
|
|
13
|
+
label,
|
|
14
|
+
render: ({
|
|
15
|
+
value,
|
|
16
|
+
onChange,
|
|
17
|
+
}: {
|
|
18
|
+
value: unknown;
|
|
19
|
+
onChange: (value: unknown) => void;
|
|
20
|
+
}) => (
|
|
21
|
+
<Select
|
|
22
|
+
allowClear
|
|
23
|
+
placeholder={label ?? source}
|
|
24
|
+
value={value as boolean | undefined}
|
|
25
|
+
onChange={(v) => onChange(v)}
|
|
26
|
+
options={[
|
|
27
|
+
{ label: "Yes", value: true },
|
|
28
|
+
{ label: "No", value: false },
|
|
29
|
+
]}
|
|
30
|
+
style={{ minWidth: 100 }}
|
|
31
|
+
/>
|
|
32
|
+
),
|
|
33
|
+
}),
|
|
34
|
+
[source, label],
|
|
35
|
+
);
|
|
36
|
+
useRegisterFilter(def);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DatePicker } from "antd";
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import type { BaseSourceProps } from "../types";
|
|
5
|
+
import { useRegisterFilter } from "../context/FilterContext";
|
|
6
|
+
|
|
7
|
+
export type DateFilterProps = BaseSourceProps;
|
|
8
|
+
|
|
9
|
+
export function DateFilter({ source, label }: DateFilterProps) {
|
|
10
|
+
const def = useMemo(
|
|
11
|
+
() => ({
|
|
12
|
+
key: source,
|
|
13
|
+
source,
|
|
14
|
+
label,
|
|
15
|
+
render: ({
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
}: {
|
|
19
|
+
value: unknown;
|
|
20
|
+
onChange: (value: unknown) => void;
|
|
21
|
+
}) => (
|
|
22
|
+
<DatePicker
|
|
23
|
+
allowClear
|
|
24
|
+
placeholder={label ?? source}
|
|
25
|
+
value={value ? dayjs(String(value)) : null}
|
|
26
|
+
onChange={(d) => onChange(d ? d.format("YYYY-MM-DD") : undefined)}
|
|
27
|
+
style={{ minWidth: 160 }}
|
|
28
|
+
/>
|
|
29
|
+
),
|
|
30
|
+
}),
|
|
31
|
+
[source, label],
|
|
32
|
+
);
|
|
33
|
+
useRegisterFilter(def);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { InputNumber } from "antd";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import type { BaseSourceProps } from "../types";
|
|
4
|
+
import { useRegisterFilter } from "../context/FilterContext";
|
|
5
|
+
|
|
6
|
+
export type NumberFilterProps = BaseSourceProps;
|
|
7
|
+
|
|
8
|
+
export function NumberFilter({ source, label }: NumberFilterProps) {
|
|
9
|
+
const def = useMemo(
|
|
10
|
+
() => ({
|
|
11
|
+
key: source,
|
|
12
|
+
source,
|
|
13
|
+
label,
|
|
14
|
+
render: ({
|
|
15
|
+
value,
|
|
16
|
+
onChange,
|
|
17
|
+
}: {
|
|
18
|
+
value: unknown;
|
|
19
|
+
onChange: (value: unknown) => void;
|
|
20
|
+
}) => (
|
|
21
|
+
<InputNumber
|
|
22
|
+
placeholder={label ?? source}
|
|
23
|
+
value={value as number | undefined}
|
|
24
|
+
onChange={(v) => onChange(v ?? undefined)}
|
|
25
|
+
style={{ minWidth: 120 }}
|
|
26
|
+
/>
|
|
27
|
+
),
|
|
28
|
+
}),
|
|
29
|
+
[source, label],
|
|
30
|
+
);
|
|
31
|
+
useRegisterFilter(def);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Select } from "antd";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import type { BaseSourceProps, ChoiceOption, ReferenceProps } from "../types";
|
|
4
|
+
import { useRegisterFilter } from "../context/FilterContext";
|
|
5
|
+
import { useChoices } from "../utils/useChoices";
|
|
6
|
+
|
|
7
|
+
export type SelectFilterProps = BaseSourceProps & {
|
|
8
|
+
choices: ChoiceOption[];
|
|
9
|
+
multiple?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function SelectFilterInput({
|
|
13
|
+
source,
|
|
14
|
+
label,
|
|
15
|
+
choices,
|
|
16
|
+
multiple,
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
}: SelectFilterProps & {
|
|
20
|
+
value: unknown;
|
|
21
|
+
onChange: (value: unknown) => void;
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<Select
|
|
25
|
+
allowClear
|
|
26
|
+
mode={multiple ? "multiple" : undefined}
|
|
27
|
+
placeholder={label ?? source}
|
|
28
|
+
value={value as string | number | (string | number)[] | undefined}
|
|
29
|
+
onChange={onChange}
|
|
30
|
+
options={choices}
|
|
31
|
+
style={{ minWidth: 160 }}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function SelectFilter({
|
|
37
|
+
source,
|
|
38
|
+
label,
|
|
39
|
+
choices,
|
|
40
|
+
multiple,
|
|
41
|
+
}: SelectFilterProps) {
|
|
42
|
+
const def = useMemo(
|
|
43
|
+
() => ({
|
|
44
|
+
key: source,
|
|
45
|
+
source,
|
|
46
|
+
label,
|
|
47
|
+
render: ({
|
|
48
|
+
value,
|
|
49
|
+
onChange,
|
|
50
|
+
}: {
|
|
51
|
+
value: unknown;
|
|
52
|
+
onChange: (value: unknown) => void;
|
|
53
|
+
}) => (
|
|
54
|
+
<SelectFilterInput
|
|
55
|
+
source={source}
|
|
56
|
+
label={label}
|
|
57
|
+
choices={choices}
|
|
58
|
+
multiple={multiple}
|
|
59
|
+
value={value}
|
|
60
|
+
onChange={onChange}
|
|
61
|
+
/>
|
|
62
|
+
),
|
|
63
|
+
}),
|
|
64
|
+
[source, label, choices, multiple],
|
|
65
|
+
);
|
|
66
|
+
useRegisterFilter(def);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type ReferenceFilterProps = BaseSourceProps &
|
|
71
|
+
ReferenceProps & {
|
|
72
|
+
multiple?: boolean;
|
|
73
|
+
search?: boolean;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function ReferenceFilterInput({
|
|
77
|
+
source,
|
|
78
|
+
label,
|
|
79
|
+
reference,
|
|
80
|
+
choices,
|
|
81
|
+
optionLabel,
|
|
82
|
+
optionValue,
|
|
83
|
+
multiple,
|
|
84
|
+
search,
|
|
85
|
+
value,
|
|
86
|
+
onChange,
|
|
87
|
+
}: ReferenceFilterProps & {
|
|
88
|
+
value: unknown;
|
|
89
|
+
onChange: (value: unknown) => void;
|
|
90
|
+
}) {
|
|
91
|
+
const [searchText, setSearchText] = useState<string | undefined>();
|
|
92
|
+
const { options, loading } = useChoices(
|
|
93
|
+
choices,
|
|
94
|
+
reference,
|
|
95
|
+
optionLabel,
|
|
96
|
+
optionValue,
|
|
97
|
+
search ? searchText : undefined,
|
|
98
|
+
);
|
|
99
|
+
return (
|
|
100
|
+
<Select
|
|
101
|
+
allowClear
|
|
102
|
+
mode={multiple ? "multiple" : undefined}
|
|
103
|
+
placeholder={label ?? source}
|
|
104
|
+
value={value as string | number | (string | number)[] | undefined}
|
|
105
|
+
onChange={onChange}
|
|
106
|
+
options={options.map((o) => ({
|
|
107
|
+
label: o.label,
|
|
108
|
+
value: o.value as string | number,
|
|
109
|
+
}))}
|
|
110
|
+
loading={loading}
|
|
111
|
+
showSearch={search}
|
|
112
|
+
filterOption={search ? false : undefined}
|
|
113
|
+
onSearch={search ? setSearchText : undefined}
|
|
114
|
+
optionFilterProp="label"
|
|
115
|
+
style={{ minWidth: 180 }}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function ReferenceFilter({
|
|
121
|
+
source,
|
|
122
|
+
label,
|
|
123
|
+
reference,
|
|
124
|
+
choices,
|
|
125
|
+
optionLabel = "name",
|
|
126
|
+
optionValue = "id",
|
|
127
|
+
multiple,
|
|
128
|
+
search,
|
|
129
|
+
}: ReferenceFilterProps) {
|
|
130
|
+
const def = useMemo(
|
|
131
|
+
() => ({
|
|
132
|
+
key: source,
|
|
133
|
+
source,
|
|
134
|
+
label,
|
|
135
|
+
render: ({
|
|
136
|
+
value,
|
|
137
|
+
onChange,
|
|
138
|
+
}: {
|
|
139
|
+
value: unknown;
|
|
140
|
+
onChange: (value: unknown) => void;
|
|
141
|
+
}) => (
|
|
142
|
+
<ReferenceFilterInput
|
|
143
|
+
source={source}
|
|
144
|
+
label={label}
|
|
145
|
+
reference={reference}
|
|
146
|
+
choices={choices}
|
|
147
|
+
optionLabel={optionLabel}
|
|
148
|
+
optionValue={optionValue}
|
|
149
|
+
multiple={multiple}
|
|
150
|
+
search={search}
|
|
151
|
+
value={value}
|
|
152
|
+
onChange={onChange}
|
|
153
|
+
/>
|
|
154
|
+
),
|
|
155
|
+
}),
|
|
156
|
+
[
|
|
157
|
+
source,
|
|
158
|
+
label,
|
|
159
|
+
reference,
|
|
160
|
+
choices,
|
|
161
|
+
optionLabel,
|
|
162
|
+
optionValue,
|
|
163
|
+
multiple,
|
|
164
|
+
search,
|
|
165
|
+
],
|
|
166
|
+
);
|
|
167
|
+
useRegisterFilter(def);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function ReferenceManyFilter(props: ReferenceFilterProps) {
|
|
172
|
+
return <ReferenceFilter {...props} multiple />;
|
|
173
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Select } from "antd";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import type { BaseSourceProps, ChoiceOption } from "../types";
|
|
4
|
+
import { useRegisterFilter } from "../context/FilterContext";
|
|
5
|
+
|
|
6
|
+
export type SelectFilterProps = BaseSourceProps & {
|
|
7
|
+
choices: ChoiceOption[];
|
|
8
|
+
multiple?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function SelectFilter({
|
|
12
|
+
source,
|
|
13
|
+
label,
|
|
14
|
+
choices,
|
|
15
|
+
multiple,
|
|
16
|
+
}: SelectFilterProps) {
|
|
17
|
+
const def = useMemo(
|
|
18
|
+
() => ({
|
|
19
|
+
key: source,
|
|
20
|
+
source,
|
|
21
|
+
label,
|
|
22
|
+
render: ({
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
}: {
|
|
26
|
+
value: unknown;
|
|
27
|
+
onChange: (value: unknown) => void;
|
|
28
|
+
}) => (
|
|
29
|
+
<Select
|
|
30
|
+
allowClear
|
|
31
|
+
mode={multiple ? "multiple" : undefined}
|
|
32
|
+
placeholder={label ?? source}
|
|
33
|
+
value={value as string | number | (string | number)[] | undefined}
|
|
34
|
+
onChange={onChange}
|
|
35
|
+
options={choices}
|
|
36
|
+
style={{ minWidth: 160 }}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
}),
|
|
40
|
+
[source, label, choices, multiple],
|
|
41
|
+
);
|
|
42
|
+
useRegisterFilter(def);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Input } from "antd";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import type { BaseSourceProps } from "../types";
|
|
4
|
+
import { useRegisterFilter } from "../context/FilterContext";
|
|
5
|
+
|
|
6
|
+
export type TextFilterProps = BaseSourceProps & {
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function TextFilter({ source, label, placeholder }: TextFilterProps) {
|
|
11
|
+
const def = useMemo(
|
|
12
|
+
() => ({
|
|
13
|
+
key: source,
|
|
14
|
+
source,
|
|
15
|
+
label,
|
|
16
|
+
render: ({
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
}: {
|
|
20
|
+
value: unknown;
|
|
21
|
+
onChange: (value: unknown) => void;
|
|
22
|
+
}) => (
|
|
23
|
+
<Input
|
|
24
|
+
allowClear
|
|
25
|
+
placeholder={placeholder ?? label ?? source}
|
|
26
|
+
value={(value as string) ?? ""}
|
|
27
|
+
onChange={(e) => onChange(e.target.value || undefined)}
|
|
28
|
+
style={{ minWidth: 160 }}
|
|
29
|
+
/>
|
|
30
|
+
),
|
|
31
|
+
}),
|
|
32
|
+
[source, label, placeholder],
|
|
33
|
+
);
|
|
34
|
+
useRegisterFilter(def);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export { ResourceList, useResourceListContext } from "./ResourceList";
|
|
2
|
+
export type { ResourceListProps } from "./ResourceList";
|
|
3
|
+
export { FilterBar } from "./FilterBar";
|
|
4
|
+
export { ResourceForm } from "./ResourceForm";
|
|
5
|
+
export type {
|
|
6
|
+
ResourceFormProps,
|
|
7
|
+
ResourceFormInlineConfig,
|
|
8
|
+
} from "./ResourceForm";
|
|
9
|
+
export { ResourceFormModal } from "./ResourceFormModal";
|
|
10
|
+
export type { ResourceFormModalProps } from "./ResourceFormModal";
|
|
11
|
+
export {
|
|
12
|
+
InlineFormSet,
|
|
13
|
+
saveInlineRows,
|
|
14
|
+
loadInlineRows,
|
|
15
|
+
} from "./InlineFormSet";
|
|
16
|
+
export type { InlineFormSetProps, SaveInlineOptions, InlineFormSetLayout } from "./InlineFormSet";
|
|
17
|
+
export { FormTabs, FormTab } from "./FormTabs";
|
|
18
|
+
export type { FormTabsProps, FormTabProps } from "./FormTabs";
|
|
19
|
+
export { FormSteps, FormStep } from "./FormSteps";
|
|
20
|
+
export type { FormStepsProps, FormStepProps } from "./FormSteps";
|
|
21
|
+
|
|
22
|
+
export { TextField } from "./fields/TextField";
|
|
23
|
+
export { NumberField } from "./fields/NumberField";
|
|
24
|
+
export { BooleanField } from "./fields/BooleanField";
|
|
25
|
+
export { DateField } from "./fields/DateField";
|
|
26
|
+
export { SelectField } from "./fields/SelectField";
|
|
27
|
+
export { PasswordField } from "./fields/PasswordField";
|
|
28
|
+
export { ReferenceField } from "./fields/ReferenceField";
|
|
29
|
+
export { ReferenceManyField } from "./fields/ReferenceManyField";
|
|
30
|
+
|
|
31
|
+
export { TextColumn } from "./columns/TextColumn";
|
|
32
|
+
export { NumberColumn } from "./columns/NumberColumn";
|
|
33
|
+
export { BooleanColumn } from "./columns/BooleanColumn";
|
|
34
|
+
export { DateColumn } from "./columns/DateColumn";
|
|
35
|
+
export { ReferenceColumn } from "./columns/ReferenceColumn";
|
|
36
|
+
export { ReferenceManyColumn } from "./columns/ReferenceManyColumn";
|
|
37
|
+
export { CustomColumn } from "./columns/CustomColumn";
|
|
38
|
+
|
|
39
|
+
export { TextFilter } from "./filters/TextFilter";
|
|
40
|
+
export { NumberFilter } from "./filters/NumberFilter";
|
|
41
|
+
export { BooleanFilter } from "./filters/BooleanFilter";
|
|
42
|
+
export { DateFilter } from "./filters/DateFilter";
|
|
43
|
+
export { SelectFilter } from "./filters/SelectFilter";
|
|
44
|
+
export {
|
|
45
|
+
ReferenceFilter,
|
|
46
|
+
ReferenceManyFilter,
|
|
47
|
+
} from "./filters/ReferenceFilter";
|
|
48
|
+
|
|
49
|
+
export { useListQueryState } from "./utils/useListQueryState";
|
|
50
|
+
export type {
|
|
51
|
+
ListQueryState,
|
|
52
|
+
ListQueryActions,
|
|
53
|
+
} from "./utils/useListQueryState";
|
|
54
|
+
export { useChoices } from "./utils/useChoices";
|
|
55
|
+
export { getByPath } from "./utils/getByPath";
|
|
56
|
+
export { pickBySources } from "./utils/pickBySources";
|
|
57
|
+
export { inlineArrayName } from "./utils/inlineArrayName";
|
|
58
|
+
export { setByPath } from "./utils/setByPath";
|
|
59
|
+
export { useRegisterFormSource } from "./context/FormFieldsContext";
|
|
60
|
+
|
|
61
|
+
export type {
|
|
62
|
+
BaseSourceProps,
|
|
63
|
+
ChoiceOption,
|
|
64
|
+
ChoicesLoader,
|
|
65
|
+
ReferenceProps,
|
|
66
|
+
DisplayProps,
|
|
67
|
+
EditMode,
|
|
68
|
+
FieldRules,
|
|
69
|
+
ResourceListBuiltInActions,
|
|
70
|
+
ResourceListBulkAction,
|
|
71
|
+
ResourceListBulkActionHelpers,
|
|
72
|
+
ResourceListRowActionsHelpers,
|
|
73
|
+
} from "./types";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ColumnsType } from "antd/es/table";
|
|
3
|
+
import type { DataProvider } from "../data/dataProviderTypes";
|
|
4
|
+
import type { RegisterOptions } from "react-hook-form";
|
|
5
|
+
|
|
6
|
+
export type BaseSourceProps = {
|
|
7
|
+
source: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ChoiceOption = {
|
|
12
|
+
label: string;
|
|
13
|
+
value: unknown;
|
|
14
|
+
/** Full record when loaded from a reference resource (for dependent fields). */
|
|
15
|
+
record?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ChoicesContext = {
|
|
19
|
+
dataProvider: DataProvider;
|
|
20
|
+
search?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ChoicesLoader =
|
|
24
|
+
| { resource: string; filter?: Record<string, unknown> }
|
|
25
|
+
| ChoiceOption[]
|
|
26
|
+
| ((ctx: ChoicesContext) => Promise<ChoiceOption[]>);
|
|
27
|
+
|
|
28
|
+
export type ReferenceProps = {
|
|
29
|
+
reference?: string;
|
|
30
|
+
choices?: ChoicesLoader;
|
|
31
|
+
optionLabel?: string | ((record: Record<string, unknown>) => string);
|
|
32
|
+
optionValue?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type DisplayProps = {
|
|
36
|
+
display?: string | ((record: Record<string, unknown>) => ReactNode);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type FieldRules = RegisterOptions;
|
|
40
|
+
|
|
41
|
+
export type ColumnDefinition<T extends Record<string, unknown>> = {
|
|
42
|
+
key: string;
|
|
43
|
+
source: string;
|
|
44
|
+
label?: string;
|
|
45
|
+
sortable?: boolean;
|
|
46
|
+
buildColumn: () => ColumnsType<T>[number];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type FilterDefinition = {
|
|
50
|
+
key: string;
|
|
51
|
+
source: string;
|
|
52
|
+
label?: string;
|
|
53
|
+
render: (props: {
|
|
54
|
+
value: unknown;
|
|
55
|
+
onChange: (value: unknown) => void;
|
|
56
|
+
}) => ReactNode;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type InlineFieldDefinition = {
|
|
60
|
+
key: string;
|
|
61
|
+
source: string;
|
|
62
|
+
label?: string;
|
|
63
|
+
/** Column width in tabular inline tables (Ant Design Table `width`). */
|
|
64
|
+
width?: number | string;
|
|
65
|
+
/** Minimum column width in tabular inline tables. */
|
|
66
|
+
minWidth?: number | string;
|
|
67
|
+
render: (props: {
|
|
68
|
+
name: string;
|
|
69
|
+
index: number;
|
|
70
|
+
}) => ReactNode;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type InlineFieldOptions = {
|
|
74
|
+
width?: number | string;
|
|
75
|
+
minWidth?: number | string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type FieldRenderProps = {
|
|
79
|
+
value: unknown;
|
|
80
|
+
onChange: (value: unknown) => void;
|
|
81
|
+
onBlur: () => void;
|
|
82
|
+
disabled?: boolean;
|
|
83
|
+
/** Set when rendered inside `InlineFormSet`. */
|
|
84
|
+
name?: string;
|
|
85
|
+
index?: number;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type EditMode = "page" | "modal" | "both";
|
|
89
|
+
|
|
90
|
+
/** Toggle built-in row actions. Omitted keys default to `true` (still gated by permissions). */
|
|
91
|
+
export type ResourceListBuiltInActions = {
|
|
92
|
+
edit?: boolean;
|
|
93
|
+
quickEdit?: boolean;
|
|
94
|
+
delete?: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type ResourceListRowActionsHelpers = {
|
|
98
|
+
reload: () => void;
|
|
99
|
+
openEditModal: (id: string | number) => void;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type ResourceListBulkActionHelpers = {
|
|
103
|
+
reload: () => void;
|
|
104
|
+
clearSelection: () => void;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Bulk list action (Django admin–style). Runs on selected row ids. */
|
|
108
|
+
export type ResourceListBulkAction = {
|
|
109
|
+
key: string;
|
|
110
|
+
label: string;
|
|
111
|
+
/** Optional confirmation message. Return false to cancel. */
|
|
112
|
+
confirm?:
|
|
113
|
+
| string
|
|
114
|
+
| ((
|
|
115
|
+
selectedIds: (string | number)[],
|
|
116
|
+
helpers: ResourceListBulkActionHelpers,
|
|
117
|
+
) => string | false | Promise<string | false>);
|
|
118
|
+
execute: (
|
|
119
|
+
selectedIds: (string | number)[],
|
|
120
|
+
helpers: ResourceListBulkActionHelpers,
|
|
121
|
+
) => void | Promise<void>;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const LIST_QUERY_RESERVED = new Set([
|
|
125
|
+
"page",
|
|
126
|
+
"perPage",
|
|
127
|
+
"sort",
|
|
128
|
+
"create",
|
|
129
|
+
"edit",
|
|
130
|
+
]);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { MessageInstance } from "antd/es/message/interface";
|
|
2
|
+
import type { FieldPath, FieldValues, UseFormReturn } from "react-hook-form";
|
|
3
|
+
import type { DataProvider, ParseFormErrorContext } from "../../data/dataProviderTypes";
|
|
4
|
+
|
|
5
|
+
function toMessages(value: string | string[] | undefined): string[] {
|
|
6
|
+
if (!value) return [];
|
|
7
|
+
return Array.isArray(value) ? value : [value];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function applyFormErrors<T extends FieldValues>(
|
|
11
|
+
form: UseFormReturn<T>,
|
|
12
|
+
parsed: { fields?: Record<string, string | string[]>; global?: string | string[] },
|
|
13
|
+
message: MessageInstance,
|
|
14
|
+
) {
|
|
15
|
+
for (const msg of toMessages(parsed.global)) {
|
|
16
|
+
message.error(msg);
|
|
17
|
+
}
|
|
18
|
+
for (const [path, msg] of Object.entries(parsed.fields ?? {})) {
|
|
19
|
+
const text = Array.isArray(msg) ? msg.join(", ") : msg;
|
|
20
|
+
form.setError(path as FieldPath<T>, { type: "server", message: text });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseAndApplyFormErrors<T extends FieldValues>(
|
|
25
|
+
dp: DataProvider,
|
|
26
|
+
form: UseFormReturn<T>,
|
|
27
|
+
message: MessageInstance,
|
|
28
|
+
error: unknown,
|
|
29
|
+
context: ParseFormErrorContext,
|
|
30
|
+
): boolean {
|
|
31
|
+
const parsed = dp.parseFormError?.(error, context);
|
|
32
|
+
if (!parsed) return false;
|
|
33
|
+
const hasFields = Object.keys(parsed.fields ?? {}).length > 0;
|
|
34
|
+
const hasGlobal = toMessages(parsed.global).length > 0;
|
|
35
|
+
if (!hasFields && !hasGlobal) return false;
|
|
36
|
+
applyFormErrors(form, parsed, message);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useEffect, useRef, type MutableRefObject } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useFormContext,
|
|
4
|
+
useFormState,
|
|
5
|
+
type FieldPath,
|
|
6
|
+
type FieldValues,
|
|
7
|
+
type FormState,
|
|
8
|
+
type UseFormGetFieldState,
|
|
9
|
+
} from "react-hook-form";
|
|
10
|
+
|
|
11
|
+
export function sectionHasError<T extends FieldValues>(
|
|
12
|
+
sources: Set<string>,
|
|
13
|
+
getFieldState: UseFormGetFieldState<T>,
|
|
14
|
+
formState: FormState<T>,
|
|
15
|
+
): boolean {
|
|
16
|
+
for (const source of sources) {
|
|
17
|
+
if (getFieldState(source as FieldPath<T>, formState).invalid) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useSectionSourceRefs(count: number) {
|
|
25
|
+
const refs = useRef<MutableRefObject<Set<string>>[]>([]);
|
|
26
|
+
|
|
27
|
+
while (refs.current.length < count) {
|
|
28
|
+
refs.current.push({ current: new Set() });
|
|
29
|
+
}
|
|
30
|
+
if (refs.current.length > count) {
|
|
31
|
+
refs.current.length = count;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return refs.current;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useActivateFirstErrorSection(
|
|
38
|
+
sectionRefs: MutableRefObject<Set<string>>[],
|
|
39
|
+
activate: (index: number) => void,
|
|
40
|
+
) {
|
|
41
|
+
const { control, getFieldState, setFocus } = useFormContext<FieldValues>();
|
|
42
|
+
const formState = useFormState({ control });
|
|
43
|
+
const prevSubmitCount = useRef(0);
|
|
44
|
+
const prevErrorCount = useRef(0);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (formState.submitCount === 0) return;
|
|
48
|
+
|
|
49
|
+
const errorCount = Object.keys(formState.errors).length;
|
|
50
|
+
const submitChanged = formState.submitCount !== prevSubmitCount.current;
|
|
51
|
+
const serverErrorsArrived =
|
|
52
|
+
!submitChanged && errorCount > 0 && prevErrorCount.current === 0;
|
|
53
|
+
|
|
54
|
+
prevSubmitCount.current = formState.submitCount;
|
|
55
|
+
prevErrorCount.current = errorCount;
|
|
56
|
+
|
|
57
|
+
if ((!submitChanged && !serverErrorsArrived) || errorCount === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const first = sectionRefs.findIndex((ref) =>
|
|
62
|
+
sectionHasError(ref.current, getFieldState, formState),
|
|
63
|
+
);
|
|
64
|
+
if (first < 0) return;
|
|
65
|
+
|
|
66
|
+
activate(first);
|
|
67
|
+
|
|
68
|
+
const firstInvalid = [...sectionRefs[first].current].find((source) =>
|
|
69
|
+
getFieldState(source as FieldPath<FieldValues>, formState).invalid,
|
|
70
|
+
);
|
|
71
|
+
if (firstInvalid) {
|
|
72
|
+
requestAnimationFrame(() => {
|
|
73
|
+
void setFocus(firstInvalid as FieldPath<FieldValues>);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}, [
|
|
77
|
+
activate,
|
|
78
|
+
formState,
|
|
79
|
+
formState.errors,
|
|
80
|
+
formState.submitCount,
|
|
81
|
+
getFieldState,
|
|
82
|
+
sectionRefs,
|
|
83
|
+
setFocus,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Read nested value from a record using dot notation, e.g. `brand.name`. */
|
|
2
|
+
export function getByPath(
|
|
3
|
+
record: Record<string, unknown>,
|
|
4
|
+
path: string,
|
|
5
|
+
): unknown {
|
|
6
|
+
const parts = path.split(".");
|
|
7
|
+
let cur: unknown = record;
|
|
8
|
+
for (const part of parts) {
|
|
9
|
+
if (cur == null || typeof cur !== "object") return undefined;
|
|
10
|
+
cur = (cur as Record<string, unknown>)[part];
|
|
11
|
+
}
|
|
12
|
+
return cur;
|
|
13
|
+
}
|