@rebasepro/ui 0.6.1 → 0.8.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/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +13 -0
- package/dist/components/VirtualTable/fields/VirtualTableInput.d.ts +10 -0
- package/dist/components/VirtualTable/fields/VirtualTableNumberInput.d.ts +9 -0
- package/dist/components/VirtualTable/fields/VirtualTableSelect.d.ts +17 -0
- package/dist/components/VirtualTable/fields/VirtualTableSwitch.d.ts +8 -0
- package/dist/components/VirtualTable/index.d.ts +7 -0
- package/dist/components/VirtualTable/selection/SelectionContext.d.ts +11 -0
- package/dist/components/VirtualTable/selection/SelectionStore.d.ts +14 -0
- package/dist/icons/index.d.ts +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.es.js +2322 -23
- package/dist/index.es.js.map +1 -1
- package/dist/styles.d.ts +1 -1
- package/dist/views/CardView.d.ts +32 -0
- package/dist/views/CollectionView/CollectionCardView.d.ts +30 -0
- package/dist/views/CollectionView/CollectionKanbanView.d.ts +31 -0
- package/dist/views/CollectionView/CollectionListView.d.ts +30 -0
- package/dist/views/CollectionView/CollectionTableView.d.ts +37 -0
- package/dist/views/CollectionView/CollectionView.d.ts +121 -0
- package/dist/views/CollectionView/CollectionViewToolbar.d.ts +31 -0
- package/dist/views/CollectionView/CollectionViewTypes.d.ts +106 -0
- package/dist/views/CollectionView/DefaultCellRenderer.d.ts +9 -0
- package/dist/views/CollectionView/index.d.ts +24 -0
- package/dist/views/CollectionView/utils.d.ts +5 -0
- package/dist/views/Kanban/Board.d.ts +3 -0
- package/dist/views/Kanban/BoardColumn.d.ts +20 -0
- package/dist/views/Kanban/BoardColumnTitle.d.ts +8 -0
- package/dist/views/Kanban/BoardSortableList.d.ts +14 -0
- package/dist/views/Kanban/board_types.d.ts +61 -0
- package/dist/views/Kanban/index.d.ts +5 -0
- package/dist/views/KanbanView.d.ts +24 -0
- package/dist/views/ListView.d.ts +40 -0
- package/dist/views/TableView.d.ts +6 -0
- package/dist/views/index.d.ts +5 -0
- package/package.json +1 -1
- package/src/components/Sheet.tsx +8 -4
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +32 -0
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +82 -0
- package/src/components/VirtualTable/fields/VirtualTableNumberInput.tsx +71 -0
- package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +107 -0
- package/src/components/VirtualTable/fields/VirtualTableSwitch.tsx +28 -0
- package/src/components/VirtualTable/index.tsx +7 -0
- package/src/components/VirtualTable/selection/SelectionContext.tsx +36 -0
- package/src/components/VirtualTable/selection/SelectionStore.ts +54 -0
- package/src/icons/index.ts +3 -0
- package/src/index.ts +43 -3
- package/src/styles.ts +1 -1
- package/src/views/CardView.tsx +281 -0
- package/src/views/CollectionView/CollectionCardView.tsx +270 -0
- package/src/views/CollectionView/CollectionKanbanView.tsx +251 -0
- package/src/views/CollectionView/CollectionListView.tsx +245 -0
- package/src/views/CollectionView/CollectionTableView.tsx +229 -0
- package/src/views/CollectionView/CollectionView.tsx +388 -0
- package/src/views/CollectionView/CollectionViewToolbar.tsx +229 -0
- package/src/views/CollectionView/CollectionViewTypes.ts +137 -0
- package/src/views/CollectionView/DefaultCellRenderer.tsx +404 -0
- package/src/views/CollectionView/index.ts +44 -0
- package/src/views/CollectionView/utils.ts +14 -0
- package/src/views/Kanban/Board.tsx +421 -0
- package/src/views/Kanban/BoardColumn.tsx +135 -0
- package/src/views/Kanban/BoardColumnTitle.tsx +47 -0
- package/src/views/Kanban/BoardSortableList.tsx +170 -0
- package/src/views/Kanban/board_types.ts +70 -0
- package/src/views/Kanban/index.tsx +5 -0
- package/src/views/KanbanView.tsx +32 -0
- package/src/views/ListView.tsx +281 -0
- package/src/views/TableView.tsx +7 -0
- package/src/views/index.ts +5 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { useCallback, useMemo } from "react";
|
|
3
|
+
import { KanbanView } from "../KanbanView";
|
|
4
|
+
import type { BoardItem, BoardItemViewProps } from "../KanbanView";
|
|
5
|
+
import { Card } from "../../components/Card";
|
|
6
|
+
import { Typography } from "../../components/Typography";
|
|
7
|
+
import { DefaultCellRenderer } from "./DefaultCellRenderer";
|
|
8
|
+
import { cls } from "../../util";
|
|
9
|
+
import { cardClickableMixin, cardSelectedMixin } from "../../styles";
|
|
10
|
+
import type {
|
|
11
|
+
CollectionPropertyConfig,
|
|
12
|
+
CollectionDataController,
|
|
13
|
+
CellRendererOverride,
|
|
14
|
+
CollectionSelectionController,
|
|
15
|
+
CollectionEnumValueConfig,
|
|
16
|
+
} from "./CollectionViewTypes";
|
|
17
|
+
|
|
18
|
+
import { getValueInPath } from "./utils";
|
|
19
|
+
|
|
20
|
+
function getEnumLabel(enumConfig: CollectionPropertyConfig["enum"], key: string): string {
|
|
21
|
+
if (!enumConfig) return key;
|
|
22
|
+
if (enumConfig instanceof Map) {
|
|
23
|
+
const cfg: CollectionEnumValueConfig | undefined = enumConfig.get(key);
|
|
24
|
+
if (!cfg) return key;
|
|
25
|
+
return typeof cfg === "string" ? cfg : cfg.label;
|
|
26
|
+
}
|
|
27
|
+
const cfg: CollectionEnumValueConfig | undefined = enumConfig[key];
|
|
28
|
+
if (!cfg) return key;
|
|
29
|
+
return typeof cfg === "string" ? cfg : cfg.label;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Props for the {@link CollectionKanbanView} component.
|
|
34
|
+
* @group Collection View
|
|
35
|
+
*/
|
|
36
|
+
export type CollectionKanbanViewProps<T = Record<string, unknown>> = {
|
|
37
|
+
dataController: CollectionDataController<T>;
|
|
38
|
+
properties: Record<string, CollectionPropertyConfig>;
|
|
39
|
+
propertiesOrder?: string[];
|
|
40
|
+
idProperty?: string;
|
|
41
|
+
titleProperty?: string;
|
|
42
|
+
kanbanProperty: string;
|
|
43
|
+
onRowClick?: (row: T) => void;
|
|
44
|
+
cellRenderer?: CellRendererOverride<T>;
|
|
45
|
+
selectionEnabled?: boolean;
|
|
46
|
+
selectionController?: CollectionSelectionController<T>;
|
|
47
|
+
highlightedItems?: T[];
|
|
48
|
+
emptyComponent?: React.ReactNode;
|
|
49
|
+
onRowCreate?: (defaults?: Record<string, unknown>) => void;
|
|
50
|
+
className?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Headless kanban view that wraps {@link KanbanView}.
|
|
55
|
+
*
|
|
56
|
+
* Groups data by a kanban property's enum values into columns.
|
|
57
|
+
* Each card displays a title and 1–2 preview properties.
|
|
58
|
+
*
|
|
59
|
+
* @group Collection View
|
|
60
|
+
*/
|
|
61
|
+
export function CollectionKanbanView<T extends Record<string, unknown> = Record<string, unknown>>({
|
|
62
|
+
dataController,
|
|
63
|
+
properties,
|
|
64
|
+
propertiesOrder,
|
|
65
|
+
idProperty = "id",
|
|
66
|
+
titleProperty,
|
|
67
|
+
kanbanProperty,
|
|
68
|
+
onRowClick,
|
|
69
|
+
cellRenderer,
|
|
70
|
+
emptyComponent,
|
|
71
|
+
onRowCreate,
|
|
72
|
+
className,
|
|
73
|
+
}: CollectionKanbanViewProps<T>) {
|
|
74
|
+
const order = useMemo(
|
|
75
|
+
() => propertiesOrder ?? Object.keys(properties),
|
|
76
|
+
[propertiesOrder, properties]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// ── Resolve kanban columns ───────────────────────────────────────
|
|
80
|
+
const columns = useMemo<string[]>(() => {
|
|
81
|
+
const kanbanConfig = properties[kanbanProperty];
|
|
82
|
+
if (kanbanConfig?.enum) {
|
|
83
|
+
if (kanbanConfig.enum instanceof Map) {
|
|
84
|
+
return Array.from(kanbanConfig.enum.keys()).map(String);
|
|
85
|
+
}
|
|
86
|
+
return Object.keys(kanbanConfig.enum);
|
|
87
|
+
}
|
|
88
|
+
// Deduce from data
|
|
89
|
+
const seen = new Set<string>();
|
|
90
|
+
for (const row of dataController.data) {
|
|
91
|
+
const val = getValueInPath(row as Record<string, unknown>, kanbanProperty);
|
|
92
|
+
if (val !== null && val !== undefined) {
|
|
93
|
+
seen.add(String(val));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return Array.from(seen);
|
|
97
|
+
}, [properties, kanbanProperty, dataController.data]);
|
|
98
|
+
|
|
99
|
+
// ── Column labels ────────────────────────────────────────────────
|
|
100
|
+
const columnLabels = useMemo(() => {
|
|
101
|
+
const kanbanConfig = properties[kanbanProperty];
|
|
102
|
+
const labels: Record<string, string> = {};
|
|
103
|
+
for (const col of columns) {
|
|
104
|
+
labels[col] = getEnumLabel(kanbanConfig?.enum, col);
|
|
105
|
+
}
|
|
106
|
+
return labels;
|
|
107
|
+
}, [properties, kanbanProperty, columns]);
|
|
108
|
+
|
|
109
|
+
// ── Resolve title property ───────────────────────────────────────
|
|
110
|
+
const titlePropertyKey = useMemo(() => {
|
|
111
|
+
if (titleProperty) return titleProperty;
|
|
112
|
+
for (const key of order) {
|
|
113
|
+
const prop = properties[key];
|
|
114
|
+
if (prop && prop.type === "string" && !prop.hideFromCollection && !prop.storage) {
|
|
115
|
+
return key;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return idProperty;
|
|
119
|
+
}, [titleProperty, order, properties, idProperty]);
|
|
120
|
+
|
|
121
|
+
// ── Preview properties (up to 2) ─────────────────────────────────
|
|
122
|
+
const previewProperties = useMemo(() => {
|
|
123
|
+
const result: { key: string; property: CollectionPropertyConfig }[] = [];
|
|
124
|
+
for (const key of order) {
|
|
125
|
+
if (key === titlePropertyKey || key === kanbanProperty) continue;
|
|
126
|
+
const prop = properties[key];
|
|
127
|
+
if (prop && !prop.hideFromCollection) {
|
|
128
|
+
result.push({ key, property: prop });
|
|
129
|
+
if (result.length >= 2) break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}, [order, properties, titlePropertyKey, kanbanProperty]);
|
|
134
|
+
|
|
135
|
+
// ── Convert data to BoardItem<T>[] ───────────────────────────────
|
|
136
|
+
const boardItems = useMemo<BoardItem<T>[]>(() => {
|
|
137
|
+
return dataController.data.map((row, index) => {
|
|
138
|
+
const id = getValueInPath(row as Record<string, unknown>, idProperty);
|
|
139
|
+
return {
|
|
140
|
+
id: id !== null && id !== undefined ? String(id) : String(index),
|
|
141
|
+
data: row,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}, [dataController.data, idProperty]);
|
|
145
|
+
|
|
146
|
+
// ── Assign column ────────────────────────────────────────────────
|
|
147
|
+
const assignColumn = useCallback(
|
|
148
|
+
(item: BoardItem<T>): string => {
|
|
149
|
+
const val = getValueInPath(item.data as Record<string, unknown>, kanbanProperty);
|
|
150
|
+
const strVal = val !== null && val !== undefined ? String(val) : "";
|
|
151
|
+
if (columns.includes(strVal)) return strVal;
|
|
152
|
+
return columns[0] ?? "";
|
|
153
|
+
},
|
|
154
|
+
[kanbanProperty, columns]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// ── Item component ───────────────────────────────────────────────
|
|
158
|
+
const ItemComponent = useMemo(() => {
|
|
159
|
+
function KanbanCard(props: BoardItemViewProps<T>) {
|
|
160
|
+
const { item } = props;
|
|
161
|
+
const titleValue = getValueInPath(
|
|
162
|
+
item.data as Record<string, unknown>,
|
|
163
|
+
titlePropertyKey
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<Card
|
|
168
|
+
onClick={() => onRowClick?.(item.data)}
|
|
169
|
+
className={cls(
|
|
170
|
+
"p-3",
|
|
171
|
+
cardClickableMixin,
|
|
172
|
+
props.isDragging && cardSelectedMixin,
|
|
173
|
+
props.isDragging && "shadow-lg opacity-75"
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
<Typography variant="subtitle2" noWrap>
|
|
177
|
+
{String(titleValue ?? "")}
|
|
178
|
+
</Typography>
|
|
179
|
+
{previewProperties.map(({ key, property }) => {
|
|
180
|
+
const value = getValueInPath(
|
|
181
|
+
item.data as Record<string, unknown>,
|
|
182
|
+
key
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (cellRenderer) {
|
|
186
|
+
const custom = cellRenderer({
|
|
187
|
+
row: item.data,
|
|
188
|
+
propertyKey: key,
|
|
189
|
+
property,
|
|
190
|
+
value,
|
|
191
|
+
size: "tiny",
|
|
192
|
+
});
|
|
193
|
+
if (custom !== undefined) {
|
|
194
|
+
return (
|
|
195
|
+
<div key={key} className="mt-1">
|
|
196
|
+
{custom}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div key={key} className="mt-1">
|
|
204
|
+
<DefaultCellRenderer
|
|
205
|
+
row={item.data}
|
|
206
|
+
propertyKey={key}
|
|
207
|
+
property={property}
|
|
208
|
+
value={value}
|
|
209
|
+
size="tiny"
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
})}
|
|
214
|
+
</Card>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return KanbanCard;
|
|
218
|
+
}, [titlePropertyKey, previewProperties, cellRenderer, onRowClick]);
|
|
219
|
+
|
|
220
|
+
// ── Add item to column ───────────────────────────────────────────
|
|
221
|
+
const handleAddItemToColumn = useCallback(
|
|
222
|
+
(column: string) => {
|
|
223
|
+
onRowCreate?.({ [kanbanProperty]: column });
|
|
224
|
+
},
|
|
225
|
+
[onRowCreate, kanbanProperty]
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (columns.length === 0 && !dataController.loading) {
|
|
229
|
+
return (
|
|
230
|
+
<div className="flex items-center justify-center p-8">
|
|
231
|
+
{emptyComponent ?? (
|
|
232
|
+
<Typography variant="body2" color="secondary">
|
|
233
|
+
No columns available for kanban view.
|
|
234
|
+
</Typography>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<KanbanView<T, string>
|
|
242
|
+
columns={columns}
|
|
243
|
+
columnLabels={columnLabels as Record<string, string>}
|
|
244
|
+
data={boardItems}
|
|
245
|
+
assignColumn={assignColumn}
|
|
246
|
+
ItemComponent={ItemComponent}
|
|
247
|
+
onAddItemToColumn={onRowCreate ? handleAddItemToColumn : undefined}
|
|
248
|
+
className={className}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { useCallback, useMemo } from "react";
|
|
3
|
+
import { ListView } from "../ListView";
|
|
4
|
+
import { Typography } from "../../components/Typography";
|
|
5
|
+
import { Checkbox } from "../../components/Checkbox";
|
|
6
|
+
import { DefaultCellRenderer } from "./DefaultCellRenderer";
|
|
7
|
+
import { cls } from "../../util";
|
|
8
|
+
import type {
|
|
9
|
+
CollectionPropertyConfig,
|
|
10
|
+
CollectionDataController,
|
|
11
|
+
CellRendererOverride,
|
|
12
|
+
CollectionViewSize,
|
|
13
|
+
CollectionSelectionController,
|
|
14
|
+
} from "./CollectionViewTypes";
|
|
15
|
+
|
|
16
|
+
import { getValueInPath } from "./utils";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Props for the {@link CollectionListView} component.
|
|
20
|
+
* @group Collection View
|
|
21
|
+
*/
|
|
22
|
+
export type CollectionListViewProps<T = Record<string, unknown>> = {
|
|
23
|
+
dataController: CollectionDataController<T>;
|
|
24
|
+
properties: Record<string, CollectionPropertyConfig>;
|
|
25
|
+
propertiesOrder?: string[];
|
|
26
|
+
idProperty?: string;
|
|
27
|
+
titleProperty?: string;
|
|
28
|
+
onRowClick?: (row: T) => void;
|
|
29
|
+
cellRenderer?: CellRendererOverride<T>;
|
|
30
|
+
size?: CollectionViewSize;
|
|
31
|
+
selectionEnabled?: boolean;
|
|
32
|
+
selectionController?: CollectionSelectionController<T>;
|
|
33
|
+
highlightedItems?: T[];
|
|
34
|
+
emptyComponent?: React.ReactNode;
|
|
35
|
+
className?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Headless list view that wraps {@link ListView}.
|
|
40
|
+
*
|
|
41
|
+
* Each row displays a title (bold), subtitle with 1–2 secondary property values,
|
|
42
|
+
* and optional selection support.
|
|
43
|
+
*
|
|
44
|
+
* @group Collection View
|
|
45
|
+
*/
|
|
46
|
+
export function CollectionListView<T extends Record<string, unknown> = Record<string, unknown>>({
|
|
47
|
+
dataController,
|
|
48
|
+
properties,
|
|
49
|
+
propertiesOrder,
|
|
50
|
+
idProperty = "id",
|
|
51
|
+
titleProperty,
|
|
52
|
+
onRowClick,
|
|
53
|
+
cellRenderer,
|
|
54
|
+
size = "m",
|
|
55
|
+
selectionEnabled,
|
|
56
|
+
selectionController,
|
|
57
|
+
highlightedItems,
|
|
58
|
+
emptyComponent,
|
|
59
|
+
className,
|
|
60
|
+
}: CollectionListViewProps<T>) {
|
|
61
|
+
const order = useMemo(
|
|
62
|
+
() => propertiesOrder ?? Object.keys(properties),
|
|
63
|
+
[propertiesOrder, properties]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// ── Resolve title property ───────────────────────────────────────
|
|
67
|
+
const titlePropertyKey = useMemo(() => {
|
|
68
|
+
if (titleProperty) return titleProperty;
|
|
69
|
+
for (const key of order) {
|
|
70
|
+
const prop = properties[key];
|
|
71
|
+
if (prop && prop.type === "string" && !prop.hideFromCollection && !prop.storage) {
|
|
72
|
+
return key;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return idProperty;
|
|
76
|
+
}, [titleProperty, order, properties, idProperty]);
|
|
77
|
+
|
|
78
|
+
// ── Subtitle properties (up to 2) ────────────────────────────────
|
|
79
|
+
const subtitleProperties = useMemo(() => {
|
|
80
|
+
const result: { key: string; property: CollectionPropertyConfig }[] = [];
|
|
81
|
+
for (const key of order) {
|
|
82
|
+
if (key === titlePropertyKey) continue;
|
|
83
|
+
const prop = properties[key];
|
|
84
|
+
if (prop && !prop.hideFromCollection) {
|
|
85
|
+
result.push({ key, property: prop });
|
|
86
|
+
if (result.length >= 2) break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}, [order, properties, titlePropertyKey]);
|
|
91
|
+
|
|
92
|
+
// ── Selection IDs ────────────────────────────────────────────────
|
|
93
|
+
const selectedIds = useMemo(() => {
|
|
94
|
+
if (!selectionController?.selectedItems) return undefined;
|
|
95
|
+
const ids = new Set<string | number>();
|
|
96
|
+
for (const item of selectionController.selectedItems) {
|
|
97
|
+
const id = getValueInPath(item as Record<string, unknown>, idProperty);
|
|
98
|
+
if (id !== undefined && id !== null) {
|
|
99
|
+
ids.add(id as string | number);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return ids;
|
|
103
|
+
}, [selectionController?.selectedItems, idProperty]);
|
|
104
|
+
|
|
105
|
+
// ── Highlighted IDs ──────────────────────────────────────────────
|
|
106
|
+
const highlightedIds = useMemo(() => {
|
|
107
|
+
if (!highlightedItems) return undefined;
|
|
108
|
+
const ids = new Set<string | number>();
|
|
109
|
+
for (const item of highlightedItems) {
|
|
110
|
+
const id = getValueInPath(item as Record<string, unknown>, idProperty);
|
|
111
|
+
if (id !== undefined && id !== null) {
|
|
112
|
+
ids.add(id as string | number);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return ids;
|
|
116
|
+
}, [highlightedItems, idProperty]);
|
|
117
|
+
|
|
118
|
+
// ── Selection handler ────────────────────────────────────────────
|
|
119
|
+
const handleSelectionChange = useCallback(
|
|
120
|
+
(item: T, selected: boolean) => {
|
|
121
|
+
if (!selectionController) return;
|
|
122
|
+
selectionController.setSelectedItems((prev) => {
|
|
123
|
+
if (selected) {
|
|
124
|
+
return [...prev, item];
|
|
125
|
+
}
|
|
126
|
+
const itemId = getValueInPath(item as Record<string, unknown>, idProperty);
|
|
127
|
+
return prev.filter((existing) => {
|
|
128
|
+
const existingId = getValueInPath(existing as Record<string, unknown>, idProperty);
|
|
129
|
+
return existingId !== itemId;
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
[selectionController, idProperty]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// ── Render row ───────────────────────────────────────────────────
|
|
137
|
+
const renderRow = useCallback(
|
|
138
|
+
(params: {
|
|
139
|
+
item: T;
|
|
140
|
+
index: number;
|
|
141
|
+
style: React.CSSProperties;
|
|
142
|
+
className: string;
|
|
143
|
+
selected: boolean;
|
|
144
|
+
highlighted: boolean;
|
|
145
|
+
isLast: boolean;
|
|
146
|
+
onClick: (e: React.MouseEvent) => void;
|
|
147
|
+
onSelectionChange: (selected: boolean) => void;
|
|
148
|
+
}): React.ReactNode => {
|
|
149
|
+
const { item, style, className: rowClassName, selected, highlighted, onClick, onSelectionChange } = params;
|
|
150
|
+
const titleValue = getValueInPath(item as Record<string, unknown>, titlePropertyKey);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div
|
|
154
|
+
style={style}
|
|
155
|
+
className={cls(
|
|
156
|
+
"flex items-center gap-3 px-4 cursor-pointer",
|
|
157
|
+
"hover:bg-surface-50 dark:hover:bg-surface-700",
|
|
158
|
+
"transition-colors duration-100",
|
|
159
|
+
selected && "bg-primary-50 dark:bg-primary-900/20",
|
|
160
|
+
highlighted && "bg-surface-100 dark:bg-surface-800",
|
|
161
|
+
rowClassName
|
|
162
|
+
)}
|
|
163
|
+
onClick={onClick}
|
|
164
|
+
>
|
|
165
|
+
{/* Selection checkbox */}
|
|
166
|
+
{selectionEnabled && (
|
|
167
|
+
<div className="flex-shrink-0" onClick={(e) => e.stopPropagation()}>
|
|
168
|
+
<Checkbox
|
|
169
|
+
checked={selected}
|
|
170
|
+
onCheckedChange={onSelectionChange}
|
|
171
|
+
size="small"
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{/* Content */}
|
|
177
|
+
<div className="flex-1 min-w-0 py-1">
|
|
178
|
+
{/* Title */}
|
|
179
|
+
<Typography variant="subtitle2" noWrap>
|
|
180
|
+
{String(titleValue ?? "")}
|
|
181
|
+
</Typography>
|
|
182
|
+
|
|
183
|
+
{/* Subtitle properties */}
|
|
184
|
+
{subtitleProperties.length > 0 && (
|
|
185
|
+
<div className="flex items-center gap-2">
|
|
186
|
+
{subtitleProperties.map(({ key, property }) => {
|
|
187
|
+
const value = getValueInPath(item as Record<string, unknown>, key);
|
|
188
|
+
|
|
189
|
+
if (cellRenderer) {
|
|
190
|
+
const custom = cellRenderer({
|
|
191
|
+
row: item,
|
|
192
|
+
propertyKey: key,
|
|
193
|
+
property,
|
|
194
|
+
value,
|
|
195
|
+
size: "tiny",
|
|
196
|
+
});
|
|
197
|
+
if (custom !== undefined) {
|
|
198
|
+
return <React.Fragment key={key}>{custom}</React.Fragment>;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<React.Fragment key={key}>
|
|
204
|
+
<DefaultCellRenderer
|
|
205
|
+
row={item}
|
|
206
|
+
propertyKey={key}
|
|
207
|
+
property={property}
|
|
208
|
+
value={value}
|
|
209
|
+
size="tiny"
|
|
210
|
+
/>
|
|
211
|
+
</React.Fragment>
|
|
212
|
+
);
|
|
213
|
+
})}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
[titlePropertyKey, subtitleProperties, cellRenderer, selectionEnabled]
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div className={cls("w-full h-full overflow-auto", className)}>
|
|
225
|
+
<ListView<T>
|
|
226
|
+
data={dataController.data}
|
|
227
|
+
dataLoading={dataController.loading}
|
|
228
|
+
noMoreToLoad={dataController.noMoreToLoad}
|
|
229
|
+
dataLoadingError={dataController.error}
|
|
230
|
+
itemCount={dataController.itemCount}
|
|
231
|
+
setItemCount={dataController.setItemCount}
|
|
232
|
+
pageSize={dataController.pageSize}
|
|
233
|
+
paginationEnabled={dataController.paginationEnabled}
|
|
234
|
+
onItemClick={onRowClick}
|
|
235
|
+
selectedIds={selectedIds}
|
|
236
|
+
highlightedIds={highlightedIds}
|
|
237
|
+
selectionEnabled={!!selectionEnabled}
|
|
238
|
+
onSelectionChange={handleSelectionChange}
|
|
239
|
+
size={size}
|
|
240
|
+
renderRow={renderRow}
|
|
241
|
+
emptyComponent={emptyComponent}
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|