ding-react-admin 2.0.8 → 2.1.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.
Files changed (81) hide show
  1. package/README.md +1 -1
  2. package/dist/index.css +1 -1
  3. package/dist/index.js +1604 -1119
  4. package/dist/src/components/AppHub.d.ts +8 -0
  5. package/dist/src/components/AppHub.d.ts.map +1 -0
  6. package/dist/src/components/AppLauncherButton.d.ts +7 -0
  7. package/dist/src/components/AppLauncherButton.d.ts.map +1 -0
  8. package/dist/src/components/index.d.ts +2 -0
  9. package/dist/src/components/index.d.ts.map +1 -1
  10. package/dist/src/crud/InlineFormSet.d.ts +6 -2
  11. package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
  12. package/dist/src/crud/ResourceList.d.ts +6 -1
  13. package/dist/src/crud/ResourceList.d.ts.map +1 -1
  14. package/dist/src/crud/context/FilterContext.d.ts +4 -1
  15. package/dist/src/crud/context/FilterContext.d.ts.map +1 -1
  16. package/dist/src/crud/fields/BooleanField.d.ts +2 -1
  17. package/dist/src/crud/fields/BooleanField.d.ts.map +1 -1
  18. package/dist/src/crud/fields/DateField.d.ts.map +1 -1
  19. package/dist/src/crud/fields/DateTimeField.d.ts +5 -0
  20. package/dist/src/crud/fields/DateTimeField.d.ts.map +1 -0
  21. package/dist/src/crud/fields/ReferenceField.d.ts +3 -1
  22. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  23. package/dist/src/crud/fields/ReferenceManyField.d.ts +4 -1
  24. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  25. package/dist/src/crud/fields/TextAreaField.d.ts +19 -0
  26. package/dist/src/crud/fields/TextAreaField.d.ts.map +1 -0
  27. package/dist/src/crud/fields/TimeField.d.ts +11 -0
  28. package/dist/src/crud/fields/TimeField.d.ts.map +1 -0
  29. package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
  30. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
  31. package/dist/src/crud/filters/TextFilter.d.ts +9 -1
  32. package/dist/src/crud/filters/TextFilter.d.ts.map +1 -1
  33. package/dist/src/crud/index.d.ts +6 -1
  34. package/dist/src/crud/index.d.ts.map +1 -1
  35. package/dist/src/crud/types.d.ts +9 -0
  36. package/dist/src/crud/types.d.ts.map +1 -1
  37. package/dist/src/crud/utils/parseDayjsValue.d.ts +3 -0
  38. package/dist/src/crud/utils/parseDayjsValue.d.ts.map +1 -0
  39. package/dist/src/crud/utils/referenceSelectDropdownProps.d.ts +11 -0
  40. package/dist/src/crud/utils/referenceSelectDropdownProps.d.ts.map +1 -0
  41. package/dist/src/crud/utils/useDebouncedValue.d.ts +3 -0
  42. package/dist/src/crud/utils/useDebouncedValue.d.ts.map +1 -0
  43. package/dist/src/index.d.ts +4 -4
  44. package/dist/src/index.d.ts.map +1 -1
  45. package/dist/src/layouts/AdminLayout.d.ts +1 -1
  46. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  47. package/dist/src/layouts/navMenuItems.d.ts +3 -0
  48. package/dist/src/layouts/navMenuItems.d.ts.map +1 -1
  49. package/dist/src/router/createAdminRouter.d.ts.map +1 -1
  50. package/dist/src/types.d.ts +23 -0
  51. package/dist/src/types.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/src/components/AppHub.tsx +47 -0
  54. package/src/components/AppLauncherButton.tsx +33 -0
  55. package/src/components/NavMenuLabel.tsx +1 -1
  56. package/src/components/index.ts +2 -0
  57. package/src/components/navMenu.css +107 -0
  58. package/src/context/AppThemeProvider.tsx +1 -1
  59. package/src/crud/InlineFormSet.tsx +32 -14
  60. package/src/crud/ResourceList.tsx +211 -35
  61. package/src/crud/context/FilterContext.tsx +7 -1
  62. package/src/crud/fields/BooleanField.tsx +3 -1
  63. package/src/crud/fields/DateField.tsx +6 -4
  64. package/src/crud/fields/DateTimeField.tsx +9 -0
  65. package/src/crud/fields/ReferenceField.tsx +10 -0
  66. package/src/crud/fields/ReferenceManyField.tsx +13 -1
  67. package/src/crud/fields/TextAreaField.tsx +58 -0
  68. package/src/crud/fields/TimeField.tsx +48 -0
  69. package/src/crud/filters/ReferenceFilter.tsx +10 -0
  70. package/src/crud/filters/TextFilter.tsx +80 -9
  71. package/src/crud/index.ts +9 -1
  72. package/src/crud/resourceListTable.css +113 -0
  73. package/src/crud/types.ts +9 -0
  74. package/src/crud/utils/parseDayjsValue.ts +14 -0
  75. package/src/crud/utils/referenceSelectDropdownProps.ts +30 -0
  76. package/src/crud/utils/useDebouncedValue.ts +17 -0
  77. package/src/index.ts +11 -0
  78. package/src/layouts/AdminLayout.tsx +47 -9
  79. package/src/layouts/navMenuItems.tsx +11 -2
  80. package/src/router/createAdminRouter.tsx +3 -1
  81. package/src/types.ts +24 -0
