ding-react-admin 2.0.8 → 2.0.9

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 (69) hide show
  1. package/dist/index.css +1 -1
  2. package/dist/index.js +1564 -1139
  3. package/dist/src/crud/InlineFormSet.d.ts +6 -2
  4. package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
  5. package/dist/src/crud/ResourceList.d.ts +6 -1
  6. package/dist/src/crud/ResourceList.d.ts.map +1 -1
  7. package/dist/src/crud/context/FilterContext.d.ts +4 -1
  8. package/dist/src/crud/context/FilterContext.d.ts.map +1 -1
  9. package/dist/src/crud/fields/BooleanField.d.ts +2 -1
  10. package/dist/src/crud/fields/BooleanField.d.ts.map +1 -1
  11. package/dist/src/crud/fields/DateField.d.ts.map +1 -1
  12. package/dist/src/crud/fields/DateTimeField.d.ts +5 -0
  13. package/dist/src/crud/fields/DateTimeField.d.ts.map +1 -0
  14. package/dist/src/crud/fields/ReferenceField.d.ts +3 -1
  15. package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
  16. package/dist/src/crud/fields/ReferenceManyField.d.ts +4 -1
  17. package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
  18. package/dist/src/crud/fields/TextAreaField.d.ts +19 -0
  19. package/dist/src/crud/fields/TextAreaField.d.ts.map +1 -0
  20. package/dist/src/crud/fields/TimeField.d.ts +11 -0
  21. package/dist/src/crud/fields/TimeField.d.ts.map +1 -0
  22. package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
  23. package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
  24. package/dist/src/crud/filters/TextFilter.d.ts +9 -1
  25. package/dist/src/crud/filters/TextFilter.d.ts.map +1 -1
  26. package/dist/src/crud/index.d.ts +6 -1
  27. package/dist/src/crud/index.d.ts.map +1 -1
  28. package/dist/src/crud/types.d.ts +9 -0
  29. package/dist/src/crud/types.d.ts.map +1 -1
  30. package/dist/src/crud/utils/parseDayjsValue.d.ts +3 -0
  31. package/dist/src/crud/utils/parseDayjsValue.d.ts.map +1 -0
  32. package/dist/src/crud/utils/referenceSelectDropdownProps.d.ts +11 -0
  33. package/dist/src/crud/utils/referenceSelectDropdownProps.d.ts.map +1 -0
  34. package/dist/src/crud/utils/useDebouncedValue.d.ts +3 -0
  35. package/dist/src/crud/utils/useDebouncedValue.d.ts.map +1 -0
  36. package/dist/src/index.d.ts +3 -3
  37. package/dist/src/index.d.ts.map +1 -1
  38. package/dist/src/layouts/AdminLayout.d.ts +1 -1
  39. package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
  40. package/dist/src/layouts/navMenuItems.d.ts +3 -0
  41. package/dist/src/layouts/navMenuItems.d.ts.map +1 -1
  42. package/dist/src/types.d.ts +16 -0
  43. package/dist/src/types.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/components/NavMenuLabel.tsx +1 -1
  46. package/src/components/navMenu.css +107 -0
  47. package/src/context/AppThemeProvider.tsx +1 -1
  48. package/src/crud/InlineFormSet.tsx +32 -14
  49. package/src/crud/ResourceList.tsx +211 -35
  50. package/src/crud/context/FilterContext.tsx +7 -1
  51. package/src/crud/fields/BooleanField.tsx +3 -1
  52. package/src/crud/fields/DateField.tsx +6 -4
  53. package/src/crud/fields/DateTimeField.tsx +9 -0
  54. package/src/crud/fields/ReferenceField.tsx +10 -0
  55. package/src/crud/fields/ReferenceManyField.tsx +13 -1
  56. package/src/crud/fields/TextAreaField.tsx +58 -0
  57. package/src/crud/fields/TimeField.tsx +48 -0
  58. package/src/crud/filters/ReferenceFilter.tsx +10 -0
  59. package/src/crud/filters/TextFilter.tsx +80 -9
  60. package/src/crud/index.ts +9 -1
  61. package/src/crud/resourceListTable.css +113 -0
  62. package/src/crud/types.ts +9 -0
  63. package/src/crud/utils/parseDayjsValue.ts +14 -0
  64. package/src/crud/utils/referenceSelectDropdownProps.ts +30 -0
  65. package/src/crud/utils/useDebouncedValue.ts +17 -0
  66. package/src/index.ts +7 -0
  67. package/src/layouts/AdminLayout.tsx +43 -6
  68. package/src/layouts/navMenuItems.tsx +11 -2
  69. package/src/types.ts +17 -0
