@rebasepro/ui 0.6.1 → 0.7.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/icons/index.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +2058 -16
- 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/icons/index.ts +3 -0
- package/src/index.ts +2 -0
- 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,229 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { useCallback, useMemo } from "react";
|
|
3
|
+
import { VirtualTable } from "../../components/VirtualTable/VirtualTable";
|
|
4
|
+
import type { VirtualTableColumn, CellRendererParams, OnVirtualTableColumnResizeParams } from "../../components/VirtualTable/VirtualTableProps";
|
|
5
|
+
import { DefaultCellRenderer } from "./DefaultCellRenderer";
|
|
6
|
+
import { cls } from "../../util";
|
|
7
|
+
import type {
|
|
8
|
+
CollectionPropertyConfig,
|
|
9
|
+
CollectionDataController,
|
|
10
|
+
CellRendererOverride,
|
|
11
|
+
CollectionViewSize,
|
|
12
|
+
CollectionSelectionController,
|
|
13
|
+
CellRendererProps,
|
|
14
|
+
} from "./CollectionViewTypes";
|
|
15
|
+
|
|
16
|
+
import { getValueInPath } from "./utils";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_COLUMN_WIDTHS: Record<string, number> = {
|
|
19
|
+
string: 200,
|
|
20
|
+
number: 140,
|
|
21
|
+
date: 160,
|
|
22
|
+
boolean: 120,
|
|
23
|
+
array: 200,
|
|
24
|
+
map: 200,
|
|
25
|
+
reference: 200,
|
|
26
|
+
relation: 200,
|
|
27
|
+
geopoint: 180,
|
|
28
|
+
};
|
|
29
|
+
const FALLBACK_COLUMN_WIDTH = 200;
|
|
30
|
+
|
|
31
|
+
const SIZE_TO_ROW_HEIGHT: Record<CollectionViewSize, number> = {
|
|
32
|
+
xs: 36,
|
|
33
|
+
s: 44,
|
|
34
|
+
m: 52,
|
|
35
|
+
l: 64,
|
|
36
|
+
xl: 80,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const SORTABLE_TYPES = new Set(["string", "number", "date"]);
|
|
40
|
+
|
|
41
|
+
type RendererSize = "tiny" | "small" | "medium" | "large";
|
|
42
|
+
|
|
43
|
+
const VIEW_SIZE_TO_RENDERER_SIZE: Record<CollectionViewSize, RendererSize> = {
|
|
44
|
+
xs: "tiny",
|
|
45
|
+
s: "small",
|
|
46
|
+
m: "medium",
|
|
47
|
+
l: "large",
|
|
48
|
+
xl: "large",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Props for the {@link CollectionTableView} component.
|
|
53
|
+
* @group Collection View
|
|
54
|
+
*/
|
|
55
|
+
export type CollectionTableViewProps<T = Record<string, unknown>> = {
|
|
56
|
+
dataController: CollectionDataController<T>;
|
|
57
|
+
properties: Record<string, CollectionPropertyConfig>;
|
|
58
|
+
propertiesOrder?: string[];
|
|
59
|
+
displayedColumnIds?: string[];
|
|
60
|
+
idProperty?: string;
|
|
61
|
+
titleProperty?: string;
|
|
62
|
+
onRowClick?: (row: T) => void;
|
|
63
|
+
cellRenderer?: CellRendererOverride<T>;
|
|
64
|
+
size?: CollectionViewSize;
|
|
65
|
+
selectionEnabled?: boolean;
|
|
66
|
+
selectionController?: CollectionSelectionController<T>;
|
|
67
|
+
highlightedItems?: T[];
|
|
68
|
+
emptyComponent?: React.ReactNode;
|
|
69
|
+
hoverRow?: boolean;
|
|
70
|
+
className?: string;
|
|
71
|
+
onColumnResize?: (params: { key: string; width: number }) => void;
|
|
72
|
+
onColumnsOrderChange?: (keys: string[]) => void;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Headless table view that wraps {@link VirtualTable}.
|
|
77
|
+
*
|
|
78
|
+
* Converts property configurations into virtual table columns and wires up
|
|
79
|
+
* sorting, pagination, and cell rendering without coupling to any data layer.
|
|
80
|
+
*
|
|
81
|
+
* @group Collection View
|
|
82
|
+
*/
|
|
83
|
+
export function CollectionTableView<T extends Record<string, unknown> = Record<string, unknown>>({
|
|
84
|
+
dataController,
|
|
85
|
+
properties,
|
|
86
|
+
propertiesOrder,
|
|
87
|
+
displayedColumnIds,
|
|
88
|
+
onRowClick,
|
|
89
|
+
cellRenderer,
|
|
90
|
+
size = "m",
|
|
91
|
+
emptyComponent,
|
|
92
|
+
hoverRow = true,
|
|
93
|
+
className,
|
|
94
|
+
onColumnResize,
|
|
95
|
+
onColumnsOrderChange,
|
|
96
|
+
}: CollectionTableViewProps<T>) {
|
|
97
|
+
// ── Build columns ────────────────────────────────────────────────
|
|
98
|
+
const columns = useMemo<VirtualTableColumn[]>(() => {
|
|
99
|
+
const order = propertiesOrder ?? Object.keys(properties);
|
|
100
|
+
const filteredKeys = displayedColumnIds
|
|
101
|
+
? order.filter((key) => displayedColumnIds.includes(key))
|
|
102
|
+
: order.filter((key) => {
|
|
103
|
+
const prop = properties[key];
|
|
104
|
+
return prop && !prop.hideFromCollection;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return filteredKeys
|
|
108
|
+
.map((key): VirtualTableColumn | null => {
|
|
109
|
+
const prop = properties[key];
|
|
110
|
+
if (!prop) return null;
|
|
111
|
+
return {
|
|
112
|
+
key,
|
|
113
|
+
width: prop.columnWidth || DEFAULT_COLUMN_WIDTHS[prop.type] || FALLBACK_COLUMN_WIDTH,
|
|
114
|
+
title: prop.name,
|
|
115
|
+
sortable: SORTABLE_TYPES.has(prop.type),
|
|
116
|
+
resizable: true,
|
|
117
|
+
};
|
|
118
|
+
})
|
|
119
|
+
.filter((col): col is VirtualTableColumn => col !== null);
|
|
120
|
+
}, [properties, propertiesOrder, displayedColumnIds]);
|
|
121
|
+
|
|
122
|
+
// ── Row height ───────────────────────────────────────────────────
|
|
123
|
+
const rowHeight = SIZE_TO_ROW_HEIGHT[size];
|
|
124
|
+
const rendererSize = VIEW_SIZE_TO_RENDERER_SIZE[size];
|
|
125
|
+
|
|
126
|
+
// ── Cell renderer ────────────────────────────────────────────────
|
|
127
|
+
const tableCellRenderer = useCallback(
|
|
128
|
+
(params: CellRendererParams<Record<string, unknown>>): React.ReactNode => {
|
|
129
|
+
if (!params.rowData) return null;
|
|
130
|
+
|
|
131
|
+
const propertyConfig = properties[params.column.key];
|
|
132
|
+
if (!propertyConfig) return null;
|
|
133
|
+
|
|
134
|
+
const value = getValueInPath(
|
|
135
|
+
params.rowData,
|
|
136
|
+
params.column.key
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const cellProps: CellRendererProps<T> = {
|
|
140
|
+
row: params.rowData as T,
|
|
141
|
+
propertyKey: params.column.key,
|
|
142
|
+
property: propertyConfig,
|
|
143
|
+
value,
|
|
144
|
+
size: rendererSize,
|
|
145
|
+
width: params.width,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Try custom renderer first
|
|
149
|
+
if (cellRenderer) {
|
|
150
|
+
const custom = cellRenderer(cellProps);
|
|
151
|
+
if (custom !== undefined) {
|
|
152
|
+
return (
|
|
153
|
+
<div className="flex items-center h-full px-1 overflow-hidden">
|
|
154
|
+
{custom}
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className="flex items-center h-full px-1 overflow-hidden">
|
|
162
|
+
<DefaultCellRenderer {...cellProps} />
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
[properties, cellRenderer, rendererSize]
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// ── Row click ────────────────────────────────────────────────────
|
|
170
|
+
const handleRowClick = useCallback(
|
|
171
|
+
({ rowData }: { rowData: Record<string, unknown> }) => {
|
|
172
|
+
onRowClick?.(rowData as T);
|
|
173
|
+
},
|
|
174
|
+
[onRowClick]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// ── Pagination ──────────────────────────────────────────────────
|
|
178
|
+
const handleEndReached = useCallback(() => {
|
|
179
|
+
if (dataController.paginationEnabled && !dataController.noMoreToLoad) {
|
|
180
|
+
const currentCount = dataController.itemCount ?? dataController.pageSize ?? 50;
|
|
181
|
+
const pageSize = dataController.pageSize ?? 50;
|
|
182
|
+
dataController.setItemCount?.(currentCount + pageSize);
|
|
183
|
+
}
|
|
184
|
+
}, [
|
|
185
|
+
dataController.paginationEnabled,
|
|
186
|
+
dataController.noMoreToLoad,
|
|
187
|
+
dataController.itemCount,
|
|
188
|
+
dataController.pageSize,
|
|
189
|
+
dataController.setItemCount,
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
// ── Column resize ───────────────────────────────────────────────
|
|
193
|
+
const handleColumnResize = useCallback(
|
|
194
|
+
(params: OnVirtualTableColumnResizeParams) => {
|
|
195
|
+
onColumnResize?.({ key: params.key, width: params.width });
|
|
196
|
+
},
|
|
197
|
+
[onColumnResize]
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// ── Column reorder ──────────────────────────────────────────────
|
|
201
|
+
const handleColumnsOrderChange = useCallback(
|
|
202
|
+
(newColumns: VirtualTableColumn[]) => {
|
|
203
|
+
onColumnsOrderChange?.(newColumns.map((col) => col.key));
|
|
204
|
+
},
|
|
205
|
+
[onColumnsOrderChange]
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<VirtualTable
|
|
210
|
+
data={dataController.data}
|
|
211
|
+
columns={columns}
|
|
212
|
+
cellRenderer={tableCellRenderer}
|
|
213
|
+
rowHeight={rowHeight}
|
|
214
|
+
sortBy={dataController.sortBy}
|
|
215
|
+
onSortByUpdate={dataController.setSortBy}
|
|
216
|
+
onRowClick={handleRowClick}
|
|
217
|
+
onEndReached={handleEndReached}
|
|
218
|
+
onColumnResize={handleColumnResize}
|
|
219
|
+
onColumnsOrderChange={handleColumnsOrderChange}
|
|
220
|
+
loading={dataController.loading}
|
|
221
|
+
emptyComponent={emptyComponent}
|
|
222
|
+
error={dataController.error}
|
|
223
|
+
hoverRow={hoverRow}
|
|
224
|
+
className={cls("h-full", className)}
|
|
225
|
+
onScroll={dataController.onScroll}
|
|
226
|
+
initialScroll={dataController.initialScroll}
|
|
227
|
+
/>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { cls } from "../../util";
|
|
3
|
+
import { Typography } from "../../components/Typography";
|
|
4
|
+
import { CollectionViewToolbar } from "./CollectionViewToolbar";
|
|
5
|
+
import { CollectionTableView } from "./CollectionTableView";
|
|
6
|
+
import { CollectionCardView } from "./CollectionCardView";
|
|
7
|
+
import { CollectionListView } from "./CollectionListView";
|
|
8
|
+
import { CollectionKanbanView } from "./CollectionKanbanView";
|
|
9
|
+
import type {
|
|
10
|
+
CollectionViewMode,
|
|
11
|
+
CollectionViewSize,
|
|
12
|
+
CollectionPropertyConfig,
|
|
13
|
+
CollectionDataController,
|
|
14
|
+
CellRendererOverride,
|
|
15
|
+
CollectionSelectionController,
|
|
16
|
+
KanbanPropertyOption
|
|
17
|
+
} from "./CollectionViewTypes";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Props for the headless {@link CollectionView} component.
|
|
21
|
+
*
|
|
22
|
+
* This component is data-agnostic — it renders collection data based on
|
|
23
|
+
* property configurations and callbacks, with ZERO coupling to any entity
|
|
24
|
+
* or data layer.
|
|
25
|
+
*
|
|
26
|
+
* @group Collection View
|
|
27
|
+
*/
|
|
28
|
+
export interface CollectionViewProps<T = Record<string, unknown>> {
|
|
29
|
+
// ── Data ──────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Data controller providing rows, loading state, pagination,
|
|
33
|
+
* filter/sort/search state and setters.
|
|
34
|
+
*/
|
|
35
|
+
dataController: CollectionDataController<T>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Property definitions keyed by property key.
|
|
39
|
+
* Drives column rendering, cell formatting, and filter UI.
|
|
40
|
+
*/
|
|
41
|
+
properties: Record<string, CollectionPropertyConfig>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Display order of properties. If omitted, uses Object.keys(properties).
|
|
45
|
+
*/
|
|
46
|
+
propertiesOrder?: string[];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Subset of property keys to actually display as columns.
|
|
50
|
+
* If omitted, all non-hidden properties are shown.
|
|
51
|
+
*/
|
|
52
|
+
displayedColumnIds?: string[];
|
|
53
|
+
|
|
54
|
+
// ── Identity ──────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/** Property key used as row identifier. Default: "id" */
|
|
57
|
+
idProperty?: string;
|
|
58
|
+
|
|
59
|
+
/** Property key used as the display title in card/list views */
|
|
60
|
+
titleProperty?: string;
|
|
61
|
+
|
|
62
|
+
// ── View configuration ────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/** Title displayed above the collection. `false` to hide. */
|
|
65
|
+
title?: string | false;
|
|
66
|
+
|
|
67
|
+
/** Controlled view mode. If omitted, internal state is used. */
|
|
68
|
+
viewMode?: CollectionViewMode;
|
|
69
|
+
|
|
70
|
+
/** Default view mode when uncontrolled. Default: "list" */
|
|
71
|
+
defaultViewMode?: CollectionViewMode;
|
|
72
|
+
|
|
73
|
+
/** Which view modes are available in the toggle */
|
|
74
|
+
enabledViews?: CollectionViewMode[];
|
|
75
|
+
|
|
76
|
+
/** Callback when view mode changes */
|
|
77
|
+
onViewModeChange?: (mode: CollectionViewMode) => void;
|
|
78
|
+
|
|
79
|
+
// ── Size ──────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/** Controlled size. If omitted, internal state is used. */
|
|
82
|
+
size?: CollectionViewSize;
|
|
83
|
+
|
|
84
|
+
/** Default size. Default: "m" */
|
|
85
|
+
defaultSize?: CollectionViewSize;
|
|
86
|
+
|
|
87
|
+
/** Callback when size changes */
|
|
88
|
+
onSizeChange?: (size: CollectionViewSize) => void;
|
|
89
|
+
|
|
90
|
+
// ── Kanban ─────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/** Property key for kanban column grouping */
|
|
93
|
+
kanbanProperty?: string;
|
|
94
|
+
|
|
95
|
+
/** Available kanban property options (shown in view toggle) */
|
|
96
|
+
kanbanPropertyOptions?: KanbanPropertyOption[];
|
|
97
|
+
|
|
98
|
+
/** Callback when kanban column property changes */
|
|
99
|
+
onKanbanPropertyChange?: (property: string) => void;
|
|
100
|
+
|
|
101
|
+
// ── Interactions ──────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/** Called when a row is clicked */
|
|
104
|
+
onRowClick?: (row: T) => void;
|
|
105
|
+
|
|
106
|
+
/** Called when the "create" button is clicked */
|
|
107
|
+
onRowCreate?: (defaults?: Record<string, unknown>) => void;
|
|
108
|
+
|
|
109
|
+
/** Whether creation is allowed (shows the + button) */
|
|
110
|
+
canCreate?: boolean;
|
|
111
|
+
|
|
112
|
+
// ── Selection ─────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/** Enable row selection checkboxes */
|
|
115
|
+
selectionEnabled?: boolean;
|
|
116
|
+
|
|
117
|
+
/** Selection controller */
|
|
118
|
+
selectionController?: CollectionSelectionController<T>;
|
|
119
|
+
|
|
120
|
+
/** Highlighted items (e.g. recently clicked) */
|
|
121
|
+
highlightedItems?: T[];
|
|
122
|
+
|
|
123
|
+
/** Callback for bulk delete */
|
|
124
|
+
onMultipleDelete?: (rows: T[]) => void;
|
|
125
|
+
|
|
126
|
+
// ── Custom rendering ──────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Custom cell renderer override. Return undefined to use default.
|
|
130
|
+
* This is how the connected wrapper injects PropertyPreview.
|
|
131
|
+
*/
|
|
132
|
+
cellRenderer?: CellRendererOverride<T>;
|
|
133
|
+
|
|
134
|
+
/** Custom empty state component */
|
|
135
|
+
emptyComponent?: React.ReactNode;
|
|
136
|
+
|
|
137
|
+
// ── Toolbar ───────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/** Actions rendered at the start (left) of the toolbar */
|
|
140
|
+
toolbarActionsStart?: React.ReactNode;
|
|
141
|
+
|
|
142
|
+
/** Actions rendered at the end (right) of the toolbar */
|
|
143
|
+
toolbarActionsEnd?: React.ReactNode;
|
|
144
|
+
|
|
145
|
+
/** Hide the built-in toolbar entirely */
|
|
146
|
+
hideToolbar?: boolean;
|
|
147
|
+
|
|
148
|
+
// ── Table-specific ────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/** Enable row hover highlight in table view */
|
|
151
|
+
hoverRow?: boolean;
|
|
152
|
+
|
|
153
|
+
/** Callback when table columns are resized */
|
|
154
|
+
onColumnResize?: (params: { key: string; width: number }) => void;
|
|
155
|
+
|
|
156
|
+
/** Callback when table columns are reordered */
|
|
157
|
+
onColumnsOrderChange?: (keys: string[]) => void;
|
|
158
|
+
|
|
159
|
+
// ── Style ─────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
/** CSS class for the outer container */
|
|
162
|
+
className?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const DEFAULT_ENABLED_VIEWS: CollectionViewMode[] = ["list", "table", "cards", "kanban"];
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A data-agnostic collection view component that orchestrates table, card,
|
|
169
|
+
* list, and kanban views — driven entirely by props.
|
|
170
|
+
*
|
|
171
|
+
* Zero imports from any entity or data layer. This component lives in
|
|
172
|
+
* `@rebasepro/ui` and can be used with any data source.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```tsx
|
|
176
|
+
* import { CollectionView } from "@rebasepro/ui";
|
|
177
|
+
*
|
|
178
|
+
* <CollectionView
|
|
179
|
+
* dataController={{ data: myRows, loading: false, noMoreToLoad: true }}
|
|
180
|
+
* properties={{
|
|
181
|
+
* name: { type: "string", name: "Name" },
|
|
182
|
+
* email: { type: "string", name: "Email" },
|
|
183
|
+
* }}
|
|
184
|
+
* title="Contacts"
|
|
185
|
+
* onRowClick={(row) => openDetail(row.id)}
|
|
186
|
+
* />
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* @group Collection View
|
|
190
|
+
*/
|
|
191
|
+
export function CollectionView<T extends Record<string, unknown> = Record<string, unknown>>({
|
|
192
|
+
dataController,
|
|
193
|
+
properties,
|
|
194
|
+
propertiesOrder,
|
|
195
|
+
displayedColumnIds,
|
|
196
|
+
idProperty = "id",
|
|
197
|
+
titleProperty,
|
|
198
|
+
title,
|
|
199
|
+
viewMode: viewModeProp,
|
|
200
|
+
defaultViewMode = "list",
|
|
201
|
+
enabledViews = DEFAULT_ENABLED_VIEWS,
|
|
202
|
+
onViewModeChange: onViewModeChangeProp,
|
|
203
|
+
size: sizeProp,
|
|
204
|
+
defaultSize = "m",
|
|
205
|
+
onSizeChange: onSizeChangeProp,
|
|
206
|
+
kanbanProperty: kanbanPropertyProp,
|
|
207
|
+
kanbanPropertyOptions,
|
|
208
|
+
onKanbanPropertyChange: onKanbanPropertyChangeProp,
|
|
209
|
+
onRowClick,
|
|
210
|
+
onRowCreate,
|
|
211
|
+
canCreate = true,
|
|
212
|
+
selectionEnabled,
|
|
213
|
+
selectionController,
|
|
214
|
+
highlightedItems,
|
|
215
|
+
onMultipleDelete,
|
|
216
|
+
cellRenderer,
|
|
217
|
+
emptyComponent,
|
|
218
|
+
toolbarActionsStart,
|
|
219
|
+
toolbarActionsEnd,
|
|
220
|
+
hideToolbar,
|
|
221
|
+
hoverRow = true,
|
|
222
|
+
onColumnResize,
|
|
223
|
+
onColumnsOrderChange,
|
|
224
|
+
className
|
|
225
|
+
}: CollectionViewProps<T>) {
|
|
226
|
+
|
|
227
|
+
// ── View mode state (uncontrolled with controlled override) ───────
|
|
228
|
+
|
|
229
|
+
const [internalViewMode, setInternalViewMode] = useState<CollectionViewMode>(defaultViewMode);
|
|
230
|
+
const viewMode = viewModeProp ?? internalViewMode;
|
|
231
|
+
|
|
232
|
+
const onViewModeChange = useCallback((mode: CollectionViewMode) => {
|
|
233
|
+
setInternalViewMode(mode);
|
|
234
|
+
onViewModeChangeProp?.(mode);
|
|
235
|
+
}, [onViewModeChangeProp]);
|
|
236
|
+
|
|
237
|
+
// ── Size state (uncontrolled with controlled override) ────────────
|
|
238
|
+
|
|
239
|
+
const [internalSize, setInternalSize] = useState<CollectionViewSize>(defaultSize);
|
|
240
|
+
const size = sizeProp ?? internalSize;
|
|
241
|
+
|
|
242
|
+
const onSizeChange = useCallback((s: CollectionViewSize) => {
|
|
243
|
+
setInternalSize(s);
|
|
244
|
+
onSizeChangeProp?.(s);
|
|
245
|
+
}, [onSizeChangeProp]);
|
|
246
|
+
|
|
247
|
+
// ── Kanban property state ─────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
const [internalKanbanProperty, setInternalKanbanProperty] = useState<string | undefined>(kanbanPropertyProp);
|
|
250
|
+
const kanbanProperty = kanbanPropertyProp ?? internalKanbanProperty;
|
|
251
|
+
|
|
252
|
+
const onKanbanPropertyChange = useCallback((property: string) => {
|
|
253
|
+
setInternalKanbanProperty(property);
|
|
254
|
+
onKanbanPropertyChangeProp?.(property);
|
|
255
|
+
}, [onKanbanPropertyChangeProp]);
|
|
256
|
+
|
|
257
|
+
// Auto-detect kanban property if not provided
|
|
258
|
+
const resolvedKanbanProperty = useMemo(() => {
|
|
259
|
+
if (kanbanProperty) return kanbanProperty;
|
|
260
|
+
// Find first enum property
|
|
261
|
+
const order = propertiesOrder ?? Object.keys(properties);
|
|
262
|
+
for (const key of order) {
|
|
263
|
+
const prop = properties[key];
|
|
264
|
+
if (prop && prop.type === "string" && prop.enum) {
|
|
265
|
+
return key;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}, [kanbanProperty, properties, propertiesOrder]);
|
|
270
|
+
|
|
271
|
+
// Build kanban property options from properties if not provided
|
|
272
|
+
const resolvedKanbanPropertyOptions = useMemo((): KanbanPropertyOption[] => {
|
|
273
|
+
if (kanbanPropertyOptions) return kanbanPropertyOptions;
|
|
274
|
+
const options: KanbanPropertyOption[] = [];
|
|
275
|
+
const order = propertiesOrder ?? Object.keys(properties);
|
|
276
|
+
for (const key of order) {
|
|
277
|
+
const prop = properties[key];
|
|
278
|
+
if (prop && prop.type === "string" && prop.enum) {
|
|
279
|
+
options.push({ key, label: prop.name });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return options;
|
|
283
|
+
}, [kanbanPropertyOptions, properties, propertiesOrder]);
|
|
284
|
+
|
|
285
|
+
// ── Render ────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
const sharedProps = {
|
|
288
|
+
dataController,
|
|
289
|
+
properties,
|
|
290
|
+
propertiesOrder,
|
|
291
|
+
idProperty,
|
|
292
|
+
titleProperty,
|
|
293
|
+
onRowClick,
|
|
294
|
+
cellRenderer,
|
|
295
|
+
selectionEnabled,
|
|
296
|
+
selectionController,
|
|
297
|
+
highlightedItems,
|
|
298
|
+
emptyComponent,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
function renderView() {
|
|
302
|
+
switch (viewMode) {
|
|
303
|
+
case "table":
|
|
304
|
+
return (
|
|
305
|
+
<CollectionTableView<T>
|
|
306
|
+
{...sharedProps}
|
|
307
|
+
displayedColumnIds={displayedColumnIds}
|
|
308
|
+
size={size}
|
|
309
|
+
hoverRow={hoverRow}
|
|
310
|
+
onColumnResize={onColumnResize}
|
|
311
|
+
onColumnsOrderChange={onColumnsOrderChange}
|
|
312
|
+
/>
|
|
313
|
+
);
|
|
314
|
+
case "cards":
|
|
315
|
+
return (
|
|
316
|
+
<CollectionCardView<T>
|
|
317
|
+
{...sharedProps}
|
|
318
|
+
size={size}
|
|
319
|
+
/>
|
|
320
|
+
);
|
|
321
|
+
case "kanban":
|
|
322
|
+
return resolvedKanbanProperty ? (
|
|
323
|
+
<CollectionKanbanView<T>
|
|
324
|
+
{...sharedProps}
|
|
325
|
+
kanbanProperty={resolvedKanbanProperty}
|
|
326
|
+
onRowCreate={onRowCreate}
|
|
327
|
+
/>
|
|
328
|
+
) : (
|
|
329
|
+
<div className="flex items-center justify-center p-8">
|
|
330
|
+
<Typography variant="body2" color="secondary">
|
|
331
|
+
No enum property available for kanban grouping.
|
|
332
|
+
</Typography>
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
335
|
+
case "list":
|
|
336
|
+
default:
|
|
337
|
+
return (
|
|
338
|
+
<CollectionListView<T>
|
|
339
|
+
{...sharedProps}
|
|
340
|
+
size={size}
|
|
341
|
+
/>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div
|
|
348
|
+
className={cls(
|
|
349
|
+
"overflow-hidden h-full w-full rounded-md flex flex-col",
|
|
350
|
+
"dark:bg-surface-800",
|
|
351
|
+
className
|
|
352
|
+
)}
|
|
353
|
+
>
|
|
354
|
+
{/* Error banner */}
|
|
355
|
+
{dataController.error && dataController.data.length > 0 && (
|
|
356
|
+
<div className="flex items-center gap-4 px-4 py-2 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800 flex-shrink-0">
|
|
357
|
+
<Typography variant="body2" className="text-red-700 dark:text-red-300 flex-1">
|
|
358
|
+
<strong>Warning:</strong> {dataController.error.message || "Failed to update data."}
|
|
359
|
+
</Typography>
|
|
360
|
+
</div>
|
|
361
|
+
)}
|
|
362
|
+
|
|
363
|
+
{/* Toolbar */}
|
|
364
|
+
{!hideToolbar && (
|
|
365
|
+
<CollectionViewToolbar
|
|
366
|
+
viewMode={viewMode}
|
|
367
|
+
onViewModeChange={enabledViews.length > 1 ? onViewModeChange : undefined}
|
|
368
|
+
enabledViews={enabledViews}
|
|
369
|
+
size={size}
|
|
370
|
+
onSizeChange={onSizeChange}
|
|
371
|
+
searchString={dataController.searchString}
|
|
372
|
+
onSearchChange={dataController.setSearchString}
|
|
373
|
+
loading={dataController.loading}
|
|
374
|
+
actionsStart={toolbarActionsStart}
|
|
375
|
+
actionsEnd={toolbarActionsEnd}
|
|
376
|
+
kanbanPropertyOptions={viewMode === "kanban" ? resolvedKanbanPropertyOptions : undefined}
|
|
377
|
+
selectedKanbanProperty={resolvedKanbanProperty}
|
|
378
|
+
onKanbanPropertyChange={onKanbanPropertyChange}
|
|
379
|
+
/>
|
|
380
|
+
)}
|
|
381
|
+
|
|
382
|
+
{/* View */}
|
|
383
|
+
<div className="flex-1 overflow-hidden">
|
|
384
|
+
{renderView()}
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|