@_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,175 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type {
3
+ InstanceDeletionExecuteInput,
4
+ InstanceDeletionPreview,
5
+ InstanceDeletionPreviewInput,
6
+ InstanceDeletionResult,
7
+ InstanceDeletionTarget,
8
+ } from '../shape/contracts.js';
9
+ import { ConfirmDialog } from '@_linked/primitives/components/ConfirmDialog';
10
+ import { Spinner } from '@_linked/primitives/components/Spinner';
11
+ import style from './InstanceDeletionDialog.module.css';
12
+
13
+ type PreviewDeletion = (input: InstanceDeletionPreviewInput) => Promise<InstanceDeletionPreview>;
14
+ type ExecuteDeletion = (input: InstanceDeletionExecuteInput) => Promise<InstanceDeletionResult>;
15
+ // No module-level defaults. They used to be
16
+ // `(input) => Project.previewInstanceDeletion(input)`, which meant this organism carried a
17
+ // control-plane dependency even for callers that injected their own — and made the
18
+ // injection look optional when it is the whole seam.
19
+
20
+ export interface InstanceDeletionDialogProps {
21
+ isOpen: boolean;
22
+ projectId: string;
23
+ targets: InstanceDeletionTarget[];
24
+ labels?: Record<string, string>;
25
+ onClose: () => void;
26
+ onDeleted: (result: InstanceDeletionResult) => void;
27
+ /** How to preview the cascade. Required: the organism has no opinion on where data lives. */
28
+ previewDeletion: PreviewDeletion;
29
+ /** How to perform it. */
30
+ executeDeletion: ExecuteDeletion;
31
+ }
32
+
33
+ function errorMessage(error: unknown): string {
34
+ return error instanceof Error ? error.message : String(error);
35
+ }
36
+
37
+ function replacementPreview(error: unknown): InstanceDeletionPreview | undefined {
38
+ if (!error || typeof error !== 'object') return undefined;
39
+ const candidate = error as { code?: string; preview?: InstanceDeletionPreview };
40
+ return candidate.code === 'impact-changed' ? candidate.preview : undefined;
41
+ }
42
+
43
+ export function InstanceDeletionDialog({
44
+ isOpen,
45
+ projectId,
46
+ targets,
47
+ labels = {},
48
+ onClose,
49
+ onDeleted,
50
+ previewDeletion,
51
+ executeDeletion,
52
+ }: InstanceDeletionDialogProps) {
53
+ const [preview, setPreview] = useState<InstanceDeletionPreview>();
54
+ const [isLoading, setIsLoading] = useState(false);
55
+ const [isDeleting, setIsDeleting] = useState(false);
56
+ const [error, setError] = useState<string>();
57
+ const [impactChanged, setImpactChanged] = useState(false);
58
+ const targetKey = useMemo(
59
+ () => targets.map(({ instanceId, shapeIri }) => `${shapeIri}\u0000${instanceId}`).join('\u0001'),
60
+ [targets],
61
+ );
62
+
63
+ useEffect(() => {
64
+ if (!isOpen || !projectId || targets.length === 0) return;
65
+ let active = true;
66
+ setPreview(undefined);
67
+ setError(undefined);
68
+ setImpactChanged(false);
69
+ setIsLoading(true);
70
+ previewDeletion({ projectId, targets })
71
+ .then((next) => {
72
+ if (active) setPreview(next);
73
+ })
74
+ .catch((reason) => {
75
+ if (active) setError(errorMessage(reason));
76
+ })
77
+ .finally(() => {
78
+ if (active) setIsLoading(false);
79
+ });
80
+ return () => { active = false; };
81
+ }, [isOpen, projectId, targetKey, previewDeletion]);
82
+
83
+ const confirm = async () => {
84
+ if (!preview || isDeleting) return;
85
+ setIsDeleting(true);
86
+ setError(undefined);
87
+ setImpactChanged(false);
88
+ try {
89
+ const result = await executeDeletion({
90
+ projectId,
91
+ targets,
92
+ expectedImpactHash: preview.impactHash,
93
+ });
94
+ onDeleted(result);
95
+ onClose();
96
+ } catch (reason) {
97
+ const replacement = replacementPreview(reason);
98
+ if (replacement) {
99
+ setPreview(replacement);
100
+ setImpactChanged(true);
101
+ } else {
102
+ setError(errorMessage(reason));
103
+ }
104
+ } finally {
105
+ setIsDeleting(false);
106
+ }
107
+ };
108
+
109
+ const message = (
110
+ <div className={style.summary}>
111
+ <p className={style.intro}>
112
+ Review references and document provenance before deleting. This preview is checked again when you confirm.
113
+ </p>
114
+ {isLoading && <div role="status"><Spinner size="small" /> Checking current impact…</div>}
115
+ {impactChanged && (
116
+ <p className={style.changed} role="alert">
117
+ The impact changed since the previous preview. Review the updated details, then confirm again.
118
+ </p>
119
+ )}
120
+ {error && <p className={style.error} role="alert">{error}</p>}
121
+ {preview && (
122
+ <div className={style.targets}>
123
+ {preview.targets.map((target) => {
124
+ const hasImpact = target.inboundReferences.length > 0 || target.sourceDocuments.length > 0;
125
+ return (
126
+ <section className={style.target} key={`${target.shapeIri}:${target.instanceId}`}>
127
+ <strong>{labels[target.instanceId] || target.instanceId}</strong>
128
+ <small>{target.instanceId}</small>
129
+ {!hasImpact && <p className={style.empty}>No inbound references or source documents found.</p>}
130
+ {target.sourceDocuments.length > 0 && (
131
+ <ul className={style.impactList} aria-label="Source documents">
132
+ {target.sourceDocuments.map((documentId) => (
133
+ <li key={documentId}>Produced from document {documentId}</li>
134
+ ))}
135
+ </ul>
136
+ )}
137
+ {target.inboundReferences.length > 0 && (
138
+ <ul className={style.impactList} aria-label="Inbound references">
139
+ {target.inboundReferences.map((reference) => (
140
+ <li key={`${reference.subjectId}:${reference.propertyIri}`}>
141
+ Referenced by {reference.subjectId} through {reference.propertyIri}
142
+ </li>
143
+ ))}
144
+ </ul>
145
+ )}
146
+ {target.pendingRecreation && (
147
+ <p className={style.warning}>
148
+ This instance came from Document Studio. Deleting it also suppresses automatic recreation until it is explicitly restored.
149
+ </p>
150
+ )}
151
+ </section>
152
+ );
153
+ })}
154
+ </div>
155
+ )}
156
+ </div>
157
+ );
158
+
159
+ return (
160
+ <ConfirmDialog
161
+ open={isOpen}
162
+ onOpenChange={(next) => {
163
+ if (!next) onClose();
164
+ }}
165
+ onConfirm={confirm}
166
+ title={targets.length === 1 ? 'Delete this instance?' : `Delete ${targets.length} instances?`}
167
+ message={message}
168
+ confirmText={targets.length === 1 ? 'Delete instance' : `Delete ${targets.length} instances`}
169
+ tone="danger"
170
+ isLoading={isDeleting}
171
+ confirmDisabled={!preview || isLoading}
172
+ loadingContent={<><Spinner size="small" /> Deleting</>}
173
+ />
174
+ );
175
+ }
@@ -0,0 +1,35 @@
1
+ /* ============================================================================
2
+ InstanceOverview.module.css
3
+
4
+ Instance data display styles for table cells.
5
+ Uses semantic tokens from theme.css.
6
+ ============================================================================ */
7
+
8
+ /* === Node Badge === */
9
+ .node {
10
+ display: inline-block;
11
+ background-color: var(--instance-overview-gray-100, var(--color-gray-100));
12
+ padding: 0.25rem 0.5rem;
13
+ border-radius: var(--radius-selector);
14
+ color: var(--table-cell-text);
15
+ font-size: var(--typography-small-size);
16
+ }
17
+
18
+ /* === Literal Value === */
19
+ .literal {
20
+ display: inline-block;
21
+ color: var(--table-cell-text);
22
+ }
23
+
24
+ /* ============================================================================
25
+ Dark Mode
26
+ ============================================================================ */
27
+
28
+ [data-theme='dark'] .node {
29
+ background-color: var(--instance-overview-gray-700, var(--color-gray-700));
30
+ color: var(--table-cell-text);
31
+ }
32
+
33
+ [data-theme='dark'] .literal {
34
+ color: var(--table-cell-text);
35
+ }
@@ -0,0 +1,374 @@
1
+ import { ColumnDef } from '@tanstack/react-table';
2
+ import type { ShapeInstancesQueryConfig } from '../shape/contracts.js';
3
+ import type { PropertyShapeWire, NodeShapeWire } from '@_linked/core/shapes/nodeShapeWire';
4
+ import { useDataManagerHost } from '../hostContext.js';
5
+ import type { TableMode } from '../columns.js';
6
+ import { getNodeDisplay } from '../shape/nodeDisplay.js';
7
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
8
+ import ImageThumb from '@_linked/primitives/components/ImageThumb';
9
+ import {NodeBadge} from './NodeBadge.js';
10
+ import style from './InstanceOverview.module.css';
11
+ import ReactTable, { type ReactTableProps } from './ReactTable.js';
12
+
13
+ type PropertiesMap = Record<string, PropertyShapeWire>;
14
+ type InstanceRecord<P extends PropertiesMap> = Partial<
15
+ Record<keyof P, unknown>
16
+ >;
17
+
18
+ export interface BatchResult {
19
+ succeeded: string[];
20
+ failed: string[];
21
+ }
22
+
23
+ interface InstanceOverviewProps<
24
+ P extends PropertiesMap = PropertiesMap,
25
+ I extends InstanceRecord<P> = InstanceRecord<P>,
26
+ > {
27
+ customColumns?: any;
28
+ instances: I[];
29
+ properties: P;
30
+ shape?: NodeShapeWire;
31
+ isLoading?: boolean;
32
+ config?: ShapeInstancesQueryConfig;
33
+ setConfig?: (updated: ShapeInstancesQueryConfig) => void;
34
+ onSelectionChange?: (selectedRows: number[], selectedInstances: I[]) => void;
35
+ isPickMode?: boolean;
36
+ pickMaxCount?: number;
37
+ pickAlreadySelected?: { id: string; label: string; image?: string }[];
38
+ deletionProjectId?: string;
39
+ previewDeletion?: ReactTableProps['previewDeletion'];
40
+ executeDeletion?: ReactTableProps['executeDeletion'];
41
+ onInstancesDeleted?: ReactTableProps['onInstancesDeleted'];
42
+ onBatchUpdate?: (changes: Record<string, any>, uris: string[]) => Promise<BatchResult>;
43
+ tableMode?: TableMode;
44
+ onTableModeChange?: (mode: TableMode) => void;
45
+ }
46
+
47
+ function InstanceOverview<
48
+ P extends PropertiesMap,
49
+ I extends InstanceRecord<P>,
50
+ >({
51
+ customColumns,
52
+ instances,
53
+ properties,
54
+ shape,
55
+ isLoading = false,
56
+ config,
57
+ setConfig,
58
+ onSelectionChange,
59
+ isPickMode = false,
60
+ pickMaxCount,
61
+ pickAlreadySelected,
62
+ deletionProjectId,
63
+ previewDeletion,
64
+ executeDeletion,
65
+ onInstancesDeleted,
66
+ onBatchUpdate,
67
+ tableMode,
68
+ onTableModeChange,
69
+ }: InstanceOverviewProps<P, I>) {
70
+ // Pick mode is a host flow: the host knows where the picker was opened from and what to
71
+ // do with the choice. This component only knows that a choice was made.
72
+ //
73
+ // There is no fallback. The fallback was CN's sessionStorage-plus-router round trip,
74
+ // which is exactly the app-specific knowledge that has no business in a component meant
75
+ // to run inside someone else's app — and holding on to it kept `routes.tsx` in the
76
+ // import graph, which drags in the whole application.
77
+ const host = useDataManagerHost();
78
+
79
+ const handlePickInstance = (instance: { id: string; label: string; image?: string }) =>
80
+ host.picking?.select([instance]);
81
+
82
+ const handlePickInstances = (
83
+ instances: { id: string; label: string; image?: string }[],
84
+ ) => host.picking?.select(instances);
85
+
86
+ // URI-based selection — stable across sort/filter/pagination
87
+ const [selectedUris, setSelectedUris] = useState<Set<string>>(new Set());
88
+
89
+ const toggleSelectAll = useCallback(() => {
90
+ setSelectedUris((prev) => {
91
+ if (prev.size > 0) {
92
+ // Deselect all
93
+ return new Set();
94
+ }
95
+ // Select all visible instances
96
+ const allUris = new Set<string>();
97
+ (instances as any[]).forEach((inst) => {
98
+ if (inst?.id) allUris.add(inst.id);
99
+ });
100
+ return allUris;
101
+ });
102
+ }, [instances]);
103
+
104
+ const toggleRowSelection = useCallback((uri: string) => {
105
+ setSelectedUris((prev) => {
106
+ const next = new Set(prev);
107
+ if (next.has(uri)) {
108
+ next.delete(uri);
109
+ } else {
110
+ next.add(uri);
111
+ }
112
+ return next;
113
+ });
114
+ }, []);
115
+
116
+ const isRowSelected = useCallback(
117
+ (uri: string) => selectedUris.has(uri),
118
+ [selectedUris]
119
+ );
120
+
121
+ const clearSelection = useCallback(() => {
122
+ setSelectedUris(new Set());
123
+ }, []);
124
+
125
+ // Remove succeeded URIs from selection after batch ops
126
+ const removeFromSelection = useCallback((uris: string[]) => {
127
+ setSelectedUris((prev) => {
128
+ const next = new Set(prev);
129
+ for (const uri of uris) next.delete(uri);
130
+ return next;
131
+ });
132
+ }, []);
133
+
134
+ const hasSelection = selectedUris.size > 0;
135
+
136
+ // Pre-select rows matching pickAlreadySelected when data loads
137
+ const preSelectDone = useRef(false);
138
+ useEffect(() => {
139
+ if (preSelectDone.current || !isPickMode || !pickAlreadySelected?.length || !instances?.length) return;
140
+ preSelectDone.current = true;
141
+ const alreadyIds = new Set(pickAlreadySelected.map((a) => a.id));
142
+ const matching = new Set<string>();
143
+ (instances as any[]).forEach((inst) => {
144
+ if (inst?.id && alreadyIds.has(inst.id)) matching.add(inst.id);
145
+ });
146
+ if (matching.size > 0) {
147
+ setSelectedUris(matching);
148
+ }
149
+ }, [instances, isPickMode, pickAlreadySelected]);
150
+
151
+ const [_config, _setConfig] = useState(
152
+ config || {
153
+ pageIndex: 0,
154
+ pageSize: 10,
155
+ filters: [],
156
+ }
157
+ );
158
+ //config state can be handled by the parent or here
159
+ const __setConfig = setConfig || _setConfig;
160
+
161
+ const handleNodeClick = (nodeValue: any, propertyName: string) => {
162
+ // Get the property details to access valueShape
163
+ const property = properties[propertyName];
164
+ if (property && property.valueShape && nodeValue.id) {
165
+ // Navigate to the view page for this node
166
+ host.navigate?.toInstance(property.valueShape.id, nodeValue.id);
167
+ }
168
+ };
169
+
170
+ // Notify parent when selection changes (chatbot integration)
171
+ useEffect(() => {
172
+ if (onSelectionChange) {
173
+ const selectedInstances = (instances as any[]).filter((inst) => inst?.id && selectedUris.has(inst.id));
174
+ onSelectionChange(
175
+ Array.from(selectedUris).map((uri) => (instances as any[]).findIndex((inst) => inst?.id === uri)).filter((i) => i >= 0),
176
+ selectedInstances
177
+ );
178
+ }
179
+ }, [selectedUris]);
180
+
181
+
182
+ let initialColumns: ColumnDef<I>[] = [
183
+ {
184
+ id: 'selection',
185
+ cell: ({ row }) => (
186
+ <input
187
+ type="hidden"
188
+ onChange={() => toggleRowSelection((row.original as any)?.id)}
189
+ checked={isRowSelected((row.original as any)?.id)}
190
+ />
191
+ ),
192
+ },
193
+ ];
194
+
195
+ let columnConfig;
196
+
197
+ const cell = (info) => {
198
+ console.log('info cell', info);
199
+ return info.getValue();
200
+ };
201
+
202
+ if (customColumns) {
203
+ columnConfig = customColumns.map((config) => {
204
+ return {
205
+ id: config.label || config.property.label,
206
+ accessorKey: config.label || config.property.label,
207
+ cell: cell,
208
+ accessorFn: config.renderCell,
209
+ };
210
+ });
211
+ initialColumns.push(...columnConfig);
212
+ } else {
213
+ //dynamically define the columns based on the properties of the shape
214
+ Object.entries(properties || {}).forEach(([propLabel, propShape]) => {
215
+ //skip rdf:type
216
+ if (propShape.id === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type') {
217
+ return;
218
+ }
219
+ columnConfig = {
220
+ id: propShape.id,
221
+ // accessorFn, NOT accessorKey: TanStack reads a key as a deep PATH, so a property
222
+ // labelled "Avg. basket" was looked up as row["Avg"][" basket"] and rendered blank
223
+ // while every other column on the same row was fine. Labels are written by people and
224
+ // routinely contain a dot; reading the row directly removes the trap.
225
+ accessorFn: (row: any) => row?.[propLabel],
226
+ cell: (info) => {
227
+ let value = info.getValue();
228
+
229
+ const parseLiteral = (value: any) => {
230
+ let literalText;
231
+ if (value instanceof Date) {
232
+ literalText = value.toLocaleString();
233
+ } else if (typeof value === 'boolean') {
234
+ literalText = value ? 'Yes' : 'No';
235
+ } else if (typeof value === 'number') {
236
+ literalText = value.toString();
237
+ } else if (typeof value === 'string') {
238
+ literalText = value;
239
+ } else {
240
+ literalText = value;
241
+ }
242
+ return <span className={style.literal}>{literalText}</span>;
243
+ };
244
+
245
+ const isNode = (value: any) => {
246
+ return value && typeof value === 'object' && value.id;
247
+ };
248
+
249
+ const parseValue = (value: any, forceLiteral: boolean = false) => {
250
+ // Handle ImageObject (has contentUrl)
251
+ if (value && typeof value === 'object' && value.contentUrl) {
252
+ return <ImageThumb src={value.contentUrl} size={32} alt={value.label || propLabel} clickToEnlarge={true} />;
253
+ }
254
+
255
+ if (isNode(value)) {
256
+ const images = value.image;
257
+ const firstImage = Array.isArray(images) ? images[0] : images;
258
+
259
+ if (firstImage) {
260
+ const imgSrc = typeof firstImage === 'object' && firstImage.contentUrl
261
+ ? String(firstImage.contentUrl)
262
+ : String(firstImage);
263
+ return <ImageThumb src={imgSrc} size={32} alt={value.label || propLabel} clickToEnlarge={true} />;
264
+ }
265
+
266
+ let identifier;
267
+ const otherProperties = Object.keys(value).filter(key => key !== 'id');
268
+ if (otherProperties.length > 0) {
269
+ for (const property of otherProperties) {
270
+ if (value[property]) {
271
+ identifier = value[property];
272
+ break;
273
+ }
274
+ }
275
+ }
276
+ if(!identifier) {
277
+ identifier = getNodeDisplay(value);
278
+ }
279
+ return forceLiteral ? (
280
+ identifier
281
+ ) : (
282
+ <NodeBadge
283
+ text={identifier}
284
+ onClick={() => handleNodeClick(value, propLabel)}
285
+ clickable={true}
286
+ />
287
+ );
288
+ }
289
+ return parseLiteral(value);
290
+ };
291
+
292
+ if (Array.isArray(value)) {
293
+ const maxValues = 3;
294
+ const valuesToShow = value.slice(0, maxValues);
295
+ const leftOver = value.length - maxValues;
296
+ if (leftOver > 0) {
297
+ const title = `${leftOver} more values: ${value
298
+ .slice(maxValues)
299
+ .map((val) => parseValue(val, true))
300
+ .join(', ')}`;
301
+ valuesToShow.push(
302
+ <NodeBadge
303
+ text={`(${leftOver}...)`}
304
+ title={title}
305
+ clickable={false}
306
+ />
307
+ );
308
+ }
309
+ return valuesToShow.map((val, index) => {
310
+ const parsed = parseValue(val);
311
+ const isCurrentLiteral =
312
+ !isNode(val) && !React.isValidElement(val);
313
+ const nextVal = valuesToShow[index + 1];
314
+ const isNextLiteral =
315
+ nextVal && !isNode(nextVal) && !React.isValidElement(nextVal);
316
+ const needsComma = isCurrentLiteral && isNextLiteral;
317
+
318
+ return (
319
+ <React.Fragment key={index}>
320
+ {parsed}
321
+ {needsComma ? ', ' : ' '}
322
+ </React.Fragment>
323
+ );
324
+ });
325
+ } else {
326
+ return parseValue(value);
327
+ }
328
+ },
329
+ header: propLabel
330
+ .replace(/([A-Z])/g, ' $1')
331
+ .replace(/_/g, ' ')
332
+ .replace(/^./, (char) => char.toUpperCase()),
333
+ footer: (props) => props.column.id,
334
+ };
335
+
336
+ initialColumns.push(columnConfig);
337
+ });
338
+ }
339
+
340
+ const columns = React.useMemo<ColumnDef<I>[]>(() => initialColumns, [properties]);
341
+
342
+ return (
343
+ <ReactTable
344
+ data={instances}
345
+ columns={columns}
346
+ toggleSelectAll={toggleSelectAll}
347
+ isRowSelected={isRowSelected}
348
+ selectedUris={selectedUris}
349
+ toggleRowSelection={toggleRowSelection}
350
+ clearSelection={clearSelection}
351
+ removeFromSelection={removeFromSelection}
352
+ hasSelection={hasSelection}
353
+ config={_config}
354
+ setConfig={__setConfig}
355
+ shape={shape}
356
+ properties={properties}
357
+ isLoading={isLoading}
358
+ isPickMode={isPickMode}
359
+ onPickInstance={handlePickInstance}
360
+ onPickInstances={handlePickInstances}
361
+ pickMaxCount={pickMaxCount}
362
+ pickAlreadySelected={pickAlreadySelected}
363
+ deletionProjectId={deletionProjectId}
364
+ previewDeletion={previewDeletion}
365
+ executeDeletion={executeDeletion}
366
+ onInstancesDeleted={onInstancesDeleted}
367
+ onBatchUpdate={onBatchUpdate}
368
+ tableMode={tableMode}
369
+ onTableModeChange={onTableModeChange}
370
+ />
371
+ );
372
+ }
373
+
374
+ export default InstanceOverview;