@_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.
Files changed (210) hide show
  1. package/lib/esm/columns.d.ts +58 -0
  2. package/lib/esm/columns.js +173 -0
  3. package/lib/esm/columns.js.map +1 -0
  4. package/lib/esm/components/AddInstanceForms.d.ts +38 -0
  5. package/lib/esm/components/AddInstanceForms.js +143 -0
  6. package/lib/esm/components/AddInstanceForms.js.map +1 -0
  7. package/lib/esm/components/AddInstanceForms.module.css +12 -0
  8. package/lib/esm/components/BatchEditDrawer.d.ts +13 -0
  9. package/lib/esm/components/BatchEditDrawer.js +122 -0
  10. package/lib/esm/components/BatchEditDrawer.js.map +1 -0
  11. package/lib/esm/components/BatchEditDrawer.module.css +193 -0
  12. package/lib/esm/components/CustomMultiSelect.d.ts +26 -0
  13. package/lib/esm/components/CustomMultiSelect.js +344 -0
  14. package/lib/esm/components/CustomMultiSelect.js.map +1 -0
  15. package/lib/esm/components/CustomMultiSelect.module.css +291 -0
  16. package/lib/esm/components/DynamicFormField.d.ts +21 -0
  17. package/lib/esm/components/DynamicFormField.js +149 -0
  18. package/lib/esm/components/DynamicFormField.js.map +1 -0
  19. package/lib/esm/components/DynamicFormField.module.css +169 -0
  20. package/lib/esm/components/EditInstanceForms.d.ts +15 -0
  21. package/lib/esm/components/EditInstanceForms.js +118 -0
  22. package/lib/esm/components/EditInstanceForms.js.map +1 -0
  23. package/lib/esm/components/FilterBadge.d.ts +20 -0
  24. package/lib/esm/components/FilterBadge.js +26 -0
  25. package/lib/esm/components/FilterBadge.js.map +1 -0
  26. package/lib/esm/components/FilterBadge.module.css +25 -0
  27. package/lib/esm/components/FormField.d.ts +18 -0
  28. package/lib/esm/components/FormField.js +9 -0
  29. package/lib/esm/components/FormField.js.map +1 -0
  30. package/lib/esm/components/FormField.module.css +57 -0
  31. package/lib/esm/components/InstanceDeletionDialog.d.ts +17 -0
  32. package/lib/esm/components/InstanceDeletionDialog.js +83 -0
  33. package/lib/esm/components/InstanceDeletionDialog.js.map +1 -0
  34. package/lib/esm/components/InstanceDeletionDialog.module.css +59 -0
  35. package/lib/esm/components/InstanceOverview.d.ts +37 -0
  36. package/lib/esm/components/InstanceOverview.js +233 -0
  37. package/lib/esm/components/InstanceOverview.js.map +1 -0
  38. package/lib/esm/components/InstanceOverview.module.css +35 -0
  39. package/lib/esm/components/InstanceView.d.ts +29 -0
  40. package/lib/esm/components/InstanceView.js +97 -0
  41. package/lib/esm/components/InstanceView.js.map +1 -0
  42. package/lib/esm/components/InstanceView.module.css +157 -0
  43. package/lib/esm/components/NodeBadge.d.ts +25 -0
  44. package/lib/esm/components/NodeBadge.js +29 -0
  45. package/lib/esm/components/NodeBadge.js.map +1 -0
  46. package/lib/esm/components/NodeValuesEditor.d.ts +15 -0
  47. package/lib/esm/components/NodeValuesEditor.js +29 -0
  48. package/lib/esm/components/NodeValuesEditor.js.map +1 -0
  49. package/lib/esm/components/NodeValuesEditor.module.css +4 -0
  50. package/lib/esm/components/ReactTable.d.ts +50 -0
  51. package/lib/esm/components/ReactTable.js +370 -0
  52. package/lib/esm/components/ReactTable.js.map +1 -0
  53. package/lib/esm/components/ReactTable.module.css +950 -0
  54. package/lib/esm/components/TextfieldEditor.d.ts +1 -2
  55. package/lib/esm/components/TextfieldEditor.js +2 -2
  56. package/lib/esm/components/TextfieldEditor.js.map +1 -1
  57. package/lib/esm/components/forms.module.css +96 -0
  58. package/lib/esm/host.d.ts +196 -0
  59. package/lib/esm/host.js +25 -0
  60. package/lib/esm/host.js.map +1 -0
  61. package/lib/esm/hostContext.d.ts +34 -0
  62. package/lib/esm/hostContext.js +95 -0
  63. package/lib/esm/hostContext.js.map +1 -0
  64. package/lib/esm/index.d.ts +64 -10
  65. package/lib/esm/index.js +63 -14
  66. package/lib/esm/index.js.map +1 -1
  67. package/lib/esm/read.d.ts +48 -0
  68. package/lib/esm/read.js +96 -0
  69. package/lib/esm/read.js.map +1 -0
  70. package/lib/esm/search.d.ts +32 -0
  71. package/lib/esm/search.js +87 -0
  72. package/lib/esm/search.js.map +1 -0
  73. package/lib/esm/shape/contracts.d.ts +58 -0
  74. package/lib/esm/shape/contracts.js +15 -0
  75. package/lib/esm/shape/contracts.js.map +1 -0
  76. package/lib/esm/shape/naming.d.ts +30 -0
  77. package/lib/esm/shape/naming.js +52 -0
  78. package/lib/esm/shape/naming.js.map +1 -0
  79. package/lib/esm/shape/nodeDisplay.d.ts +36 -0
  80. package/lib/esm/shape/nodeDisplay.js +117 -0
  81. package/lib/esm/shape/nodeDisplay.js.map +1 -0
  82. package/lib/esm/shape/propertyTypes.d.ts +12 -0
  83. package/lib/esm/shape/propertyTypes.js +60 -0
  84. package/lib/esm/shape/propertyTypes.js.map +1 -0
  85. package/lib/esm/shape/propertyVisibility.d.ts +56 -0
  86. package/lib/esm/shape/propertyVisibility.js +112 -0
  87. package/lib/esm/shape/propertyVisibility.js.map +1 -0
  88. package/lib/esm/shape/shapeDependencies.d.ts +41 -0
  89. package/lib/esm/shape/shapeDependencies.js +141 -0
  90. package/lib/esm/shape/shapeDependencies.js.map +1 -0
  91. package/lib/esm/shape/tableFilters.d.ts +35 -0
  92. package/lib/esm/shape/tableFilters.js +171 -0
  93. package/lib/esm/shape/tableFilters.js.map +1 -0
  94. package/lib/esm/shape/useFormDependencies.d.ts +38 -0
  95. package/lib/esm/shape/useFormDependencies.js +178 -0
  96. package/lib/esm/shape/useFormDependencies.js.map +1 -0
  97. package/lib/esm/shape/validation.d.ts +15 -0
  98. package/lib/esm/shape/validation.js +106 -0
  99. package/lib/esm/shape/validation.js.map +1 -0
  100. package/lib/esm/tokens.css +25 -0
  101. package/lib/esm/types.d.ts +22 -0
  102. package/lib/esm/types.js +1 -0
  103. package/lib/esm/types.js.map +1 -0
  104. package/lib/esm/write.d.ts +51 -0
  105. package/lib/esm/write.js +99 -0
  106. package/lib/esm/write.js.map +1 -0
  107. package/package.json +44 -18
  108. package/src/backend.ts +1 -0
  109. package/src/columns.regression.test.ts +143 -0
  110. package/src/columns.test.ts +200 -0
  111. package/src/columns.ts +228 -0
  112. package/src/components/AddInstanceForms.module.css +12 -0
  113. package/src/components/AddInstanceForms.tsx +321 -0
  114. package/src/components/AvatarEditor.tsx +34 -0
  115. package/src/components/BatchEditDrawer.module.css +193 -0
  116. package/src/components/BatchEditDrawer.tsx +295 -0
  117. package/src/components/CheckboxEditor.module.css +6 -0
  118. package/src/components/CheckboxEditor.tsx +86 -0
  119. package/src/components/CustomMultiSelect.module.css +291 -0
  120. package/src/components/CustomMultiSelect.tsx +527 -0
  121. package/src/components/DateEditor.tsx +50 -0
  122. package/src/components/DynamicFormField.module.css +169 -0
  123. package/src/components/DynamicFormField.tsx +357 -0
  124. package/src/components/EditInstanceForms.tsx +260 -0
  125. package/src/components/FilterBadge.module.css +25 -0
  126. package/src/components/FilterBadge.tsx +70 -0
  127. package/src/components/FormField.module.css +57 -0
  128. package/src/components/FormField.tsx +59 -0
  129. package/src/components/ImageValueEditor.module.css +3 -0
  130. package/src/components/ImageValueEditor.tsx +82 -0
  131. package/src/components/InstanceDeletionDialog.module.css +59 -0
  132. package/src/components/InstanceDeletionDialog.tsx +175 -0
  133. package/src/components/InstanceOverview.module.css +35 -0
  134. package/src/components/InstanceOverview.tsx +374 -0
  135. package/src/components/InstanceView.module.css +157 -0
  136. package/src/components/InstanceView.tsx +212 -0
  137. package/src/components/NodeBadge.tsx +60 -0
  138. package/src/components/NodeValuesEditor.module.css +4 -0
  139. package/src/components/NodeValuesEditor.tsx +64 -0
  140. package/src/components/RadioButtonEditor.module.css +22 -0
  141. package/src/components/RadioButtonEditor.tsx +96 -0
  142. package/src/components/ReactTable.module.css +950 -0
  143. package/src/components/ReactTable.tsx +991 -0
  144. package/src/components/SelectEditor.tsx +85 -0
  145. package/src/components/ShapeTable.module.css +394 -0
  146. package/src/components/SwitchEditor.tsx +60 -0
  147. package/src/components/TextareaEditor.tsx +55 -0
  148. package/src/components/TextfieldEditor.tsx +67 -0
  149. package/src/components/ToggleEditor.tsx +63 -0
  150. package/src/components/forms.module.css +96 -0
  151. package/src/host.ts +226 -0
  152. package/src/hostContext.tsx +114 -0
  153. package/src/index.ts +99 -0
  154. package/src/package.ts +21 -0
  155. package/src/read.ts +148 -0
  156. package/src/search.test.ts +174 -0
  157. package/src/search.ts +108 -0
  158. package/src/shape/contracts.ts +66 -0
  159. package/src/shape/naming.ts +52 -0
  160. package/src/shape/nodeDisplay.ts +114 -0
  161. package/src/shape/propertyTypes.ts +86 -0
  162. package/src/shape/propertyVisibility.ts +180 -0
  163. package/src/shape/shapeDependencies.ts +183 -0
  164. package/src/shape/tableFilters.ts +223 -0
  165. package/src/shape/types.ts +1158 -0
  166. package/src/shape/useFormDependencies.ts +257 -0
  167. package/src/shape/validation.ts +121 -0
  168. package/src/tokens.css +25 -0
  169. package/src/types.ts +20 -0
  170. package/src/utils/helper.ts +35 -0
  171. package/src/write.test.ts +174 -0
  172. package/src/write.ts +114 -0
  173. package/.changeset/README.md +0 -20
  174. package/.changeset/config.json +0 -14
  175. package/.github/workflows/changeset-check.yml +0 -44
  176. package/.github/workflows/ci.yml +0 -43
  177. package/.github/workflows/publish.yml +0 -158
  178. package/CHANGELOG.md +0 -30
  179. package/lib/esm/components/AvatarEditor.d.ts +0 -20
  180. package/lib/esm/components/AvatarEditor.js +0 -7
  181. package/lib/esm/components/AvatarEditor.js.map +0 -1
  182. package/lib/esm/components/CheckboxEditor.d.ts +0 -21
  183. package/lib/esm/components/CheckboxEditor.js +0 -44
  184. package/lib/esm/components/CheckboxEditor.js.map +0 -1
  185. package/lib/esm/components/DateEditor.d.ts +0 -20
  186. package/lib/esm/components/DateEditor.js +0 -22
  187. package/lib/esm/components/DateEditor.js.map +0 -1
  188. package/lib/esm/components/RadioButtonEditor.d.ts +0 -23
  189. package/lib/esm/components/RadioButtonEditor.js +0 -41
  190. package/lib/esm/components/RadioButtonEditor.js.map +0 -1
  191. package/lib/esm/components/SelectEditor.d.ts +0 -21
  192. package/lib/esm/components/SelectEditor.js +0 -30
  193. package/lib/esm/components/SelectEditor.js.map +0 -1
  194. package/lib/esm/components/SwitchEditor.d.ts +0 -21
  195. package/lib/esm/components/SwitchEditor.js +0 -23
  196. package/lib/esm/components/SwitchEditor.js.map +0 -1
  197. package/lib/esm/components/TextareaEditor.d.ts +0 -23
  198. package/lib/esm/components/TextareaEditor.js +0 -15
  199. package/lib/esm/components/TextareaEditor.js.map +0 -1
  200. package/lib/esm/components/ToggleEditor.d.ts +0 -22
  201. package/lib/esm/components/ToggleEditor.js +0 -23
  202. package/lib/esm/components/ToggleEditor.js.map +0 -1
  203. package/lib/esm/ontologies/lincd-ui.d.ts +0 -6
  204. package/lib/esm/ontologies/lincd-ui.js +0 -18
  205. package/lib/esm/ontologies/lincd-ui.js.map +0 -1
  206. package/lib/esm/package.d.ts +0 -3
  207. package/lib/esm/package.js +0 -4
  208. package/lib/esm/package.js.map +0 -1
  209. package/tsconfig-cjs.json +0 -8
  210. 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;