@_linked/shape-ui 2.0.0 → 2.2.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/lib/esm/columns.d.ts +58 -0
- package/lib/esm/columns.js +173 -0
- package/lib/esm/columns.js.map +1 -0
- package/lib/esm/components/AddInstanceForms.d.ts +38 -0
- package/lib/esm/components/AddInstanceForms.js +143 -0
- package/lib/esm/components/AddInstanceForms.js.map +1 -0
- package/lib/esm/components/AddInstanceForms.module.css +12 -0
- package/lib/esm/components/BatchEditDrawer.d.ts +13 -0
- package/lib/esm/components/BatchEditDrawer.js +122 -0
- package/lib/esm/components/BatchEditDrawer.js.map +1 -0
- package/lib/esm/components/BatchEditDrawer.module.css +193 -0
- package/lib/esm/components/CustomMultiSelect.d.ts +26 -0
- package/lib/esm/components/CustomMultiSelect.js +344 -0
- package/lib/esm/components/CustomMultiSelect.js.map +1 -0
- package/lib/esm/components/CustomMultiSelect.module.css +291 -0
- package/lib/esm/components/DynamicFormField.d.ts +21 -0
- package/lib/esm/components/DynamicFormField.js +149 -0
- package/lib/esm/components/DynamicFormField.js.map +1 -0
- package/lib/esm/components/DynamicFormField.module.css +169 -0
- package/lib/esm/components/EditInstanceForms.d.ts +15 -0
- package/lib/esm/components/EditInstanceForms.js +118 -0
- package/lib/esm/components/EditInstanceForms.js.map +1 -0
- package/lib/esm/components/FilterBadge.d.ts +20 -0
- package/lib/esm/components/FilterBadge.js +26 -0
- package/lib/esm/components/FilterBadge.js.map +1 -0
- package/lib/esm/components/FilterBadge.module.css +25 -0
- package/lib/esm/components/FormField.d.ts +18 -0
- package/lib/esm/components/FormField.js +9 -0
- package/lib/esm/components/FormField.js.map +1 -0
- package/lib/esm/components/FormField.module.css +57 -0
- package/lib/esm/components/InstanceDeletionDialog.d.ts +17 -0
- package/lib/esm/components/InstanceDeletionDialog.js +83 -0
- package/lib/esm/components/InstanceDeletionDialog.js.map +1 -0
- package/lib/esm/components/InstanceDeletionDialog.module.css +59 -0
- package/lib/esm/components/InstanceOverview.d.ts +37 -0
- package/lib/esm/components/InstanceOverview.js +233 -0
- package/lib/esm/components/InstanceOverview.js.map +1 -0
- package/lib/esm/components/InstanceOverview.module.css +35 -0
- package/lib/esm/components/InstanceView.d.ts +29 -0
- package/lib/esm/components/InstanceView.js +97 -0
- package/lib/esm/components/InstanceView.js.map +1 -0
- package/lib/esm/components/InstanceView.module.css +157 -0
- package/lib/esm/components/NodeBadge.d.ts +25 -0
- package/lib/esm/components/NodeBadge.js +29 -0
- package/lib/esm/components/NodeBadge.js.map +1 -0
- package/lib/esm/components/NodeValuesEditor.d.ts +15 -0
- package/lib/esm/components/NodeValuesEditor.js +29 -0
- package/lib/esm/components/NodeValuesEditor.js.map +1 -0
- package/lib/esm/components/NodeValuesEditor.module.css +4 -0
- package/lib/esm/components/ReactTable.d.ts +50 -0
- package/lib/esm/components/ReactTable.js +370 -0
- package/lib/esm/components/ReactTable.js.map +1 -0
- package/lib/esm/components/ReactTable.module.css +950 -0
- package/lib/esm/components/TextfieldEditor.d.ts +1 -2
- package/lib/esm/components/TextfieldEditor.js +2 -2
- package/lib/esm/components/TextfieldEditor.js.map +1 -1
- package/lib/esm/components/forms.module.css +96 -0
- package/lib/esm/host.d.ts +196 -0
- package/lib/esm/host.js +25 -0
- package/lib/esm/host.js.map +1 -0
- package/lib/esm/hostContext.d.ts +34 -0
- package/lib/esm/hostContext.js +95 -0
- package/lib/esm/hostContext.js.map +1 -0
- package/lib/esm/index.d.ts +64 -10
- package/lib/esm/index.js +63 -14
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/read.d.ts +48 -0
- package/lib/esm/read.js +96 -0
- package/lib/esm/read.js.map +1 -0
- package/lib/esm/search.d.ts +32 -0
- package/lib/esm/search.js +87 -0
- package/lib/esm/search.js.map +1 -0
- package/lib/esm/shape/contracts.d.ts +58 -0
- package/lib/esm/shape/contracts.js +15 -0
- package/lib/esm/shape/contracts.js.map +1 -0
- package/lib/esm/shape/naming.d.ts +30 -0
- package/lib/esm/shape/naming.js +52 -0
- package/lib/esm/shape/naming.js.map +1 -0
- package/lib/esm/shape/nodeDisplay.d.ts +36 -0
- package/lib/esm/shape/nodeDisplay.js +117 -0
- package/lib/esm/shape/nodeDisplay.js.map +1 -0
- package/lib/esm/shape/propertyTypes.d.ts +12 -0
- package/lib/esm/shape/propertyTypes.js +60 -0
- package/lib/esm/shape/propertyTypes.js.map +1 -0
- package/lib/esm/shape/propertyVisibility.d.ts +56 -0
- package/lib/esm/shape/propertyVisibility.js +112 -0
- package/lib/esm/shape/propertyVisibility.js.map +1 -0
- package/lib/esm/shape/shapeDependencies.d.ts +41 -0
- package/lib/esm/shape/shapeDependencies.js +141 -0
- package/lib/esm/shape/shapeDependencies.js.map +1 -0
- package/lib/esm/shape/tableFilters.d.ts +35 -0
- package/lib/esm/shape/tableFilters.js +171 -0
- package/lib/esm/shape/tableFilters.js.map +1 -0
- package/lib/esm/shape/useFormDependencies.d.ts +38 -0
- package/lib/esm/shape/useFormDependencies.js +178 -0
- package/lib/esm/shape/useFormDependencies.js.map +1 -0
- package/lib/esm/shape/validation.d.ts +15 -0
- package/lib/esm/shape/validation.js +106 -0
- package/lib/esm/shape/validation.js.map +1 -0
- package/lib/esm/tokens.css +25 -0
- package/lib/esm/types.d.ts +22 -0
- package/lib/esm/types.js +1 -0
- package/lib/esm/types.js.map +1 -0
- package/lib/esm/write.d.ts +51 -0
- package/lib/esm/write.js +99 -0
- package/lib/esm/write.js.map +1 -0
- package/package.json +44 -18
- package/src/backend.ts +1 -0
- package/src/columns.regression.test.ts +143 -0
- package/src/columns.test.ts +200 -0
- package/src/columns.ts +228 -0
- package/src/components/AddInstanceForms.module.css +12 -0
- package/src/components/AddInstanceForms.tsx +321 -0
- package/src/components/AvatarEditor.tsx +34 -0
- package/src/components/BatchEditDrawer.module.css +193 -0
- package/src/components/BatchEditDrawer.tsx +295 -0
- package/src/components/CheckboxEditor.module.css +6 -0
- package/src/components/CheckboxEditor.tsx +86 -0
- package/src/components/CustomMultiSelect.module.css +291 -0
- package/src/components/CustomMultiSelect.tsx +527 -0
- package/src/components/DateEditor.tsx +50 -0
- package/src/components/DynamicFormField.module.css +169 -0
- package/src/components/DynamicFormField.tsx +357 -0
- package/src/components/EditInstanceForms.tsx +260 -0
- package/src/components/FilterBadge.module.css +25 -0
- package/src/components/FilterBadge.tsx +70 -0
- package/src/components/FormField.module.css +57 -0
- package/src/components/FormField.tsx +59 -0
- package/src/components/ImageValueEditor.module.css +3 -0
- package/src/components/ImageValueEditor.tsx +82 -0
- package/src/components/InstanceDeletionDialog.module.css +59 -0
- package/src/components/InstanceDeletionDialog.tsx +175 -0
- package/src/components/InstanceOverview.module.css +35 -0
- package/src/components/InstanceOverview.tsx +374 -0
- package/src/components/InstanceView.module.css +157 -0
- package/src/components/InstanceView.tsx +212 -0
- package/src/components/NodeBadge.tsx +60 -0
- package/src/components/NodeValuesEditor.module.css +4 -0
- package/src/components/NodeValuesEditor.tsx +64 -0
- package/src/components/RadioButtonEditor.module.css +22 -0
- package/src/components/RadioButtonEditor.tsx +96 -0
- package/src/components/ReactTable.module.css +950 -0
- package/src/components/ReactTable.tsx +991 -0
- package/src/components/SelectEditor.tsx +85 -0
- package/src/components/ShapeTable.module.css +394 -0
- package/src/components/SwitchEditor.tsx +60 -0
- package/src/components/TextareaEditor.tsx +55 -0
- package/src/components/TextfieldEditor.tsx +67 -0
- package/src/components/ToggleEditor.tsx +63 -0
- package/src/components/forms.module.css +96 -0
- package/src/host.ts +226 -0
- package/src/hostContext.tsx +114 -0
- package/src/index.ts +99 -0
- package/src/package.ts +21 -0
- package/src/read.ts +148 -0
- package/src/search.test.ts +174 -0
- package/src/search.ts +108 -0
- package/src/shape/contracts.ts +66 -0
- package/src/shape/naming.ts +52 -0
- package/src/shape/nodeDisplay.ts +114 -0
- package/src/shape/propertyTypes.ts +86 -0
- package/src/shape/propertyVisibility.ts +180 -0
- package/src/shape/shapeDependencies.ts +183 -0
- package/src/shape/tableFilters.ts +223 -0
- package/src/shape/types.ts +1158 -0
- package/src/shape/useFormDependencies.ts +257 -0
- package/src/shape/validation.ts +121 -0
- package/src/tokens.css +25 -0
- package/src/types.ts +20 -0
- package/src/utils/helper.ts +35 -0
- package/src/write.test.ts +174 -0
- package/src/write.ts +114 -0
- package/.changeset/README.md +0 -20
- package/.changeset/config.json +0 -14
- package/.github/workflows/changeset-check.yml +0 -44
- package/.github/workflows/ci.yml +0 -43
- package/.github/workflows/publish.yml +0 -158
- package/CHANGELOG.md +0 -30
- package/lib/esm/components/AvatarEditor.d.ts +0 -20
- package/lib/esm/components/AvatarEditor.js +0 -7
- package/lib/esm/components/AvatarEditor.js.map +0 -1
- package/lib/esm/components/CheckboxEditor.d.ts +0 -21
- package/lib/esm/components/CheckboxEditor.js +0 -44
- package/lib/esm/components/CheckboxEditor.js.map +0 -1
- package/lib/esm/components/DateEditor.d.ts +0 -20
- package/lib/esm/components/DateEditor.js +0 -22
- package/lib/esm/components/DateEditor.js.map +0 -1
- package/lib/esm/components/RadioButtonEditor.d.ts +0 -23
- package/lib/esm/components/RadioButtonEditor.js +0 -41
- package/lib/esm/components/RadioButtonEditor.js.map +0 -1
- package/lib/esm/components/SelectEditor.d.ts +0 -21
- package/lib/esm/components/SelectEditor.js +0 -30
- package/lib/esm/components/SelectEditor.js.map +0 -1
- package/lib/esm/components/SwitchEditor.d.ts +0 -21
- package/lib/esm/components/SwitchEditor.js +0 -23
- package/lib/esm/components/SwitchEditor.js.map +0 -1
- package/lib/esm/components/TextareaEditor.d.ts +0 -23
- package/lib/esm/components/TextareaEditor.js +0 -15
- package/lib/esm/components/TextareaEditor.js.map +0 -1
- package/lib/esm/components/ToggleEditor.d.ts +0 -22
- package/lib/esm/components/ToggleEditor.js +0 -23
- package/lib/esm/components/ToggleEditor.js.map +0 -1
- package/lib/esm/ontologies/lincd-ui.d.ts +0 -6
- package/lib/esm/ontologies/lincd-ui.js +0 -18
- package/lib/esm/ontologies/lincd-ui.js.map +0 -1
- package/lib/esm/package.d.ts +0 -3
- package/lib/esm/package.js +0 -4
- package/lib/esm/package.js.map +0 -1
- package/tsconfig-cjs.json +0 -8
- package/tsconfig-esm.json +0 -9
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ColumnDef,
|
|
3
|
+
flexRender,
|
|
4
|
+
getCoreRowModel,
|
|
5
|
+
getFilteredRowModel,
|
|
6
|
+
getPaginationRowModel,
|
|
7
|
+
getSortedRowModel,
|
|
8
|
+
useReactTable,
|
|
9
|
+
} from '@tanstack/react-table';
|
|
10
|
+
import type { ShapeInstancesQueryConfig } from '../shape/contracts.js';
|
|
11
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
12
|
+
import { Button } from '@_linked/primitives/components/Button';
|
|
13
|
+
import { Checkbox } from '@_linked/primitives/components/Checkbox';
|
|
14
|
+
import { Drawer } from '@_linked/primitives/components/Drawer';
|
|
15
|
+
import { IconButton } from '@_linked/primitives/components/IconButton';
|
|
16
|
+
import { Input } from '@_linked/primitives/components/Input';
|
|
17
|
+
import { Label } from '@_linked/primitives/components/Label';
|
|
18
|
+
import { Select } from '@_linked/primitives/components/Select';
|
|
19
|
+
import { Typography } from '@_linked/primitives/components/Typography';
|
|
20
|
+
import type { PropertyShapeWire, NodeShapeWire } from '@_linked/core/shapes/nodeShapeWire';
|
|
21
|
+
import { ShapeSet } from '@_linked/core/collections/ShapeSet';
|
|
22
|
+
import { cl } from '@_linked/react/utils/ClassNames';
|
|
23
|
+
import React from 'react';
|
|
24
|
+
import { getNodeDisplay, humanizeEnumLabel, enumOptions } from '../shape/nodeDisplay.js';
|
|
25
|
+
import { formatShapeLabel } from '../shape/naming.js';
|
|
26
|
+
import {
|
|
27
|
+
tableStaggerContainer,
|
|
28
|
+
tableRowItem,
|
|
29
|
+
useReducedMotion,
|
|
30
|
+
} from '@_linked/primitives/motion';
|
|
31
|
+
import { Icons } from '@_linked/icons';
|
|
32
|
+
import { Spinner } from '@_linked/primitives/components/Spinner';
|
|
33
|
+
import {FilterBadge} from './FilterBadge.js';
|
|
34
|
+
import { TableRowSkeleton } from '@_linked/primitives/components/SkeletonLoader';
|
|
35
|
+
import { TruncatedText } from '@_linked/primitives/components/TruncatedText';
|
|
36
|
+
import {
|
|
37
|
+
type TableFilter,
|
|
38
|
+
applyAllFilters,
|
|
39
|
+
generateFilterId,
|
|
40
|
+
getOperatorsForProperty,
|
|
41
|
+
formatFilterLabel,
|
|
42
|
+
type FilterOperator,
|
|
43
|
+
} from '../shape/tableFilters.js';
|
|
44
|
+
import { getPropertyUiType } from '../shape/propertyTypes.js';
|
|
45
|
+
import { useDataManagerHost } from '../hostContext.js';
|
|
46
|
+
import type { TableMode } from '../columns.js';
|
|
47
|
+
import type { InstanceDeletionResult, InstanceDeletionTarget } from '../shape/contracts.js';
|
|
48
|
+
import type { BatchResult } from './InstanceOverview.js';
|
|
49
|
+
import BatchEditDrawer from './BatchEditDrawer.js';
|
|
50
|
+
import { InstanceDeletionDialog, type InstanceDeletionDialogProps } from './InstanceDeletionDialog.js';
|
|
51
|
+
import style from './ReactTable.module.css';
|
|
52
|
+
|
|
53
|
+
export interface ReactTableProps {
|
|
54
|
+
data: any[] | ShapeSet<any>;
|
|
55
|
+
columns: ColumnDef<any>[];
|
|
56
|
+
toggleSelectAll: () => void;
|
|
57
|
+
isRowSelected: (uri: string) => boolean;
|
|
58
|
+
selectedUris: Set<string>;
|
|
59
|
+
toggleRowSelection: (uri: string) => void;
|
|
60
|
+
clearSelection: () => void;
|
|
61
|
+
removeFromSelection: (uris: string[]) => void;
|
|
62
|
+
hasSelection: boolean;
|
|
63
|
+
shape: NodeShapeWire;
|
|
64
|
+
properties?: Record<string, PropertyShapeWire>;
|
|
65
|
+
isLoading?: boolean;
|
|
66
|
+
config: ShapeInstancesQueryConfig;
|
|
67
|
+
setConfig: (updated: ShapeInstancesQueryConfig) => void;
|
|
68
|
+
isPickMode?: boolean;
|
|
69
|
+
onPickInstance?: (instance: { id: string; label: string; image?: string }) => void;
|
|
70
|
+
onPickInstances?: (instances: { id: string; label: string; image?: string }[]) => void;
|
|
71
|
+
pickMaxCount?: number;
|
|
72
|
+
pickAlreadySelected?: { id: string; label: string; image?: string }[];
|
|
73
|
+
deletionProjectId?: string;
|
|
74
|
+
previewDeletion?: InstanceDeletionDialogProps['previewDeletion'];
|
|
75
|
+
executeDeletion?: InstanceDeletionDialogProps['executeDeletion'];
|
|
76
|
+
onInstancesDeleted?: (uris: string[]) => void;
|
|
77
|
+
onBatchUpdate?: (changes: Record<string, any>, uris: string[]) => Promise<BatchResult>;
|
|
78
|
+
tableMode?: TableMode;
|
|
79
|
+
onTableModeChange?: (mode: TableMode) => void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ReactTable({
|
|
83
|
+
data,
|
|
84
|
+
columns,
|
|
85
|
+
toggleSelectAll,
|
|
86
|
+
isRowSelected,
|
|
87
|
+
selectedUris,
|
|
88
|
+
toggleRowSelection,
|
|
89
|
+
clearSelection,
|
|
90
|
+
removeFromSelection,
|
|
91
|
+
hasSelection,
|
|
92
|
+
shape,
|
|
93
|
+
properties: propertiesMap,
|
|
94
|
+
isLoading = false,
|
|
95
|
+
config,
|
|
96
|
+
setConfig,
|
|
97
|
+
isPickMode = false,
|
|
98
|
+
onPickInstance,
|
|
99
|
+
onPickInstances,
|
|
100
|
+
pickMaxCount,
|
|
101
|
+
pickAlreadySelected,
|
|
102
|
+
deletionProjectId,
|
|
103
|
+
previewDeletion,
|
|
104
|
+
executeDeletion,
|
|
105
|
+
onInstancesDeleted,
|
|
106
|
+
onBatchUpdate,
|
|
107
|
+
tableMode,
|
|
108
|
+
onTableModeChange,
|
|
109
|
+
}: ReactTableProps) {
|
|
110
|
+
// ─── Filter state ──────────────────────────────────────────────────
|
|
111
|
+
const [filters, setFilters] = React.useState<TableFilter[]>([]);
|
|
112
|
+
const [selectedColumn, setSelectedColumn] = React.useState('');
|
|
113
|
+
const [selectedOperator, setSelectedOperator] = React.useState<FilterOperator | ''>('');
|
|
114
|
+
const [filterValue, setFilterValue] = React.useState('');
|
|
115
|
+
const [enumSelections, setEnumSelections] = React.useState<string[]>([]);
|
|
116
|
+
const reducedMotion = useReducedMotion();
|
|
117
|
+
// Row actions go through the host. Building `${ROUTES.data.path}/…/edit?s0=…` here
|
|
118
|
+
// assumed CN's URL layout; where "edit this row" leads is the host's decision.
|
|
119
|
+
const host = useDataManagerHost();
|
|
120
|
+
|
|
121
|
+
// Deletion needs THREE things, not one: somewhere to scope the impact query, a way to
|
|
122
|
+
// preview the cascade, and a way to perform it. Gating the affordance on the project id
|
|
123
|
+
// alone offered a Delete button whose dialog then called an undefined `previewDeletion`
|
|
124
|
+
// — which is exactly what the overview page did, because it passed the id and neither
|
|
125
|
+
// callback. An affordance that cannot complete should not be offered.
|
|
126
|
+
const canDelete = Boolean(deletionProjectId && previewDeletion && executeDeletion);
|
|
127
|
+
|
|
128
|
+
// ─── Batch operation state ─────────────────────────────────────────
|
|
129
|
+
const [batchEditOpen, setBatchEditOpen] = React.useState(false);
|
|
130
|
+
const [batchOperationPending, setBatchOperationPending] = React.useState(false);
|
|
131
|
+
const [batchResult, setBatchResult] = React.useState<{ message: string; isError: boolean } | null>(null);
|
|
132
|
+
|
|
133
|
+
// Build a map of column IDs to property details for filter type detection.
|
|
134
|
+
const propertyMap = React.useMemo(() => {
|
|
135
|
+
if (propertiesMap) return propertiesMap;
|
|
136
|
+
const map: Record<string, PropertyShapeWire> = {};
|
|
137
|
+
if (shape?.propertyShapes) {
|
|
138
|
+
for (const prop of shape.propertyShapes) {
|
|
139
|
+
map[prop.label] = prop as unknown as PropertyShapeWire;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return map;
|
|
143
|
+
}, [propertiesMap, shape?.propertyShapes]);
|
|
144
|
+
|
|
145
|
+
// Get available operators when column changes
|
|
146
|
+
const selectedProperty: PropertyShapeWire | null = selectedColumn ? propertyMap[selectedColumn] || null : null;
|
|
147
|
+
const availableOperators = selectedProperty
|
|
148
|
+
? getOperatorsForProperty(selectedProperty)
|
|
149
|
+
: [];
|
|
150
|
+
const selectedUiType = selectedProperty ? getPropertyUiType(selectedProperty) : null;
|
|
151
|
+
const isEnumColumn = selectedUiType === 'enum-literal' || selectedUiType === 'enum-iri';
|
|
152
|
+
|
|
153
|
+
// Reset operator/value when column changes
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
if (availableOperators.length > 0) {
|
|
156
|
+
setSelectedOperator(availableOperators[0].value);
|
|
157
|
+
} else {
|
|
158
|
+
setSelectedOperator('');
|
|
159
|
+
}
|
|
160
|
+
setFilterValue('');
|
|
161
|
+
setEnumSelections([]);
|
|
162
|
+
}, [selectedColumn]);
|
|
163
|
+
|
|
164
|
+
// Apply filters to data (client-side)
|
|
165
|
+
const filteredData = React.useMemo(() => {
|
|
166
|
+
const activeFilters = filters.filter((f) => f.isActive);
|
|
167
|
+
if (activeFilters.length === 0) return data as any[];
|
|
168
|
+
return (data as any[]).filter((row) => applyAllFilters(row, activeFilters));
|
|
169
|
+
}, [data, filters]);
|
|
170
|
+
|
|
171
|
+
const setPagination = (updater) => {
|
|
172
|
+
// react-table passes a functional updater; resolve it against the current config.
|
|
173
|
+
const newVal =
|
|
174
|
+
typeof updater === 'function' ? updater(config) : updater;
|
|
175
|
+
// Skip no-op pagination updates. react-table re-fires onPaginationChange whenever
|
|
176
|
+
// the data changes; propagating an identical-but-new config object would change
|
|
177
|
+
// `tableConfig` and re-trigger the parent's fetch effect — an infinite refetch loop.
|
|
178
|
+
if (
|
|
179
|
+
newVal &&
|
|
180
|
+
newVal.pageIndex === config.pageIndex &&
|
|
181
|
+
newVal.pageSize === config.pageSize
|
|
182
|
+
) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
setConfig(newVal);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const table = useReactTable({
|
|
189
|
+
columns,
|
|
190
|
+
data: filteredData,
|
|
191
|
+
getCoreRowModel: getCoreRowModel(),
|
|
192
|
+
getSortedRowModel: getSortedRowModel(),
|
|
193
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
194
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
195
|
+
onPaginationChange: setPagination,
|
|
196
|
+
state: {
|
|
197
|
+
pagination: config,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Compute whether we've hit the maxCount limit for pick mode
|
|
202
|
+
const pickLimitReached = isPickMode && pickMaxCount != null && selectedUris.size >= pickMaxCount;
|
|
203
|
+
|
|
204
|
+
const handleFilterSubmit = () => {
|
|
205
|
+
if (!selectedColumn || !selectedOperator) return;
|
|
206
|
+
const value = isEnumColumn && selectedOperator === 'in'
|
|
207
|
+
? enumSelections
|
|
208
|
+
: filterValue.trim();
|
|
209
|
+
if (Array.isArray(value) ? value.length === 0 : !value) return;
|
|
210
|
+
|
|
211
|
+
const newFilter: TableFilter = {
|
|
212
|
+
id: generateFilterId(),
|
|
213
|
+
columnId: selectedColumn,
|
|
214
|
+
columnLabel: selectedColumn
|
|
215
|
+
.replace(/([A-Z])/g, ' $1')
|
|
216
|
+
.replace(/_/g, ' ')
|
|
217
|
+
.replace(/^./, (c) => c.toUpperCase()),
|
|
218
|
+
operator: selectedOperator,
|
|
219
|
+
value,
|
|
220
|
+
isActive: true,
|
|
221
|
+
};
|
|
222
|
+
setFilters([...filters, newFilter]);
|
|
223
|
+
setFilterValue('');
|
|
224
|
+
setEnumSelections([]);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handleFilterToggle = (index: number) => {
|
|
228
|
+
setFilters(filters.map((filter, i) =>
|
|
229
|
+
i === index ? { ...filter, isActive: !filter.isActive } : filter
|
|
230
|
+
));
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const handleFilterRemove = (index: number) => {
|
|
234
|
+
setFilters(filters.filter((_, i) => i !== index));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const rowToPickInstance = (row) => {
|
|
238
|
+
const original = row.original;
|
|
239
|
+
const labelKey = Object.keys(original).find((k) => k !== 'id' && original[k]);
|
|
240
|
+
const rawLabel = labelKey ? original[labelKey] : original.id;
|
|
241
|
+
const label = typeof rawLabel === 'object' && rawLabel?.label
|
|
242
|
+
? rawLabel.label
|
|
243
|
+
: typeof rawLabel === 'string'
|
|
244
|
+
? rawLabel
|
|
245
|
+
: String(original.id || '');
|
|
246
|
+
const image = original.image?.contentUrl || original.image;
|
|
247
|
+
return {
|
|
248
|
+
id: original.id,
|
|
249
|
+
label,
|
|
250
|
+
...(typeof image === 'string' ? { image } : {}),
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const handlePick = (row) => {
|
|
255
|
+
if (!onPickInstance) return;
|
|
256
|
+
onPickInstance(rowToPickInstance(row));
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const handlePickSelected = () => {
|
|
260
|
+
const rows = table.getRowModel().rows;
|
|
261
|
+
const picked = rows
|
|
262
|
+
.filter((r) => selectedUris.has(r.original?.id))
|
|
263
|
+
.map(rowToPickInstance);
|
|
264
|
+
if (picked.length === 1 && onPickInstance) {
|
|
265
|
+
onPickInstance(picked[0]);
|
|
266
|
+
} else if (picked.length > 0 && onPickInstances) {
|
|
267
|
+
onPickInstances(picked);
|
|
268
|
+
} else if (picked.length > 0 && onPickInstance) {
|
|
269
|
+
onPickInstance(picked[0]);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const handleEdit = (row) => {
|
|
274
|
+
// Single-instance edit: always `s0` (the edit page reads `s0`). `row.id` is
|
|
275
|
+
// react-table's row index, not a query-subject index — using it produced
|
|
276
|
+
// `?s1=` for later rows, which the edit page couldn't read ("Missing instance id").
|
|
277
|
+
const subject = row.original.id;
|
|
278
|
+
host.navigate?.toEdit(shape?.id, subject);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const handleView = (row) => {
|
|
282
|
+
const subject = row.original.id;
|
|
283
|
+
host.navigate?.toInstance(shape?.id, subject);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// ─── Preview-required instance deletion ────────────────────────────
|
|
287
|
+
const [deletionTarget, setDeletionTarget] = React.useState<{
|
|
288
|
+
kind: 'single' | 'batch';
|
|
289
|
+
targets: InstanceDeletionTarget[];
|
|
290
|
+
labels: Record<string, string>;
|
|
291
|
+
} | null>(null);
|
|
292
|
+
|
|
293
|
+
const handleDelete = (row: any) => {
|
|
294
|
+
const uri = row.original?.id;
|
|
295
|
+
const label = row.original?.label || row.original?.name || getNodeDisplay(row.original) || '';
|
|
296
|
+
setDeletionTarget({
|
|
297
|
+
kind: 'single',
|
|
298
|
+
targets: [{ instanceId: uri, shapeIri: shape.id }],
|
|
299
|
+
labels: { [uri]: label },
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// ─── Batch delete ──────────────────────────────────────────────────
|
|
304
|
+
const handleDeleteCollectively = () => {
|
|
305
|
+
if (!canDelete || selectedUris.size === 0) return;
|
|
306
|
+
const uris = Array.from(selectedUris);
|
|
307
|
+
const labels: Record<string, string> = {};
|
|
308
|
+
for (const row of (data as any[])) {
|
|
309
|
+
if (selectedUris.has(row?.id)) {
|
|
310
|
+
labels[row.id] = row.label || row.name || getNodeDisplay(row) || row.id;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
setDeletionTarget({
|
|
314
|
+
kind: 'batch',
|
|
315
|
+
targets: uris.map((instanceId) => ({ instanceId, shapeIri: shape.id })),
|
|
316
|
+
labels,
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const handleDeletionCompleted = (result: InstanceDeletionResult) => {
|
|
321
|
+
if (!deletionTarget) return;
|
|
322
|
+
const requested = deletionTarget.targets.map(({ instanceId }) => instanceId);
|
|
323
|
+
const deleted = result.deleted.map(({ instanceId }) => instanceId);
|
|
324
|
+
const deletedSet = new Set(deleted);
|
|
325
|
+
const failed = requested.filter((uri) => !deletedSet.has(uri));
|
|
326
|
+
onInstancesDeleted?.(deleted);
|
|
327
|
+
if (deletionTarget.kind === 'batch') {
|
|
328
|
+
if (failed.length > 0) {
|
|
329
|
+
removeFromSelection(deleted);
|
|
330
|
+
setBatchResult({
|
|
331
|
+
message: `Deleted ${deleted.length} of ${requested.length}. ${failed.length} failed and remain selected.`,
|
|
332
|
+
isError: true,
|
|
333
|
+
});
|
|
334
|
+
} else {
|
|
335
|
+
clearSelection();
|
|
336
|
+
setBatchResult({
|
|
337
|
+
message: `Successfully deleted ${deleted.length} items.`,
|
|
338
|
+
isError: false,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// ─── Batch edit ────────────────────────────────────────────────────
|
|
345
|
+
const handleBatchApply = async (changes: Record<string, any>) => {
|
|
346
|
+
if (!onBatchUpdate) return;
|
|
347
|
+
const uris = Array.from(selectedUris);
|
|
348
|
+
setBatchOperationPending(true);
|
|
349
|
+
setBatchResult(null);
|
|
350
|
+
try {
|
|
351
|
+
const result = await onBatchUpdate(changes, uris);
|
|
352
|
+
setBatchEditOpen(false);
|
|
353
|
+
if (result.failed.length > 0) {
|
|
354
|
+
removeFromSelection(result.succeeded);
|
|
355
|
+
setBatchResult({
|
|
356
|
+
message: `Updated ${result.succeeded.length} of ${uris.length}. ${result.failed.length} failed.`,
|
|
357
|
+
isError: true,
|
|
358
|
+
});
|
|
359
|
+
} else {
|
|
360
|
+
clearSelection();
|
|
361
|
+
setBatchResult({
|
|
362
|
+
message: `Successfully updated ${result.succeeded.length} items.`,
|
|
363
|
+
isError: false,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
} catch (err) {
|
|
367
|
+
setBatchResult({
|
|
368
|
+
message: `Batch update failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
369
|
+
isError: true,
|
|
370
|
+
});
|
|
371
|
+
} finally {
|
|
372
|
+
setBatchOperationPending(false);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Auto-clear result message after 5 seconds
|
|
377
|
+
React.useEffect(() => {
|
|
378
|
+
if (!batchResult) return;
|
|
379
|
+
const timer = setTimeout(() => setBatchResult(null), 5000);
|
|
380
|
+
return () => clearTimeout(timer);
|
|
381
|
+
}, [batchResult]);
|
|
382
|
+
|
|
383
|
+
const getCellStyle = (cell) => {
|
|
384
|
+
const value = cell.getValue();
|
|
385
|
+
if (value === 'Yes') {
|
|
386
|
+
return style.green;
|
|
387
|
+
}
|
|
388
|
+
return '';
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const renderCellContent = (cell) => {
|
|
392
|
+
const value = cell.getValue();
|
|
393
|
+
if (typeof value === 'string' && value.length > 80) {
|
|
394
|
+
return <TruncatedText text={value} maxLength={50} />;
|
|
395
|
+
}
|
|
396
|
+
return flexRender(cell.column.columnDef.cell, cell.getContext());
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const hasFilters = filters.length > 0;
|
|
400
|
+
const selectionCount = selectedUris.size;
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<div className={style.tableWrapper}>
|
|
404
|
+
{/* Action bar */}
|
|
405
|
+
<div className={cl(style.action, !hasFilters && !hasSelection && style.withBorder)}>
|
|
406
|
+
{/* A button, not a <Link>. An anchor would have to know the host's URL for
|
|
407
|
+
"add one of these", and a host that has no such page (a picker-only embed)
|
|
408
|
+
has no href to give. `toAdd` is the whole contract. */}
|
|
409
|
+
<Button
|
|
410
|
+
size="small"
|
|
411
|
+
onClick={() => shape?.id && host.navigate?.toAdd(shape.id)}
|
|
412
|
+
disabled={!shape?.id || !host.navigate}
|
|
413
|
+
>
|
|
414
|
+
<Icons.UserPlus width={18} height={18} />
|
|
415
|
+
New {shape ? formatShapeLabel(shape.label) : ''}
|
|
416
|
+
</Button>
|
|
417
|
+
<Drawer.Root direction="right" handleOnly>
|
|
418
|
+
<Drawer.Trigger asChild>
|
|
419
|
+
<IconButton size="small" variant="outline" aria-label="Open filters">
|
|
420
|
+
<Icons.ListFilter width={20} height={20} />
|
|
421
|
+
</IconButton>
|
|
422
|
+
</Drawer.Trigger>
|
|
423
|
+
<Drawer.Content hideHandle className={style.filterDrawer}>
|
|
424
|
+
<div className={style.filterDrawerContent}>
|
|
425
|
+
<Drawer.Header className={style.filterDrawerHeader}>
|
|
426
|
+
<Drawer.Title>Filters</Drawer.Title>
|
|
427
|
+
<Drawer.Description>
|
|
428
|
+
{filters.length > 0
|
|
429
|
+
? `${filters.length} filter${filters.length > 1 ? 's' : ''} applied`
|
|
430
|
+
: 'No filters applied'}
|
|
431
|
+
</Drawer.Description>
|
|
432
|
+
</Drawer.Header>
|
|
433
|
+
<div className={style.filterDrawerBody}>
|
|
434
|
+
<div className={style.form}>
|
|
435
|
+
<div className={style.formSection}>
|
|
436
|
+
<Label>Column</Label>
|
|
437
|
+
<Select.Root
|
|
438
|
+
value={selectedColumn}
|
|
439
|
+
onValueChange={setSelectedColumn}
|
|
440
|
+
>
|
|
441
|
+
<Select.Trigger aria-label="Select Column">
|
|
442
|
+
<Select.Value placeholder="Select column..." />
|
|
443
|
+
</Select.Trigger>
|
|
444
|
+
<Select.Content>
|
|
445
|
+
<Select.Group>
|
|
446
|
+
{shape?.propertyShapes
|
|
447
|
+
?.filter((p) => p.label !== 'id')
|
|
448
|
+
.map((prop) => (
|
|
449
|
+
<Select.Item key={prop.label} value={prop.label}>
|
|
450
|
+
{prop.label
|
|
451
|
+
.replace(/([A-Z])/g, ' $1')
|
|
452
|
+
.replace(/_/g, ' ')
|
|
453
|
+
.replace(/^./, (c) => c.toUpperCase())}
|
|
454
|
+
</Select.Item>
|
|
455
|
+
))}
|
|
456
|
+
</Select.Group>
|
|
457
|
+
</Select.Content>
|
|
458
|
+
</Select.Root>
|
|
459
|
+
</div>
|
|
460
|
+
|
|
461
|
+
{selectedColumn && availableOperators.length > 0 && (
|
|
462
|
+
<div className={style.formSection}>
|
|
463
|
+
<Label>Operator</Label>
|
|
464
|
+
<Select.Root
|
|
465
|
+
value={selectedOperator as string}
|
|
466
|
+
onValueChange={(val) => setSelectedOperator(val as FilterOperator)}
|
|
467
|
+
>
|
|
468
|
+
<Select.Trigger aria-label="Select Operator">
|
|
469
|
+
<Select.Value placeholder="Select operator..." />
|
|
470
|
+
</Select.Trigger>
|
|
471
|
+
<Select.Content>
|
|
472
|
+
<Select.Group>
|
|
473
|
+
{availableOperators.map((op) => (
|
|
474
|
+
<Select.Item key={op.value} value={op.value}>
|
|
475
|
+
{op.label}
|
|
476
|
+
</Select.Item>
|
|
477
|
+
))}
|
|
478
|
+
</Select.Group>
|
|
479
|
+
</Select.Content>
|
|
480
|
+
</Select.Root>
|
|
481
|
+
</div>
|
|
482
|
+
)}
|
|
483
|
+
|
|
484
|
+
{selectedColumn && selectedOperator && (
|
|
485
|
+
<div className={style.formSection}>
|
|
486
|
+
<Label>Value</Label>
|
|
487
|
+
{isEnumColumn &&
|
|
488
|
+
selectedOperator === 'in' &&
|
|
489
|
+
selectedProperty &&
|
|
490
|
+
enumOptions(selectedProperty).length > 0 ? (
|
|
491
|
+
<div className={style.enumCheckboxes}>
|
|
492
|
+
{enumOptions(selectedProperty).map((item) => {
|
|
493
|
+
const val = item.id;
|
|
494
|
+
const checked = enumSelections.includes(val);
|
|
495
|
+
return (
|
|
496
|
+
<label key={val} className={style.enumCheckboxLabel}>
|
|
497
|
+
<Checkbox
|
|
498
|
+
checked={checked}
|
|
499
|
+
onCheckedChange={() => {
|
|
500
|
+
setEnumSelections((prev) =>
|
|
501
|
+
checked
|
|
502
|
+
? prev.filter((v) => v !== val)
|
|
503
|
+
: [...prev, val]
|
|
504
|
+
);
|
|
505
|
+
}}
|
|
506
|
+
/>
|
|
507
|
+
<span>{humanizeEnumLabel(val)}</span>
|
|
508
|
+
</label>
|
|
509
|
+
);
|
|
510
|
+
})}
|
|
511
|
+
</div>
|
|
512
|
+
) : selectedUiType === 'boolean' ? (
|
|
513
|
+
<Select.Root
|
|
514
|
+
value={filterValue}
|
|
515
|
+
onValueChange={setFilterValue}
|
|
516
|
+
>
|
|
517
|
+
<Select.Trigger aria-label="Select Value">
|
|
518
|
+
<Select.Value placeholder="Select..." />
|
|
519
|
+
</Select.Trigger>
|
|
520
|
+
<Select.Content>
|
|
521
|
+
<Select.Group>
|
|
522
|
+
<Select.Item value="true">Yes</Select.Item>
|
|
523
|
+
<Select.Item value="false">No</Select.Item>
|
|
524
|
+
</Select.Group>
|
|
525
|
+
</Select.Content>
|
|
526
|
+
</Select.Root>
|
|
527
|
+
) : (
|
|
528
|
+
<Input
|
|
529
|
+
name="filterValue"
|
|
530
|
+
type={selectedUiType === 'number' ? 'number' : selectedUiType === 'date' || selectedUiType === 'datetime' ? 'date' : 'text'}
|
|
531
|
+
placeholder="Enter value..."
|
|
532
|
+
value={filterValue}
|
|
533
|
+
onChange={(e) => setFilterValue(e.target.value)}
|
|
534
|
+
onKeyDown={(e) => {
|
|
535
|
+
if (e.key === 'Enter') handleFilterSubmit();
|
|
536
|
+
}}
|
|
537
|
+
/>
|
|
538
|
+
)}
|
|
539
|
+
</div>
|
|
540
|
+
)}
|
|
541
|
+
</div>
|
|
542
|
+
<Typography className={style.filterNotice}>
|
|
543
|
+
Filters apply to loaded rows only
|
|
544
|
+
</Typography>
|
|
545
|
+
</div>
|
|
546
|
+
<Drawer.Footer className={style.filterDrawerFooter}>
|
|
547
|
+
<Drawer.Close asChild>
|
|
548
|
+
<Button variant="outline">Cancel</Button>
|
|
549
|
+
</Drawer.Close>
|
|
550
|
+
<Drawer.Close asChild>
|
|
551
|
+
<Button onClick={handleFilterSubmit}>Apply Filter</Button>
|
|
552
|
+
</Drawer.Close>
|
|
553
|
+
</Drawer.Footer>
|
|
554
|
+
</div>
|
|
555
|
+
</Drawer.Content>
|
|
556
|
+
</Drawer.Root>
|
|
557
|
+
{tableMode && onTableModeChange && (
|
|
558
|
+
<div className={style.tableModeToggle}>
|
|
559
|
+
<Button
|
|
560
|
+
size="small"
|
|
561
|
+
variant={tableMode === 'auto' ? 'solid' : 'outline'}
|
|
562
|
+
onClick={() => onTableModeChange('auto')}
|
|
563
|
+
>
|
|
564
|
+
Smart
|
|
565
|
+
</Button>
|
|
566
|
+
<Button
|
|
567
|
+
size="small"
|
|
568
|
+
variant={tableMode === 'full' ? 'solid' : 'outline'}
|
|
569
|
+
onClick={() => onTableModeChange('full')}
|
|
570
|
+
>
|
|
571
|
+
All
|
|
572
|
+
</Button>
|
|
573
|
+
</div>
|
|
574
|
+
)}
|
|
575
|
+
{isPickMode && selectionCount > 0 && (
|
|
576
|
+
<Button
|
|
577
|
+
size="small"
|
|
578
|
+
onClick={handlePickSelected}
|
|
579
|
+
style={{ marginLeft: 'auto' }}
|
|
580
|
+
>
|
|
581
|
+
<Icons.Check width="16" height="16" />
|
|
582
|
+
Use Selected ({selectionCount}{pickMaxCount != null ? ` / ${pickMaxCount}` : ''})
|
|
583
|
+
</Button>
|
|
584
|
+
)}
|
|
585
|
+
</div>
|
|
586
|
+
|
|
587
|
+
{/* Selection bar — shown when items are selected (non-pick mode) */}
|
|
588
|
+
<AnimatePresence>
|
|
589
|
+
{hasSelection && !isPickMode && (
|
|
590
|
+
<motion.div
|
|
591
|
+
className={style.selectionBar}
|
|
592
|
+
initial={{ opacity: 0, height: 0 }}
|
|
593
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
594
|
+
exit={{ opacity: 0, height: 0 }}
|
|
595
|
+
transition={{ duration: reducedMotion ? 0 : 0.2, ease: [0.4, 0, 0.2, 1] }}
|
|
596
|
+
>
|
|
597
|
+
<span className={style.selectionCount}>
|
|
598
|
+
{selectionCount} selected
|
|
599
|
+
</span>
|
|
600
|
+
<div className={style.selectionActions}>
|
|
601
|
+
<Button size="small" variant="ghost" onClick={clearSelection} disabled={batchOperationPending}>
|
|
602
|
+
Clear
|
|
603
|
+
</Button>
|
|
604
|
+
{onBatchUpdate && (
|
|
605
|
+
<Button
|
|
606
|
+
size="small"
|
|
607
|
+
onClick={() => setBatchEditOpen(true)}
|
|
608
|
+
disabled={batchOperationPending}
|
|
609
|
+
>
|
|
610
|
+
<Icons.Pencil width={16} height={16} />
|
|
611
|
+
Edit Selected ({selectionCount})
|
|
612
|
+
</Button>
|
|
613
|
+
)}
|
|
614
|
+
{canDelete && (
|
|
615
|
+
<Button
|
|
616
|
+
size="small"
|
|
617
|
+
variant="outline"
|
|
618
|
+
className={style.deleteDataButton}
|
|
619
|
+
onClick={handleDeleteCollectively}
|
|
620
|
+
disabled={batchOperationPending}
|
|
621
|
+
>
|
|
622
|
+
{batchOperationPending ? <Spinner size="small" /> : <Icons.Trash2 width={16} height={16} />}
|
|
623
|
+
Delete ({selectionCount})
|
|
624
|
+
</Button>
|
|
625
|
+
)}
|
|
626
|
+
</div>
|
|
627
|
+
</motion.div>
|
|
628
|
+
)}
|
|
629
|
+
</AnimatePresence>
|
|
630
|
+
|
|
631
|
+
{/* Batch result message */}
|
|
632
|
+
<AnimatePresence>
|
|
633
|
+
{batchResult && (
|
|
634
|
+
<motion.div
|
|
635
|
+
className={cl(style.batchResultBar, batchResult.isError && style.batchResultError)}
|
|
636
|
+
initial={{ opacity: 0, height: 0 }}
|
|
637
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
638
|
+
exit={{ opacity: 0, height: 0 }}
|
|
639
|
+
transition={{ duration: reducedMotion ? 0 : 0.2 }}
|
|
640
|
+
>
|
|
641
|
+
{batchResult.message}
|
|
642
|
+
<button className={style.batchResultDismiss} onClick={() => setBatchResult(null)}>
|
|
643
|
+
<Icons.X width={14} height={14} />
|
|
644
|
+
</button>
|
|
645
|
+
</motion.div>
|
|
646
|
+
)}
|
|
647
|
+
</AnimatePresence>
|
|
648
|
+
|
|
649
|
+
{/* Filter badges bar */}
|
|
650
|
+
<AnimatePresence>
|
|
651
|
+
{filters.length > 0 && (
|
|
652
|
+
<motion.div
|
|
653
|
+
className={style.tableBar}
|
|
654
|
+
initial={{ opacity: 0, x: -20 }}
|
|
655
|
+
animate={{ opacity: 1, x: 0 }}
|
|
656
|
+
exit={{ opacity: 0, x: -20 }}
|
|
657
|
+
transition={{
|
|
658
|
+
duration: reducedMotion ? 0 : 0.25,
|
|
659
|
+
ease: [0.4, 0, 0.2, 1],
|
|
660
|
+
}}
|
|
661
|
+
>
|
|
662
|
+
<div className={style.filterBar}>
|
|
663
|
+
<>
|
|
664
|
+
<motion.div
|
|
665
|
+
initial={{ opacity: 0, x: -10 }}
|
|
666
|
+
animate={{ opacity: 1, x: 0 }}
|
|
667
|
+
transition={{
|
|
668
|
+
duration: reducedMotion ? 0 : 0.2,
|
|
669
|
+
ease: [0.4, 0, 0.2, 1],
|
|
670
|
+
}}
|
|
671
|
+
>
|
|
672
|
+
<Typography className={style.filterLabel}>
|
|
673
|
+
Filters ({filteredData.length} of {(data as any[]).length}):
|
|
674
|
+
</Typography>
|
|
675
|
+
</motion.div>
|
|
676
|
+
{filters.map((filter, index) => (
|
|
677
|
+
<motion.div
|
|
678
|
+
key={filter.id}
|
|
679
|
+
initial={{ opacity: 0, x: -10 }}
|
|
680
|
+
animate={{ opacity: 1, x: 0 }}
|
|
681
|
+
exit={{ opacity: 0, x: -10, scale: 0.9 }}
|
|
682
|
+
transition={{
|
|
683
|
+
duration: reducedMotion ? 0 : 0.2,
|
|
684
|
+
delay: reducedMotion ? 0 : 0.1 + index * 0.05,
|
|
685
|
+
ease: [0.4, 0, 0.2, 1],
|
|
686
|
+
}}
|
|
687
|
+
>
|
|
688
|
+
<FilterBadge
|
|
689
|
+
label={formatFilterLabel(filter)}
|
|
690
|
+
isActive={filter.isActive}
|
|
691
|
+
onToggle={() => handleFilterToggle(index)}
|
|
692
|
+
onRemove={() => handleFilterRemove(index)}
|
|
693
|
+
/>
|
|
694
|
+
</motion.div>
|
|
695
|
+
))}
|
|
696
|
+
{filters.length > 1 && (
|
|
697
|
+
<Button
|
|
698
|
+
size="small"
|
|
699
|
+
variant="ghost"
|
|
700
|
+
onClick={() => setFilters([])}
|
|
701
|
+
>
|
|
702
|
+
Clear all
|
|
703
|
+
</Button>
|
|
704
|
+
)}
|
|
705
|
+
</>
|
|
706
|
+
</div>
|
|
707
|
+
</motion.div>
|
|
708
|
+
)}
|
|
709
|
+
</AnimatePresence>
|
|
710
|
+
<div className={style.tableContainer}>
|
|
711
|
+
<table className={style.table}>
|
|
712
|
+
<thead>
|
|
713
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
714
|
+
<tr key={headerGroup.id} className={style.tr}>
|
|
715
|
+
<th className={cl(style.th, style.sticky, style.stickyLeft)}>
|
|
716
|
+
<Checkbox onCheckedChange={toggleSelectAll} />
|
|
717
|
+
</th>
|
|
718
|
+
{headerGroup.headers.map((header, index) => {
|
|
719
|
+
return (
|
|
720
|
+
<th
|
|
721
|
+
key={header.id}
|
|
722
|
+
colSpan={header.colSpan}
|
|
723
|
+
className={style.th}
|
|
724
|
+
>
|
|
725
|
+
{index === 0 ? (
|
|
726
|
+
<div>
|
|
727
|
+
{flexRender(
|
|
728
|
+
header.column.columnDef.header,
|
|
729
|
+
header.getContext()
|
|
730
|
+
)}
|
|
731
|
+
</div>
|
|
732
|
+
) : (
|
|
733
|
+
<div
|
|
734
|
+
{...{
|
|
735
|
+
className: header.column.getCanSort()
|
|
736
|
+
? style.canSort
|
|
737
|
+
: '',
|
|
738
|
+
onClick: header.column.getToggleSortingHandler(),
|
|
739
|
+
}}
|
|
740
|
+
>
|
|
741
|
+
{flexRender(
|
|
742
|
+
header.column.columnDef.header,
|
|
743
|
+
header.getContext()
|
|
744
|
+
)}
|
|
745
|
+
{{
|
|
746
|
+
asc: <Icons.ChevronUp width={14} height={14} />,
|
|
747
|
+
desc: <Icons.ChevronDown width={14} height={14} />,
|
|
748
|
+
}[header.column.getIsSorted() as string] ?? (
|
|
749
|
+
<Icons.ChevronsLeftRight width={14} height={14} />
|
|
750
|
+
)}
|
|
751
|
+
</div>
|
|
752
|
+
)}
|
|
753
|
+
</th>
|
|
754
|
+
);
|
|
755
|
+
})}
|
|
756
|
+
<th className={cl(style.th, style.sticky, style.stickyRight)}>
|
|
757
|
+
Actions
|
|
758
|
+
</th>
|
|
759
|
+
</tr>
|
|
760
|
+
))}
|
|
761
|
+
</thead>
|
|
762
|
+
<motion.tbody
|
|
763
|
+
variants={tableStaggerContainer}
|
|
764
|
+
initial="initial"
|
|
765
|
+
animate="animate"
|
|
766
|
+
>
|
|
767
|
+
{isLoading
|
|
768
|
+
? Array.from({ length: config.pageSize }, (_, index) => (
|
|
769
|
+
<TableRowSkeleton key={`skeleton-${index}`} columns={1} />
|
|
770
|
+
))
|
|
771
|
+
: table.getRowModel().rows.map((row) => {
|
|
772
|
+
const rowUri = row.original?.id;
|
|
773
|
+
const rowSelected = rowUri ? isRowSelected(rowUri) : false;
|
|
774
|
+
return (
|
|
775
|
+
<motion.tr
|
|
776
|
+
key={row.id}
|
|
777
|
+
className={cl(
|
|
778
|
+
style.tr,
|
|
779
|
+
isPickMode && style.pickableRow,
|
|
780
|
+
isPickMode && pickLimitReached && !rowSelected && style.pickDisabledRow,
|
|
781
|
+
)}
|
|
782
|
+
variants={reducedMotion ? undefined : tableRowItem}
|
|
783
|
+
layout
|
|
784
|
+
onClick={isPickMode ? () => {
|
|
785
|
+
if (!rowUri) return;
|
|
786
|
+
if (pickLimitReached && !rowSelected) return;
|
|
787
|
+
toggleRowSelection(rowUri);
|
|
788
|
+
} : undefined}
|
|
789
|
+
style={isPickMode ? { cursor: pickLimitReached && !rowSelected ? 'not-allowed' : 'pointer' } : undefined}
|
|
790
|
+
>
|
|
791
|
+
<td
|
|
792
|
+
className={cl(style.td, style.sticky, style.stickyLeft)}
|
|
793
|
+
onClick={isPickMode ? (e) => e.stopPropagation() : undefined}
|
|
794
|
+
>
|
|
795
|
+
<Checkbox
|
|
796
|
+
onCheckedChange={() => {
|
|
797
|
+
if (!rowUri) return;
|
|
798
|
+
if (isPickMode && pickLimitReached && !rowSelected) return;
|
|
799
|
+
toggleRowSelection(rowUri);
|
|
800
|
+
}}
|
|
801
|
+
checked={rowSelected}
|
|
802
|
+
disabled={isPickMode && pickLimitReached && !rowSelected}
|
|
803
|
+
/>
|
|
804
|
+
</td>
|
|
805
|
+
{row.getVisibleCells().map((cell) => {
|
|
806
|
+
return (
|
|
807
|
+
<td
|
|
808
|
+
key={cell.id}
|
|
809
|
+
className={cl(style.td, getCellStyle(cell))}
|
|
810
|
+
>
|
|
811
|
+
{renderCellContent(cell)}
|
|
812
|
+
</td>
|
|
813
|
+
);
|
|
814
|
+
})}
|
|
815
|
+
<td
|
|
816
|
+
className={cl(
|
|
817
|
+
style.td,
|
|
818
|
+
style.sticky,
|
|
819
|
+
style.stickyRight
|
|
820
|
+
)}
|
|
821
|
+
>
|
|
822
|
+
{isPickMode ? (
|
|
823
|
+
<Button
|
|
824
|
+
size="small"
|
|
825
|
+
onClick={() => handlePick(row)}
|
|
826
|
+
disabled={pickLimitReached && !rowSelected}
|
|
827
|
+
>
|
|
828
|
+
<Icons.Check width="16" height="16" />
|
|
829
|
+
Select
|
|
830
|
+
</Button>
|
|
831
|
+
) : (
|
|
832
|
+
<div className={style.actionButtons}>
|
|
833
|
+
<IconButton
|
|
834
|
+
size="small"
|
|
835
|
+
variant="ghost"
|
|
836
|
+
aria-label="View"
|
|
837
|
+
className={cl(
|
|
838
|
+
style.actionTableButton,
|
|
839
|
+
style.viewAction
|
|
840
|
+
)}
|
|
841
|
+
onClick={() => handleView(row)}
|
|
842
|
+
>
|
|
843
|
+
<Icons.Eye width={16} height={16} />
|
|
844
|
+
</IconButton>
|
|
845
|
+
<IconButton
|
|
846
|
+
size="small"
|
|
847
|
+
variant="ghost"
|
|
848
|
+
aria-label="Edit"
|
|
849
|
+
className={cl(
|
|
850
|
+
style.actionTableButton,
|
|
851
|
+
style.editAction
|
|
852
|
+
)}
|
|
853
|
+
onClick={() => handleEdit(row)}
|
|
854
|
+
>
|
|
855
|
+
<Icons.Pencil width={16} height={16} />
|
|
856
|
+
</IconButton>
|
|
857
|
+
{/* The row's delete button was rendered unconditionally while
|
|
858
|
+
the dialog behind it was gated — so it was reachable without
|
|
859
|
+
the callbacks that make deletion work, and the dialog then
|
|
860
|
+
called an undefined previewDeletion. Same gate as the batch
|
|
861
|
+
action and the dialog. */}
|
|
862
|
+
{canDelete && (
|
|
863
|
+
<IconButton
|
|
864
|
+
size="small"
|
|
865
|
+
variant="ghost"
|
|
866
|
+
aria-label="Delete"
|
|
867
|
+
className={cl(
|
|
868
|
+
style.actionTableButton,
|
|
869
|
+
style.removeAction
|
|
870
|
+
)}
|
|
871
|
+
onClick={() => handleDelete(row)}
|
|
872
|
+
>
|
|
873
|
+
<Icons.Trash2 width={16} height={16} />
|
|
874
|
+
</IconButton>
|
|
875
|
+
)}
|
|
876
|
+
</div>
|
|
877
|
+
)}
|
|
878
|
+
</td>
|
|
879
|
+
</motion.tr>
|
|
880
|
+
);
|
|
881
|
+
})}
|
|
882
|
+
</motion.tbody>
|
|
883
|
+
</table>
|
|
884
|
+
</div>
|
|
885
|
+
<div className={style.pagination}>
|
|
886
|
+
<Typography className={style.pageInfo}>
|
|
887
|
+
Page{' '}
|
|
888
|
+
<strong>
|
|
889
|
+
{table.getState().pagination.pageIndex + 1} of{' '}
|
|
890
|
+
{table.getPageCount().toLocaleString()}
|
|
891
|
+
</strong>
|
|
892
|
+
</Typography>
|
|
893
|
+
<div className={style.pageInput}>
|
|
894
|
+
<Input
|
|
895
|
+
size="small"
|
|
896
|
+
type="number"
|
|
897
|
+
className={style.goToPageForm}
|
|
898
|
+
value={table.getState().pagination.pageIndex + 1}
|
|
899
|
+
onChange={(e) => {
|
|
900
|
+
const page = e.target.value ? Number(e.target.value) - 1 : 0;
|
|
901
|
+
table.setPageIndex(page);
|
|
902
|
+
}}
|
|
903
|
+
/>
|
|
904
|
+
</div>
|
|
905
|
+
<div className={style.pageShowItem}>
|
|
906
|
+
<Select.Root
|
|
907
|
+
value={table.getState().pagination.pageSize.toString()}
|
|
908
|
+
onValueChange={(value) => table.setPageSize(Number(value))}
|
|
909
|
+
>
|
|
910
|
+
<Select.Trigger size="small">
|
|
911
|
+
<Select.Value placeholder="Show Rows" />
|
|
912
|
+
</Select.Trigger>
|
|
913
|
+
<Select.Content>
|
|
914
|
+
<Select.Group>
|
|
915
|
+
{[10, 20, 30, 40, 50].map((pageSize) => (
|
|
916
|
+
<Select.Item key={pageSize} value={pageSize.toString()}>
|
|
917
|
+
Show {pageSize}
|
|
918
|
+
</Select.Item>
|
|
919
|
+
))}
|
|
920
|
+
</Select.Group>
|
|
921
|
+
</Select.Content>
|
|
922
|
+
</Select.Root>
|
|
923
|
+
</div>
|
|
924
|
+
<IconButton
|
|
925
|
+
size="small"
|
|
926
|
+
variant="outline"
|
|
927
|
+
className={style.paginationButton}
|
|
928
|
+
onClick={() => table.firstPage()}
|
|
929
|
+
disabled={!table.getCanPreviousPage()}
|
|
930
|
+
>
|
|
931
|
+
<Icons.ChevronsLeft width={18} height={18} />
|
|
932
|
+
</IconButton>
|
|
933
|
+
<IconButton
|
|
934
|
+
size="small"
|
|
935
|
+
variant="outline"
|
|
936
|
+
className={style.paginationButton}
|
|
937
|
+
onClick={() => table.previousPage()}
|
|
938
|
+
disabled={!table.getCanPreviousPage()}
|
|
939
|
+
>
|
|
940
|
+
<Icons.ChevronLeft width={18} height={18} />
|
|
941
|
+
</IconButton>
|
|
942
|
+
<IconButton
|
|
943
|
+
size="small"
|
|
944
|
+
variant="outline"
|
|
945
|
+
className={style.paginationButton}
|
|
946
|
+
onClick={() => table.nextPage()}
|
|
947
|
+
disabled={!table.getCanNextPage()}
|
|
948
|
+
>
|
|
949
|
+
<Icons.ChevronRight width={18} height={18} />
|
|
950
|
+
</IconButton>
|
|
951
|
+
<IconButton
|
|
952
|
+
size="small"
|
|
953
|
+
variant="outline"
|
|
954
|
+
className={style.paginationButton}
|
|
955
|
+
onClick={() => table.lastPage()}
|
|
956
|
+
disabled={!table.getCanNextPage()}
|
|
957
|
+
>
|
|
958
|
+
<Icons.ChevronsRight width={18} height={18} />
|
|
959
|
+
</IconButton>
|
|
960
|
+
</div>
|
|
961
|
+
|
|
962
|
+
{deletionTarget && canDelete && (
|
|
963
|
+
<InstanceDeletionDialog
|
|
964
|
+
isOpen
|
|
965
|
+
projectId={deletionProjectId}
|
|
966
|
+
targets={deletionTarget.targets}
|
|
967
|
+
labels={deletionTarget.labels}
|
|
968
|
+
onClose={() => setDeletionTarget(null)}
|
|
969
|
+
onDeleted={handleDeletionCompleted}
|
|
970
|
+
previewDeletion={previewDeletion!}
|
|
971
|
+
executeDeletion={executeDeletion!}
|
|
972
|
+
/>
|
|
973
|
+
)}
|
|
974
|
+
|
|
975
|
+
{/* Batch edit drawer */}
|
|
976
|
+
{onBatchUpdate && propertiesMap && (
|
|
977
|
+
<BatchEditDrawer
|
|
978
|
+
isOpen={batchEditOpen}
|
|
979
|
+
onClose={() => setBatchEditOpen(false)}
|
|
980
|
+
shape={shape}
|
|
981
|
+
properties={propertiesMap}
|
|
982
|
+
selectedCount={selectionCount}
|
|
983
|
+
onApply={handleBatchApply}
|
|
984
|
+
isLoading={batchOperationPending}
|
|
985
|
+
/>
|
|
986
|
+
)}
|
|
987
|
+
</div>
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
export default ReactTable;
|