@@ -8,12 +8,15 @@ import {
8
8
  type ReactNode,
9
9
  } from "react";
10
10
  import type { FilterDefinition } from "../types";
11
+ import { DEFAULT_TEXT_FILTER_DEBOUNCE_MS } from "../filters/TextFilter";
11
12
 
12
13
  type FilterContextValue = {
13
14
  registerFilter: (def: FilterDefinition) => () => void;
14
15
  filters: FilterDefinition[];
15
16
  values: Record<string, unknown>;
16
17
  setFilterValue: (source: string, value: unknown) => void;
18
+ /** Default debounce for `TextFilter` controls on this list (ms). */
19
+ textFilterDebounceMs: number;
17
20
  };
18
21
 
19
22
  const FilterContext = createContext<FilterContextValue | null>(null);
@@ -22,10 +25,12 @@ export function FilterContextProvider({
22
25
  children,
23
26
  values,
24
27
  setFilterValue,
28
+ textFilterDebounceMs = DEFAULT_TEXT_FILTER_DEBOUNCE_MS,
25
29
  }: {
26
30
  children: ReactNode;
27
31
  values: Record<string, unknown>;
28
32
  setFilterValue: (source: string, value: unknown) => void;
33
+ textFilterDebounceMs?: number;
29
34
  }) {
30
35
  const [filters, setFilters] = useState<FilterDefinition[]>([]);
31
36
 
@@ -49,8 +54,9 @@ export function FilterContextProvider({
49
54
  values,
50
55
  setFilterValue,
51
56
  registerFilter,
57
+ textFilterDebounceMs,
52
58
  }),
53
- [filters, values, setFilterValue, registerFilter],
59
+ [filters, values, setFilterValue, registerFilter, textFilterDebounceMs],
54
60
  );
55
61
 