@@ -13,6 +13,113 @@
13
13
  white-space: nowrap;
14
14
  }
15
15
 
16
+ .ding-admin-nav-menu--wrap-labels .ant-menu-title-content {
17
+ overflow: visible;
18
+ white-space: normal;
19
+ text-overflow: clip;
20
+ }
21
+
22
+ .ding-admin-nav-menu--wrap-labels .ding-admin-menu-label {
23
+ white-space: normal;
24
+ overflow: visible;
25
+ text-overflow: clip;
26
+ line-height: 1.3;
27
+ }
28
+
29
+ .ding-admin-nav-menu--item-divider-full.ant-menu-root
30
+ > .ant-menu-item
31
+ + .ant-menu-item,
32
+ .ding-admin-nav-menu--item-divider-full.ant-menu-root
33
+ > .ant-menu-item
34
+ + .ant-menu-submenu,
35
+ .ding-admin-nav-menu--item-divider-full.ant-menu-root
36
+ > .ant-menu-submenu
37
+ + .ant-menu-item,
38
+ .ding-admin-nav-menu--item-divider-full.ant-menu-root
39
+ > .ant-menu-submenu
40
+ + .ant-menu-submenu,
41
+ .ding-admin-nav-menu--item-divider-full .ant-menu-sub > .ant-menu-item + .ant-menu-item,
42
+ .ding-admin-nav-menu--item-divider-full
43
+ .ant-menu-sub
44
+ > .ant-menu-item
45
+ + .ant-menu-submenu,
46
+ .ding-admin-nav-menu--item-divider-full
47
+ .ant-menu-sub
48
+ > .ant-menu-submenu
49
+ + .ant-menu-item,
50
+ .ding-admin-nav-menu--item-divider-full
51
+ .ant-menu-sub
52
+ > .ant-menu-submenu
53
+ + .ant-menu-submenu {
54
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
55
+ }
56
+
57
+ .ding-admin-nav-menu--item-divider-inset.ant-menu-root
58
+ > .ant-menu-item
59
+ + .ant-menu-item,
60
+ .ding-admin-nav-menu--item-divider-inset.ant-menu-root
61
+ > .ant-menu-item
62
+ + .ant-menu-submenu,
63
+ .ding-admin-nav-menu--item-divider-inset.ant-menu-root
64
+ > .ant-menu-submenu
65
+ + .ant-menu-item,
66
+ .ding-admin-nav-menu--item-divider-inset.ant-menu-root
67
+ > .ant-menu-submenu
68
+ + .ant-menu-submenu,
69
+ .ding-admin-nav-menu--item-divider-inset .ant-menu-sub > .ant-menu-item + .ant-menu-item,
70
+ .ding-admin-nav-menu--item-divider-inset
71
+ .ant-menu-sub
72
+ > .ant-menu-item
73
+ + .ant-menu-submenu,
74
+ .ding-admin-nav-menu--item-divider-inset
75
+ .ant-menu-sub
76
+ > .ant-menu-submenu
77
+ + .ant-menu-item,
78
+ .ding-admin-nav-menu--item-divider-inset
79
+ .ant-menu-sub
80
+ > .ant-menu-submenu
81
+ + .ant-menu-submenu {
82
+ position: relative;
83
+ }
84
+
85
+ .ding-admin-nav-menu--item-divider-inset.ant-menu-root
86
+ > .ant-menu-item
87
+ + .ant-menu-item::before,
88
+ .ding-admin-nav-menu--item-divider-inset.ant-menu-root
89
+ > .ant-menu-item
90
+ + .ant-menu-submenu::before,
91
+ .ding-admin-nav-menu--item-divider-inset.ant-menu-root
92
+ > .ant-menu-submenu
93
+ + .ant-menu-item::before,
94
+ .ding-admin-nav-menu--item-divider-inset.ant-menu-root
95
+ > .ant-menu-submenu
96
+ + .ant-menu-submenu::before,
97
+ .ding-admin-nav-menu--item-divider-inset
98
+ .ant-menu-sub
99
+ > .ant-menu-item
100
+ + .ant-menu-item::before,
101
+ .ding-admin-nav-menu--item-divider-inset
102
+ .ant-menu-sub
103
+ > .ant-menu-item
104
+ + .ant-menu-submenu::before,
105
+ .ding-admin-nav-menu--item-divider-inset
106
+ .ant-menu-sub
107
+ > .ant-menu-submenu
108
+ + .ant-menu-item::before,
109
+ .ding-admin-nav-menu--item-divider-inset
110
+ .ant-menu-sub
111
+ > .ant-menu-submenu
112
+ + .ant-menu-submenu::before {
113
+ content: "";
114
+ position: absolute;
115
+ top: 0;
116
+ left: 16px;
117
+ right: 16px;
118
+ height: 1px;
119
+ background: rgba(255, 255, 255, 0.08);
120
+ pointer-events: none;
121
+ }
122
+
16
123
  /* Mobile drawer: wrap long labels instead of truncating */
