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