56
62
  return (
@@ -7,6 +7,7 @@ export type BooleanFieldProps = BaseSourceProps & {
7
7
  required?: boolean;
8
8
  rules?: FieldRules;
9
9
  hideLabel?: boolean;
10
+ disabled?: boolean;
10
11
  };
11
12
 
12
13
  export function BooleanField({
@@ -16,6 +17,7 @@ export function BooleanField({
16
17
  required,
17
18
  rules,
18
19
  hideLabel,
20
+ disabled: disabledProp,
19
21
  }: BooleanFieldProps) {
20
22
  return (
21
23
  <FieldWrapper
@@ -30,7 +32,7 @@ export function BooleanField({
30
32
  <Switch
31
33
  checked={Boolean(value)}
32
34
  onChange={onChange}
33
- disabled={disabled}
35
+ disabled={disabled || disabledProp}
34
36
  />
35
37
  )}
36
38
  </FieldWrapper>
@@ -1,9 +1,11 @@
1
1
  import { DatePicker } from "antd";
2
- import dayjs from "dayjs";
3
2
  import type { BaseSourceProps, FieldRules } from "../types";
3
+ import { parseDayjsValue } from "../utils/parseDayjsValue";
4
4
  import { FieldWrapper } from "./FieldWrapper";
5
5
 
6
6
  const DATE_FORMAT = "YYYY-MM-DD";
7
+ const DATE_TIME_FORMAT = `${DATE_FORMAT} HH:mm:ss`;
8
+ const PARSE_FORMATS = [DATE_FORMAT, DATE_TIME_FORMAT, "YYYY-MM-DDTHH:mm:ss", "YYYY-MM-DDTHH:mm:ssZ"];
7
9
 
8
10
  export type DateFieldProps = BaseSourceProps & {
9
11
  name?: string;
@@ -33,18 +35,18 @@ export function DateField({
33
35
  >
34
36
  {({ value, onChange, onBlur, disabled }) => (
35
37
  <DatePicker
36
- value={value ? dayjs(String(value)) : null}
38
+ value={parseDayjsValue(value, showTime ? [...PARSE_FORMATS, DATE_TIME_FORMAT] : PARSE_FORMATS)}
37
39
  onChange={(d) =>
38
40
  onChange(
39
41
  d
40
- ? d.format(showTime ? `${DATE_FORMAT} HH:mm:ss` : DATE_FORMAT)
42
+ ? d.format(showTime ? DATE_TIME_FORMAT : DATE_FORMAT)
41
43
  : null,
42
44
  )
43
45
  }
44
46
  onBlur={onBlur}
45
47
  showTime={showTime}
46
48
  disabled={disabled}
47
- format={showTime ? `${DATE_FORMAT} HH:mm:ss` : DATE_FORMAT}
49
+ format={showTime ? DATE_TIME_FORMAT : DATE_FORMAT}
48
50
  style={{ width: "100%" }}
49
51
  />
50
52
  )}
@@ -0,0 +1,9 @@
1
+ import type { DateFieldProps } from "./DateField";
2
+ import { DateField } from "./DateField";
3
+
4
+ export type DateTimeFieldProps = Omit<DateFieldProps, "showTime">;
5
+
6
+ /** Shorthand for `<DateField showTime />` — date + time via Ant Design DatePicker. */
7
+ export function DateTimeField(props: DateTimeFieldProps) {
8
+ return <DateField showTime {...props} />;
9
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  ReferenceProps,
10
10
  } from "../types";
11
11
  import { valueAsId, resolveOptionLabel } from "../utils/choiceSelectionUtils";
12
+ import { referenceSelectDropdownProps } from "../utils/referenceSelectDropdownProps";
12
13
  import { referenceSelectNotFoundContent } from "../utils/referenceSelectNotFoundContent";
13
14
  import { useChoices } from "../utils/useChoices";
14
15
  import { FieldWrapper } from "./FieldWrapper";
@@ -42,6 +43,8 @@ export type ReferenceFieldProps = BaseSourceProps &
42
43
  referenceModalWidth?: number;
43
44
  /** When false, hide add/edit buttons. Default true when `referenceForm` is set. */
44
45
  referenceActions?: boolean;
46
+ popupMatchSelectWidth?: boolean | number;
47
+ popupMinWidth?: number;
45
48
  };
46
49
 
47
50
  type ReferenceFieldSelectProps = Omit<
@@ -77,6 +80,8 @@ function ReferenceFieldSelect({
77
80
  referenceDefaultValues,
78
81
  referenceModalWidth,
79
82
  referenceActions = true,
83
+ popupMatchSelectWidth,
84
+ popupMinWidth,
80
85
  }: ReferenceFieldSelectProps) {
81
86
  const [searchText, setSearchText] = useState<string | undefined>();
82
87
  const [dropdownOpen, setDropdownOpen] = useState(false);
@@ -124,6 +129,7 @@ function ReferenceFieldSelect({
124
129
 
125
130
  const select = (
126
131
  <Select
132
+ {...referenceSelectDropdownProps({ popupMatchSelectWidth, popupMinWidth })}
127
133
  value={selectValue}
128
134
  onChange={(next) => {
129
135
  onChange(next);
@@ -192,6 +198,8 @@ export function ReferenceField({
192
198
  referenceDefaultValues,
193
199
  referenceModalWidth,
194
200
  referenceActions = true,
201
+ popupMatchSelectWidth,
202
+ popupMinWidth,
195
203
  }: ReferenceFieldProps) {
196
204
  const embeddedRecord = useWatch({
197
205
  name: recordSource ?? "",
@@ -230,6 +238,8 @@ export function ReferenceField({
230
238
  referenceDefaultValues={referenceDefaultValues}
231
239
  referenceModalWidth={referenceModalWidth}
232
240
  referenceActions={referenceActions}
241
+ popupMatchSelectWidth={popupMatchSelectWidth}
242
+ popupMinWidth={popupMinWidth}
233
243
  />
234
244
  )}
235
245
  </FieldWrapper>
@@ -3,6 +3,7 @@ import { useMemo, useState } from "react";
3
3
  import { useWatch } from "react-hook-form";
4
4
  import type { BaseSourceProps, FieldRules, ReferenceProps } from "../types";
5
5
  import { valuesAsIds } from "../utils/choiceSelectionUtils";
6
+ import { referenceSelectDropdownProps } from "../utils/referenceSelectDropdownProps";
6
7
  import { referenceSelectNotFoundContent } from "../utils/referenceSelectNotFoundContent";
7
8
  import { useChoices } from "../utils/useChoices";
8
9
  import { FieldWrapper } from "./FieldWrapper";
@@ -18,6 +19,7 @@ export type ReferenceManyFieldProps = BaseSourceProps &
18
19
  search?: boolean;
19
20
  allowClear?: boolean;
20
21
  hideLabel?: boolean;
22
+ disabled?: boolean;
21
23
  referenceForm?: ReactNode;
22
24
  referencePermissions?: ResourcePermissions;
23
25
  referenceTitle?: string;
@@ -25,6 +27,8 @@ export type ReferenceManyFieldProps = BaseSourceProps &
25
27
  referenceModalWidth?: number;
26
28
  /** When false, hide add button even if catalog defines a form. Default true. */
27
29
  referenceActions?: boolean;
30
+ popupMatchSelectWidth?: boolean | number;
31
+ popupMinWidth?: number;
28
32
  };
29
33
 
30
34
  type ReferenceManyFieldSelectProps = Omit<
@@ -56,6 +60,8 @@ function ReferenceManyFieldSelect({
56
60
  referenceDefaultValues,
57
61
  referenceModalWidth,
58
62
  referenceActions = true,
63
+ popupMatchSelectWidth,
64
+ popupMinWidth,
59
65
  }: ReferenceManyFieldSelectProps) {
60
66
  const [searchText, setSearchText] = useState<string | undefined>();
61
67
  const [dropdownOpen, setDropdownOpen] = useState(false);
@@ -88,6 +94,7 @@ function ReferenceManyFieldSelect({
88
94
 
89
95
  const select = (
90
96
  <Select
97
+ {...referenceSelectDropdownProps({ popupMatchSelectWidth, popupMinWidth })}
91
98
  mode="multiple"
92
99
  value={selectValue}
93
100
  onChange={onChange}
@@ -149,6 +156,7 @@ export function ReferenceManyField({
149
156
  search,
150
157
  allowClear = true,
151
158
  hideLabel,
159
+ disabled: disabledProp,
152
160
  lazy = true,
153
161
  recordSource,
154
162
  fetchSelected = true,
@@ -158,6 +166,8 @@ export function ReferenceManyField({
158
166
  referenceDefaultValues,
159
167
  referenceModalWidth,
160
168
  referenceActions = true,
169
+ popupMatchSelectWidth,
170
+ popupMinWidth,
161
171
  }: ReferenceManyFieldProps) {
162
172
  const embeddedRecords = useWatch({
163
173
  name: recordSource ?? "",
@@ -185,7 +195,7 @@ export function ReferenceManyField({
185
195
  fetchSelected={fetchSelected}
186
196
  value={value}
187
197
  onChange={onChange}
188
- disabled={disabled}
198
+ disabled={disabled || disabledProp}
189
199
  selectedRecords={recordSource ? embeddedRecords : undefined}
190
200
  referenceForm={referenceForm}
191
201
  referencePermissions={referencePermissions}
@@ -193,6 +203,8 @@ export function ReferenceManyField({
193
203
  referenceDefaultValues={referenceDefaultValues}
194
204
  referenceModalWidth={referenceModalWidth}
195
205
  referenceActions={referenceActions}
206
+ popupMatchSelectWidth={popupMatchSelectWidth}
207
+ popupMinWidth={popupMinWidth}
196
208
  />
197
209
  )}
198
210
  </FieldWrapper>
@@ -0,0 +1,58 @@
1
+ import { Input } from "antd";
2
+ import type { CSSProperties } from "react";
3
+ import type { BaseSourceProps, FieldRules } from "../types";
4
+ import { FieldWrapper } from "./FieldWrapper";
5
+
6
+ export type TextAreaFieldProps = BaseSourceProps & {
7
+ name?: string;
8
+ required?: boolean;
9
+ rules?: FieldRules;
10
+ placeholder?: string;
11
+ inputStyle?: CSSProperties;
12
+ hideLabel?: boolean;
13
+ rows?: number;
14
+ maxLength?: number;
15
+ showCount?: boolean;
16
+ autoSize?: boolean | { minRows?: number; maxRows?: number };
17
+ };
18
+
19
+ export function TextAreaField({
20
+ source,
21
+ name,
22
+ label,
23
+ required,
24
+ rules,
25
+ placeholder,
26
+ inputStyle,
27
+ hideLabel,
28
+ rows = 4,
29
+ maxLength,
30
+ showCount,
31
+ autoSize,
32
+ }: TextAreaFieldProps) {
33
+ return (
34
+ <FieldWrapper
35
+ source={source}
36
+ name={name}
37
+ label={label}
38
+ required={required}
39
+ rules={rules}
40
+ hideLabel={hideLabel}
41
+ >
42
+ {({ value, onChange, onBlur, disabled }) => (
43
+ <Input.TextArea
44
+ value={value as string | undefined}
45
+ onChange={(e) => onChange(e.target.value)}
46
+ onBlur={onBlur}
47
+ placeholder={placeholder}
48
+ disabled={disabled}
49
+ style={inputStyle}
50
+ rows={autoSize ? undefined : rows}
51
+ maxLength={maxLength}
52
+ showCount={showCount}
53
+ autoSize={autoSize}
54
+ />
55
+ )}
56
+ </FieldWrapper>
57
+ );
58
+ }
@@ -0,0 +1,48 @@
1
+ import { TimePicker } from "antd";
2
+ import type { BaseSourceProps, FieldRules } from "../types";
3
+ import { parseDayjsValue } from "../utils/parseDayjsValue";
4
+ import { FieldWrapper } from "./FieldWrapper";
5
+
6
+ const DEFAULT_TIME_FORMAT = "HH:mm:ss";
7
+ const PARSE_FORMATS = [DEFAULT_TIME_FORMAT, "HH:mm", "H:mm:ss", "H:mm"];
8
+
9
+ export type TimeFieldProps = BaseSourceProps & {
10
+ name?: string;
11
+ required?: boolean;
12
+ rules?: FieldRules;
13
+ hideLabel?: boolean;
14
+ /** Output / display format. Default `HH:mm:ss` (Django TimeField). */
15
+ format?: string;
16
+ };
17
+
18
+ export function TimeField({
19
+ source,
20
+ name,
21
+ label,
22
+ required,
23
+ rules,
24
+ hideLabel,
25
+ format = DEFAULT_TIME_FORMAT,
26
+ }: TimeFieldProps) {
27
+ return (
28
+ <FieldWrapper
29
+ source={source}
30
+ name={name}
31
+ label={label}
32
+ required={required}
33
+ rules={rules}
34
+ hideLabel={hideLabel}
35
+ >
36
+ {({ value, onChange, onBlur, disabled }) => (
37
+ <TimePicker
38
+ value={parseDayjsValue(value, PARSE_FORMATS)}
39
+ onChange={(d) => onChange(d ? d.format(format) : null)}
40
+ onBlur={onBlur}
41
+ disabled={disabled}
42
+ format={format}
43
+ style={{ width: "100%" }}
44
+ />
45
+ )}
46
+ </FieldWrapper>
47
+ );
48
+ }
@@ -3,6 +3,7 @@ import { useMemo, useState } from "react";
3
3
  import type { BaseSourceProps, ChoiceOption, ReferenceProps } from "../types";
4
4
  import { useRegisterFilter } from "../context/FilterContext";
5
5
  import { useChoices } from "../utils/useChoices";
6
+ import { referenceSelectDropdownProps } from "../utils/referenceSelectDropdownProps";
6
7
  import { referenceSelectNotFoundContent } from "../utils/referenceSelectNotFoundContent";
7
8
 
8
9
  export type SelectFilterProps = BaseSourceProps & {
@@ -85,6 +86,8 @@ function ReferenceFilterInput({
85
86
  search,
86
87
  lazy = true,
87
88
  fetchSelected = true,
89
+ popupMatchSelectWidth,
90
+ popupMinWidth,
88
91
  value,
89
92
  onChange,
90
93
  }: ReferenceFilterProps & {
@@ -106,6 +109,7 @@ function ReferenceFilterInput({
106
109
 
107
110
  return (
108
111
  <Select
112
+ {...referenceSelectDropdownProps({ popupMatchSelectWidth, popupMinWidth })}
109
113
  allowClear
110
114
  mode={multiple ? "multiple" : undefined}
111
115
  placeholder={label ?? source}
@@ -141,6 +145,8 @@ export function ReferenceFilter({
141
145
  search,
142
146
  lazy = true,
143
147
  fetchSelected = true,
148
+ popupMatchSelectWidth,
149
+ popupMinWidth,
144
150
  }: ReferenceFilterProps) {
145
151
  const def = useMemo(
146
152
  () => ({
@@ -165,6 +171,8 @@ export function ReferenceFilter({
165
171
  search={search}
166
172
  lazy={lazy}
167
173
  fetchSelected={fetchSelected}
174
+ popupMatchSelectWidth={popupMatchSelectWidth}
175
+ popupMinWidth={popupMinWidth}
168
176
  value={value}
169
177
  onChange={onChange}
170
178
  />
@@ -181,6 +189,8 @@ export function ReferenceFilter({
181
189
  search,
182
190
  lazy,
183
191
  fetchSelected,
192
+ popupMatchSelectWidth,
193
+ popupMinWidth,
184
194
  ],
185
195
  );
186
196
  useRegisterFilter(def);
@@ -1,13 +1,85 @@
1
1
  import { Input } from "antd";
2
- import { useMemo } from "react";
2
+ import { useEffect, useMemo, useState } from "react";
3
3
  import type { BaseSourceProps } from "../types";
4
- import { useRegisterFilter } from "../context/FilterContext";
4
+ import { useFilterContext, useRegisterFilter } from "../context/FilterContext";
5
+ import { useDebouncedValue } from "../utils/useDebouncedValue";
6
+
7
+ /** Default debounce for list text filters (ms). */
8
+ export const DEFAULT_TEXT_FILTER_DEBOUNCE_MS = 300;
5
9
 
6
10
  export type TextFilterProps = BaseSourceProps & {
7
11
  placeholder?: string;
12
+ /**
13
+ * Debounce delay before applying the filter to the list / URL (ms).
14
+ * Defaults to `ResourceList` `textFilterDebounceMs`, then {@link DEFAULT_TEXT_FILTER_DEBOUNCE_MS}.
15
+ * Set `0` for immediate apply (legacy behavior).
16
+ */
17
+ debounceMs?: number;
8
18
  };
9
19
 
10
- export function TextFilter({ source, label, placeholder }: TextFilterProps) {
20
+ function normalizeFilterText(value: unknown): string | undefined {
21
+ if (value === undefined || value === null || value === "") return undefined;
22
+ return String(value);
23
+ }
24
+
25
+ function TextFilterInput({
26
+ value,
27
+ onChange,
28
+ placeholder,
29
+ debounceMs,
30
+ }: {
31
+ value: unknown;
32
+ onChange: (value: unknown) => void;
33
+ placeholder?: string;
34
+ debounceMs: number;
35
+ }) {
36
+ const [draft, setDraft] = useState(() => (value as string) ?? "");
37
+ const debouncedDraft = useDebouncedValue(draft, debounceMs);
38
+
39
+ useEffect(() => {
40
+ setDraft((value as string) ?? "");
41
+ }, [value]);
42
+
43
+ useEffect(() => {
44
+ if (debounceMs <= 0) return;
45
+ // Ignore stale debounced values while the user is still typing or after an immediate clear.
46
+ if (debouncedDraft !== draft) return;
47
+ const next = normalizeFilterText(debouncedDraft);
48
+ const current = normalizeFilterText(value);
49
+ if (next !== current) {
50
+ onChange(next);
51
+ }
52
+ }, [debouncedDraft, draft, debounceMs, onChange, value]);
53
+
54
+ return (
55
+ <Input
56
+ allowClear
57
+ placeholder={placeholder}
58
+ value={draft}
59
+ onChange={(e) => {
60
+ const nextDraft = e.target.value;
61
+ setDraft(nextDraft);
62
+ if (debounceMs <= 0 || nextDraft === "") {
63
+ onChange(normalizeFilterText(nextDraft));
64
+ }
65
+ }}
66
+ style={{ minWidth: 160 }}
67
+ />
68
+ );
69
+ }
70
+
71
+ export function TextFilter({
72
+ source,
73
+ label,
74
+ placeholder,
75
+ debounceMs: debounceMsProp,
76
+ }: TextFilterProps) {
77
+ const filterCtx = useFilterContext();
78
+ const debounceMs =
79
+ debounceMsProp ??
80
+ filterCtx?.textFilterDebounceMs ??
81
+ DEFAULT_TEXT_FILTER_DEBOUNCE_MS;
82
+
11
83
  const def = useMemo(
12
84
  () => ({
13
85
  key: source,
@@ -20,16 +92,15 @@ export function TextFilter({ source, label, placeholder }: TextFilterProps) {
20
92
  value: unknown;
21
93
  onChange: (value: unknown) => void;
22
94
  }) => (
23
- <Input
24
- allowClear
95
+ <TextFilterInput
96
+ value={value}
97
+ onChange={onChange}
25
98
  placeholder={placeholder ?? label ?? source}
26
- value={(value as string) ?? ""}
27
- onChange={(e) => onChange(e.target.value || undefined)}
28
- style={{ minWidth: 160 }}
99
+ debounceMs={debounceMs}
29
100
  />
30
101
  ),
31
102
  }),
32
- [source, label, placeholder],
103
+ [source, label, placeholder, debounceMs],
33
104
  );
34
105
  useRegisterFilter(def);
35
106
  return null;
package/src/crud/index.ts CHANGED
@@ -17,9 +17,13 @@ export { FormSteps, FormStep } from "./FormSteps";
17
17
  export type { FormStepsProps, FormStepProps } from "./FormSteps";
18
18
 
19
19
  export { TextField } from "./fields/TextField";
20
+ export { TextAreaField } from "./fields/TextAreaField";
21
+ export type { TextAreaFieldProps } from "./fields/TextAreaField";
20
22
  export { NumberField } from "./fields/NumberField";
21
23
  export { BooleanField } from "./fields/BooleanField";
22
24
  export { DateField } from "./fields/DateField";
25
+ export { DateTimeField } from "./fields/DateTimeField";
26
+ export { TimeField } from "./fields/TimeField";
23
27
  export { SelectField } from "./fields/SelectField";
24
28
  export { PasswordField } from "./fields/PasswordField";
25
29
  export { ReferenceField } from "./fields/ReferenceField";
@@ -45,7 +49,11 @@ export { ReferenceManyColumn } from "./columns/ReferenceManyColumn";
45
49
  export { ImageColumn } from "./columns/ImageColumn";
46
50
  export { CustomColumn } from "./columns/CustomColumn";
47
51
 
48
- export { TextFilter } from "./filters/TextFilter";
52
+ export {
53
+ TextFilter,
54
+ DEFAULT_TEXT_FILTER_DEBOUNCE_MS,
55
+ } from "./filters/TextFilter";
56
+ export type { TextFilterProps } from "./filters/TextFilter";
49
57
  export { NumberFilter } from "./filters/NumberFilter";
50
58
  export { BooleanFilter } from "./filters/BooleanFilter";
51
59
  export { DateFilter } from "./filters/DateFilter";
@@ -0,0 +1,113 @@
1
+ .ding-admin-scroll.ding-admin-resource-list-scroll,
2
+ .ding-admin-scroll:has(.ding-admin-resource-list-root) {
3
+ display: flex !important;
4
+ flex-direction: column !important;
5
+ overflow: hidden !important;
6
+ }
7
+
8
+ .ding-admin-resource-list-root {
9
+ display: flex;
10
+ flex-direction: column;
11
+ flex: 1;
12
+ min-height: 0;
13
+ }
14
+
15
+ .ding-admin-resource-list-root > :not(.ant-card) {
16
+ flex-shrink: 0;
17
+ }
18
+
19
+ .ding-admin-resource-list-root > .ant-card {
20
+ flex: 1;
21
+ min-height: 0;
22
+ display: flex;
23
+ flex-direction: column;
24
+ }
25
+
26
+ .ding-admin-resource-list-root > .ant-card > .ant-card-head {
27
+ flex-shrink: 0;
28
+ }
29
+
30
+ .ding-admin-resource-list-root > .ant-card > .ant-card-body {
31
+ flex: 1;
32
+ min-height: 0;
33
+ display: flex;
34
+ flex-direction: column;
35
+ }
36
+
37
+ .ding-admin-resource-list-root > .ant-card > .ant-card-body > :not(.ding-admin-resource-list-table) {
38
+ flex-shrink: 0;
39
+ }
40
+
41
+ .ding-admin-resource-list-table {
42
+ flex: 1;
43
+ min-height: 0;
44
+ display: flex;
45
+ flex-direction: column;
46
+ }
47
+
48
+ .ding-admin-resource-list-table .ant-spin-nested-loading,
49
+ .ding-admin-resource-list-table .ant-spin-container,
50
+ .ding-admin-resource-list-table .ant-table-wrapper,
51
+ .ding-admin-resource-list-table .ant-table,
52
+ .ding-admin-resource-list-table .ant-table-container {
53
+ flex: 1;
54
+ min-height: 0;
55
+ display: flex;
56
+ flex-direction: column;
57
+ }
58
+
59
+ .ding-admin-resource-list-table .ant-table-header {
60
+ flex-shrink: 0;
61
+ }
62
+
63
+ .ding-admin-resource-list-pagination {
64
+ display: flex;
65
+ justify-content: flex-end;
66
+ margin-top: 16px;
67
+ flex-shrink: 0;
68
+ position: relative;
69
+ z-index: 2;
70
+ }
71
+
72
+ .ding-admin-resource-list-table .ant-table-body,
73
+ .ding-admin-resource-list-table .ant-table-content {
74
+ scrollbar-width: thin;
75
+ scrollbar-color: var(--ding-scroll-thumb) transparent;
76
+ }
77
+
78
+ .ding-admin-resource-list-table .ant-table-body::-webkit-scrollbar,
79
+ .ding-admin-resource-list-table .ant-table-content::-webkit-scrollbar {
80
+ width: 6px;
81
+ height: 6px;
82
+ }
83
+
84
+ .ding-admin-resource-list-table .ant-table-body::-webkit-scrollbar-track,
85
+ .ding-admin-resource-list-table .ant-table-content::-webkit-scrollbar-track {
86
+ background: transparent;
87
+ }
88
+
89
+ .ding-admin-resource-list-table .ant-table-body::-webkit-scrollbar-thumb,
90
+ .ding-admin-resource-list-table .ant-table-content::-webkit-scrollbar-thumb {
91
+ background-color: var(--ding-scroll-thumb);
92
+ border-radius: 999px;
93
+ border: 1px solid transparent;
94
+ background-clip: padding-box;
95
+ }
96
+
97
+ .ding-admin-resource-list-table .ant-table-body::-webkit-scrollbar-thumb:hover,
98
+ .ding-admin-resource-list-table .ant-table-content::-webkit-scrollbar-thumb:hover {
99
+ background-color: var(--ding-scroll-thumb-hover);
100
+ }
101
+
102
+ .ding-admin-resource-list-table .ant-table-body::-webkit-scrollbar-corner,
103
+ .ding-admin-resource-list-table .ant-table-content::-webkit-scrollbar-corner {
104
+ background: transparent;
105
+ }
106
+
107
+ .ding-admin-resource-list-table .ding-admin-row-edit-link {
108
+ color: var(--ant-color-primary);
109
+ }
110
+
111
+ .ding-admin-resource-list-table .ding-admin-row-edit-link:hover {
112
+ color: var(--ant-color-primary-hover, var(--ant-color-primary));
113
+ }
package/src/crud/types.ts CHANGED
@@ -42,6 +42,13 @@ export type ReferenceProps = {
42
42
  * already embedded in the form record. Set `false` to never call `getOne`.
43
43
  */
44
44
  fetchSelected?: boolean;
45
+ /**
46
+ * When false (default), the options dropdown may be wider than the input.
47
+ * Pass true to match input width, or a number for a fixed popup width.
48
+ */
49
+ popupMatchSelectWidth?: boolean | number;
50
+ /** Minimum popup width in px when `popupMatchSelectWidth` is false. Default 360. */
51
+ popupMinWidth?: number;
45
52
  };
46
53
 
47
54
  export type DisplayProps = {
@@ -75,6 +82,8 @@ export type InlineFormSetBaseProps = {
75
82
  /** API key when different from `field`. */
76
83
  payloadKey?: string;
77
84
  transformRows?: (rows: Record<string, unknown>[]) => unknown;
85
+ /** Values applied when appending a new inline row (e.g. `{ is_active: true }`). */
86
+ defaultRow?: Record<string, unknown>;
78
87
  };
79
88
 
80
89
  /** Passed to each tabular inline column's `cell` renderer. */
@@ -0,0 +1,14 @@
1
+ import dayjs, { type Dayjs } from "dayjs";
2
+ import customParseFormat from "dayjs/plugin/customParseFormat";
3
+
4
+ dayjs.extend(customParseFormat);
5
+
6
+ export function parseDayjsValue(
7
+ value: unknown,
8
+ formats: string[],
9
+ ): Dayjs | null {
10
+ if (value == null || value === "") return null;
11
+ if (dayjs.isDayjs(value)) return value;
12
+ const parsed = dayjs(String(value), formats, true);
13
+ return parsed.isValid() ? parsed : dayjs(String(value)).isValid() ? dayjs(String(value)) : null;
14
+ }