17
124
  .ant-drawer .ding-admin-menu-label {
18
125
  white-space: normal;
@@ -46,7 +46,7 @@ function readStoredDensity(key: string): ThemeDensity {
46
46
  } catch {
47
47
  /* ignore */
48
48
  }
49
- return "comfortable";
49
+ return "compact";
50
50
  }
51
51
 
52
52
  const DEFAULT_MODE_KEY = "ding-react-admin-theme-mode";
@@ -20,15 +20,25 @@ export type InlineFormSetStackedProps = InlineFormSetBaseProps & {
20
20
  /** Field sources in each row — used when appending an empty row. */
21
21
  sources: string[];
22
22
  renderRow: (ctx: InlineRowContext) => ReactNode;
23
+ /** Optional card title per row (defaults to "Item N"). */
24
+ getCardTitle?: (ctx: InlineRowContext) => ReactNode;
25
+ /** Optional content below the row list (e.g. combined preview). */
26
+ footer?: ReactNode;
23
27
  };
24
28
 
25
- function emptyRow(sources: string[]) {
29
+ function emptyRow(sources: string[], defaults?: Record<string, unknown>) {
26
30
  const row: Record<string, unknown> = {};
27
- for (const source of sources) row[source] = undefined;
31
+ for (const source of sources) {
32
+ row[source] = defaults?.[source] ?? undefined;
33
+ }
28
34
  return row;
29
35
  }
30
36
 
31
- function useInlineRows(field: string, sources: string[]) {
37
+ function useInlineRows(
38
+ field: string,
39
+ sources: string[],
40
+ defaultRow?: Record<string, unknown>,
41
+ ) {
32
42
  const { control } = useFormContext<FieldValues>();
33
43
  const { fields, append, remove } = useFieldArray({
34
44
  control,
@@ -36,7 +46,7 @@ function useInlineRows(field: string, sources: string[]) {
36
46
  keyName: "rowKey",
37
47
  });
38
48
 
39
- const appendEmpty = () => append(emptyRow(sources));
49
+ const appendEmpty = () => append(emptyRow(sources, defaultRow));
40
50
 
41
51
  return { fields, remove, appendEmpty };
42
52
  }
@@ -48,12 +58,13 @@ export function InlineFormSet({
48
58
  payloadKey,
49
59
  transformRows,
50
60
  columns,
61
+ defaultRow,
51
62
  }: InlineFormSetProps) {
52
63
  const sources = useMemo(
53
64
  () => columns.map((col) => col.source),
54
65
  [columns],
55
66
  );
56
- const { fields, remove, appendEmpty } = useInlineRows(field, sources);
67
+ const { fields, remove, appendEmpty } = useInlineRows(field, sources, defaultRow);
57
68
 
58
69
  useRegisterPayloadField(field);
59
70
  useRegisterInlineField(field, sources, payloadKey, transformRows);
@@ -120,8 +131,11 @@ export function InlineFormSetStacked({
120
131
  transformRows,
121
132
  sources,
122
133
  renderRow,
134
+ getCardTitle,
135
+ footer,
136
+ defaultRow,
123
137
  }: InlineFormSetStackedProps) {
124
- const { fields, remove, appendEmpty } = useInlineRows(field, sources);
138
+ const { fields, remove, appendEmpty } = useInlineRows(field, sources, defaultRow);
125
139
 
126
140
  useRegisterPayloadField(field);
127
141
  useRegisterInlineField(field, sources, payloadKey, transformRows);
@@ -130,11 +144,17 @@ export function InlineFormSetStacked({
130
144
  <div style={{ marginTop: 24 }}>
131
145
  <Typography.Title level={5}>{label ?? "Related items"}</Typography.Title>
132
146
  <Space orientation="vertical" size="middle" style={{ width: "100%" }}>
133
- {fields.map((rowField, index) => (
147
+ {fields.map((rowField, index) => {
148
+ const rowContext = {
149
+ field,
150
+ index,
151
+ name: (source: string) => nestedFieldPath(field, index, source),
152
+ };
153
+ return (
134
154
  <Card
135
155
  key={rowField.rowKey}
136
156
  size="small"
137
- title={`Item ${index + 1}`}
157
+ title={getCardTitle?.(rowContext) ?? `Item ${index + 1}`}
138
158
  extra={
139
159
  <Button
140
160
  type="link"
@@ -146,17 +166,15 @@ export function InlineFormSetStacked({
146
166
  </Button>
147
167
  }
148
168
  >
149
- {renderRow({
150
- field,
151
- index,
152
- name: (source) => nestedFieldPath(field, index, source),
153
- })}
169
+ {renderRow(rowContext)}
154
170
  </Card>
155
- ))}
171
+ );
172
+ })}
156
173
  </Space>
157
174
  <Button type="dashed" style={{ marginTop: 8 }} onClick={appendEmpty}>
158
175
  Add item
159
176
  </Button>
177
+ {footer}
160
178
  </div>
161
179
  );
162
180
  }
@@ -1,5 +1,5 @@
1
1
  import { CaretDownOutlined, CaretUpOutlined } from "@ant-design/icons";
2
- import { App, Button, Card, Space, Table, Typography } from "antd";
2
+ import { App, Button, Card, Pagination, Space, Table, theme, Typography } from "antd";
3
3
  import type { ColumnsType } from "antd/es/table";
4
4
  import type {
5
5
  SortOrder as AntSortOrder,
@@ -9,9 +9,12 @@ import {
9
9
  createContext,
10
10
  useCallback,
11
11
  useContext,
12
+ useLayoutEffect,
12
13
  useMemo,
14
+ useRef,
13
15
  useState,
14
16
  type Key,
17
+ type CSSProperties,
15
18
  type ReactNode,
16
19
  } from "react";
17
20
  import { Link } from "react-router-dom";
@@ -33,6 +36,7 @@ import type {
33
36
  import { useListQueryState } from "./utils/useListQueryState";
34
37
  import { useAbortableEffect } from "./utils/useAbortableEffect";
35
38
  import { isAbortError } from "../data/abortError";
39
+ import "./resourceListTable.css";
36
40
 
37
41
  type ResourceListContextValue = {
38
42
  filterValues: Record<string, unknown>;
@@ -54,6 +58,11 @@ export type ResourceListProps = {
54
58
  newPath?: string;
55
59
  /** Merged into every list request (not synced to URL). */
56
60
  staticFilter?: Record<string, unknown>;
61
+ /**
62
+ * Debounce delay for `TextFilter` before syncing to URL / refetching (ms).
63
+ * Default 300. Per-field override: `TextFilter debounceMs`.
64
+ */
65
+ textFilterDebounceMs?: number;
57
66
  editMode?: EditMode;
58
67
  syncQueryParams?: boolean;
59
68
  children: ReactNode;
@@ -78,6 +87,64 @@ export type ResourceListProps = {
78
87
  permissions?: ResourcePermissions;
79
88
  };
80
89
 
90
+ function formatCellValue(value: unknown): ReactNode {
91
+ if (value == null || value === "") return "—";
92
+ return String(value);
93
+ }
94
+
95
+ function withFirstColumnEditLink<T extends Record<string, unknown>>(
96
+ columns: ColumnsType<T>,
97
+ options: {
98
+ showEdit: boolean;
99
+ showQuickEdit: boolean;
100
+ pathPrefix: string;
101
+ openEditModal: (id: string | number) => void;
102
+ },
103
+ ): ColumnsType<T> {
104
+ if (columns.length === 0 || (!options.showEdit && !options.showQuickEdit)) {
105
+ return columns;
106
+ }
107
+
108
+ const [first, ...rest] = columns;
109
+ const originalRender = first.render;
110
+
111
+ return [
112
+ {
113
+ ...first,
114
+ render: (value, row, index) => {
115
+ const rendered = originalRender
116
+ ? originalRender(value, row, index)
117
+ : formatCellValue(value);
118
+ const content = rendered as ReactNode;
119
+
120
+ if (options.showEdit) {
121
+ return (
122
+ <Link
123
+ to={`${options.pathPrefix}/${String(row.id)}`}
124
+ className="ding-admin-row-edit-link"
125
+ >
126
+ {content}
127
+ </Link>
128
+ );
129
+ }
130
+
131
+ return (
132
+ <Button
133
+ type="link"
134
+ size="small"
135
+ className="ding-admin-row-edit-link"
136
+ style={{ padding: 0, height: "auto", textAlign: "inherit" }}
137
+ onClick={() => options.openEditModal(row.id as string | number)}
138
+ >
139
+ {content}
140
+ </Button>
141
+ );
142
+ },
143
+ },
144
+ ...rest,
145
+ ];
146
+ }
147
+
81
148
  function ResourceListTable<T extends Record<string, unknown>>({
82
149
  resource,
83
150
  title,
@@ -117,7 +184,10 @@ function ResourceListTable<T extends Record<string, unknown>>({
117
184
  const dp = useDataProvider();
118
185
  const can = usePermissions();
119
186
  const { message, modal } = App.useApp();
187
+ const { token } = theme.useToken();
120
188
  const { columns, sortOrders, sortPriorities } = useListContext();
189
+ const tableWrapRef = useRef<HTMLDivElement>(null);
190
+ const [tableScrollY, setTableScrollY] = useState<number>();
121
191
  const [loading, setLoading] = useState(false);
122
192
  const [data, setData] = useState<T[]>([]);
123
193
  const [total, setTotal] = useState(0);
@@ -178,6 +248,66 @@ function ResourceListTable<T extends Record<string, unknown>>({
178
248
  const showBulkActions = availableBulkActions.length > 0;
179
249
 
180
250
  const selectedCount = selectedIds.size;
251
+
252
+ useLayoutEffect(() => {
253
+ const wrap = tableWrapRef.current;
254
+ if (!wrap) return;
255
+
256
+ const scrollParent = wrap.closest(".ding-admin-scroll") as HTMLElement | null;
257
+ scrollParent?.classList.add("ding-admin-resource-list-scroll");
258
+
259
+ const applyScrollY = () => {
260
+ if (wrap.clientHeight <= 0) return;
261
+
262
+ const paginationEl = wrap.querySelector(
263
+ ".ding-admin-resource-list-pagination",
264
+ ) as HTMLElement | null;
265
+ const tableHead = wrap.querySelector(
266
+ ".ant-table-header",
267
+ ) as HTMLElement | null;
268
+
269
+ const paginationHeight = paginationEl?.offsetHeight ?? 0;
270
+ const paginationMargin = paginationEl
271
+ ? parseFloat(getComputedStyle(paginationEl).marginTop) +
272
+ parseFloat(getComputedStyle(paginationEl).marginBottom)
273
+ : 0;
274
+ const headHeight = tableHead?.offsetHeight ?? 0;
275
+ const tableBody = wrap.querySelector(
276
+ ".ant-table-body",
277
+ ) as HTMLElement | null;
278
+ const horizontalScrollbar =
279
+ tableBody && tableBody.scrollWidth > tableBody.clientWidth
280
+ ? tableBody.offsetHeight - tableBody.clientHeight
281
+ : 0;
282
+
283
+ const next = Math.max(
284
+ 120,
285
+ Math.floor(
286
+ wrap.clientHeight -
287
+ headHeight -
288
+ paginationHeight -
289
+ paginationMargin -
290
+ horizontalScrollbar -
291
+ 8,
292
+ ),
293
+ );
294
+
295
+ setTableScrollY((prev) => (prev === next ? prev : next));
296
+ };
297
+
298
+ applyScrollY();
299
+
300
+ const resizeObserver = new ResizeObserver(() => applyScrollY());
301
+ resizeObserver.observe(wrap);
302
+
303
+ window.addEventListener("resize", applyScrollY);
304
+ return () => {
305
+ scrollParent?.classList.remove("ding-admin-resource-list-scroll");
306
+ resizeObserver.disconnect();
307
+ window.removeEventListener("resize", applyScrollY);
308
+ };
309
+ }, [showBulkActions, selectedCount, total, loading]);
310
+
181
311
  const allPageSelected =
182
312
  data.length > 0 &&
183
313
  data.every((row) => selectedIds.has(row.id as string | number));
@@ -251,7 +381,7 @@ function ResourceListTable<T extends Record<string, unknown>>({
251
381
  }
252
382
 
253
383
  const field = resolveField(sorter);
254
- if (field) {
384
+ if (field && !Array.isArray(sorter) && sorter.order) {
255
385
  queryActions.toggleSort(field);
256
386
  return;
257
387
  }
@@ -405,7 +535,14 @@ function ResourceListTable<T extends Record<string, unknown>>({
405
535
  return col;
406
536
  });
407
537
 
408
- if (!showActionsColumn) return built;
538
+ if (!showActionsColumn) {
539
+ return withFirstColumnEditLink(built, {
540
+ showEdit,
541
+ showQuickEdit,
542
+ pathPrefix,
543
+ openEditModal: queryActions.openEditModal,
544
+ });
545
+ }
409
546
 
410
547
  const rowHelpers: ResourceListRowActionsHelpers = {
411
548
  reload: () => void load(),
@@ -448,7 +585,12 @@ function ResourceListTable<T extends Record<string, unknown>>({
448
585
  </Space>
449
586
  ),
450
587
  };
451
- return [...built, action];
588
+ return withFirstColumnEditLink([...built, action], {
589
+ showEdit,
590
+ showQuickEdit,
591
+ pathPrefix,
592
+ openEditModal: queryActions.openEditModal,
593
+ });
452
594
  }, [
453
595
  columns,
454
596
  showActionsColumn,
@@ -465,6 +607,19 @@ function ResourceListTable<T extends Record<string, unknown>>({
465
607
  load,
466
608
  ]);
467
609
 
610
+ const handlePaginationChange = useCallback(
611
+ (page: number, pageSize: number) => {
612
+ if (pageSize !== queryState.perPage) {
613
+ queryActions.setPerPage(pageSize);
614
+ return;
615
+ }
616
+ if (page !== queryState.page) {
617
+ queryActions.setPage(page);
618
+ }
619
+ },
620
+ [queryActions, queryState.page, queryState.perPage],
621
+ );
622
+
468
623
  const showModal =
469
624
  formChildren &&
470
625
  (queryState.createModal || queryState.editId != null) &&
@@ -521,35 +676,49 @@ function ResourceListTable<T extends Record<string, unknown>>({
521
676
  running={bulkRunning || loading}
522
677
  />
523
678
  ) : null}
524
- <Table<T>
525
- rowKey="id"
526
- loading={loading}
527
- columns={tableColumns}
528
- dataSource={data}
529
- scroll={{ x: "max-content" }}
530
- rowSelection={
531
- showBulkActions
532
- ? {
533
- selectedRowKeys,
534
- onChange: handleRowSelectionChange,
535
- preserveSelectedRowKeys: true,
536
- }
537
- : undefined
679
+ <div
680
+ ref={tableWrapRef}
681
+ className="ding-admin-resource-list-table"
682
+ style={
683
+ {
684
+ "--ding-scroll-thumb": token.colorTextQuaternary,
685
+ "--ding-scroll-thumb-hover": token.colorTextTertiary,
686
+ } as CSSProperties
538
687
  }
539
- pagination={{
540
- current: queryState.page,
541
- pageSize: queryState.perPage,
542
- total,
543
- showSizeChanger: true,
544
- onChange: (p, ps) => {
545
- queryActions.setPage(p);
546
- if (ps) queryActions.setPerPage(ps);
547
- },
548
- }}
549
- onChange={(_pag, _filters, sorter) => {
550
- handleTableSortChange(sorter as SorterResult<T> | SorterResult<T>[]);
551
- }}
552
- />
688
+ >
689
+ <Table<T>
690
+ rowKey="id"
691
+ loading={loading}
692
+ columns={tableColumns}
693
+ dataSource={data}
694
+ scroll={{
695
+ x: "max-content",
696
+ ...(tableScrollY ? { y: tableScrollY } : {}),
697
+ }}
698
+ rowSelection={
699
+ showBulkActions
700
+ ? {
701
+ selectedRowKeys,
702
+ onChange: handleRowSelectionChange,
703
+ preserveSelectedRowKeys: true,
704
+ }
705
+ : undefined
706
+ }
707
+ pagination={false}
708
+ onChange={(_pag, _filters, sorter) => {
709
+ handleTableSortChange(sorter as SorterResult<T> | SorterResult<T>[]);
710
+ }}
711
+ />
712
+ <div className="ding-admin-resource-list-pagination">
713
+ <Pagination
714
+ current={queryState.page}
715
+ pageSize={queryState.perPage}
716
+ total={total}
717
+ showSizeChanger
718
+ onChange={handlePaginationChange}
719
+ />
720
+ </div>
721
+ </div>
553
722
  </Card>
554
723
  {showModal ? (
555
724
  <ResourceFormModal
@@ -573,6 +742,7 @@ export function ResourceList({
573
742
  pathPrefix,
574
743
  newPath,
575
744
  staticFilter,
745
+ textFilterDebounceMs,
576
746
  editMode = "page",
577
747
  syncQueryParams = true,
578
748
  children,
@@ -613,13 +783,18 @@ export function ResourceList({
613
783
 
614
784
  return (
615
785
  <ResourceListContext.Provider value={listCtx}>
616
- <FilterContextProvider values={filterValues} setFilterValue={setFilterValue}>
786
+ <FilterContextProvider
787
+ values={filterValues}
788
+ setFilterValue={setFilterValue}
789
+ textFilterDebounceMs={textFilterDebounceMs}
790
+ >
617
791
  <ListContextProvider
618
792
  toggleSort={queryActions.toggleSort}
619
793
  sort={queryState.sort}
620
794
  >
621
- {children}
622
- <ResourceListTable
795
+ <div className="ding-admin-resource-list-root">
796
+ {children}
797
+ <ResourceListTable
623
798
  resource={resource}
624
799
  title={title}
625
800
  pathPrefix={pathPrefix}
@@ -636,6 +811,7 @@ export function ResourceList({
636
811
  queryState={queryState}
637
812
  queryActions={queryActions}
638
813
  />
814
+ </div>
639
815
  </ListContextProvider>
640
816
  </FilterContextProvider>
641
817
  </ResourceListContext.Provider>
@@ -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
+ }