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.
- package/dist/index.css +1 -1
- package/dist/index.js +1564 -1139
- package/dist/src/crud/InlineFormSet.d.ts +6 -2
- package/dist/src/crud/InlineFormSet.d.ts.map +1 -1
- package/dist/src/crud/ResourceList.d.ts +6 -1
- package/dist/src/crud/ResourceList.d.ts.map +1 -1
- package/dist/src/crud/context/FilterContext.d.ts +4 -1
- package/dist/src/crud/context/FilterContext.d.ts.map +1 -1
- package/dist/src/crud/fields/BooleanField.d.ts +2 -1
- package/dist/src/crud/fields/BooleanField.d.ts.map +1 -1
- package/dist/src/crud/fields/DateField.d.ts.map +1 -1
- package/dist/src/crud/fields/DateTimeField.d.ts +5 -0
- package/dist/src/crud/fields/DateTimeField.d.ts.map +1 -0
- package/dist/src/crud/fields/ReferenceField.d.ts +3 -1
- package/dist/src/crud/fields/ReferenceField.d.ts.map +1 -1
- package/dist/src/crud/fields/ReferenceManyField.d.ts +4 -1
- package/dist/src/crud/fields/ReferenceManyField.d.ts.map +1 -1
- package/dist/src/crud/fields/TextAreaField.d.ts +19 -0
- package/dist/src/crud/fields/TextAreaField.d.ts.map +1 -0
- package/dist/src/crud/fields/TimeField.d.ts +11 -0
- package/dist/src/crud/fields/TimeField.d.ts.map +1 -0
- package/dist/src/crud/filters/ReferenceFilter.d.ts +1 -1
- package/dist/src/crud/filters/ReferenceFilter.d.ts.map +1 -1
- package/dist/src/crud/filters/TextFilter.d.ts +9 -1
- package/dist/src/crud/filters/TextFilter.d.ts.map +1 -1
- package/dist/src/crud/index.d.ts +6 -1
- package/dist/src/crud/index.d.ts.map +1 -1
- package/dist/src/crud/types.d.ts +9 -0
- package/dist/src/crud/types.d.ts.map +1 -1
- package/dist/src/crud/utils/parseDayjsValue.d.ts +3 -0
- package/dist/src/crud/utils/parseDayjsValue.d.ts.map +1 -0
- package/dist/src/crud/utils/referenceSelectDropdownProps.d.ts +11 -0
- package/dist/src/crud/utils/referenceSelectDropdownProps.d.ts.map +1 -0
- package/dist/src/crud/utils/useDebouncedValue.d.ts +3 -0
- package/dist/src/crud/utils/useDebouncedValue.d.ts.map +1 -0
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/layouts/AdminLayout.d.ts +1 -1
- package/dist/src/layouts/AdminLayout.d.ts.map +1 -1
- package/dist/src/layouts/navMenuItems.d.ts +3 -0
- package/dist/src/layouts/navMenuItems.d.ts.map +1 -1
- package/dist/src/types.d.ts +16 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/NavMenuLabel.tsx +1 -1
- package/src/components/navMenu.css +107 -0
- package/src/context/AppThemeProvider.tsx +1 -1
- package/src/crud/InlineFormSet.tsx +32 -14
- package/src/crud/ResourceList.tsx +211 -35
- package/src/crud/context/FilterContext.tsx +7 -1
- package/src/crud/fields/BooleanField.tsx +3 -1
- package/src/crud/fields/DateField.tsx +6 -4
- package/src/crud/fields/DateTimeField.tsx +9 -0
- package/src/crud/fields/ReferenceField.tsx +10 -0
- package/src/crud/fields/ReferenceManyField.tsx +13 -1
- package/src/crud/fields/TextAreaField.tsx +58 -0
- package/src/crud/fields/TimeField.tsx +48 -0
- package/src/crud/filters/ReferenceFilter.tsx +10 -0
- package/src/crud/filters/TextFilter.tsx +80 -9
- package/src/crud/index.ts +9 -1
- package/src/crud/resourceListTable.css +113 -0
- package/src/crud/types.ts +9 -0
- package/src/crud/utils/parseDayjsValue.ts +14 -0
- package/src/crud/utils/referenceSelectDropdownProps.ts +30 -0
- package/src/crud/utils/useDebouncedValue.ts +17 -0
- package/src/index.ts +7 -0
- package/src/layouts/AdminLayout.tsx +43 -6
- package/src/layouts/navMenuItems.tsx +11 -2
- 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;
|
|
@@ -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)
|
|
31
|
+
for (const source of sources) {
|
|
32
|
+
row[source] = defaults?.[source] ?? undefined;
|
|
33
|
+
}
|
|
28
34
|
return row;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
function useInlineRows(
|
|
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)
|
|
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
|
-
<
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
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
|
-
|
|
622
|
-
|
|
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 ?
|
|
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 ?
|
|
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 ?
|
|
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
|
+
}
|