@_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.
- package/lib/esm/columns.d.ts +58 -0
- package/lib/esm/columns.js +173 -0
- package/lib/esm/columns.js.map +1 -0
- package/lib/esm/components/AddInstanceForms.d.ts +38 -0
- package/lib/esm/components/AddInstanceForms.js +143 -0
- package/lib/esm/components/AddInstanceForms.js.map +1 -0
- package/lib/esm/components/AddInstanceForms.module.css +12 -0
- package/lib/esm/components/BatchEditDrawer.d.ts +13 -0
- package/lib/esm/components/BatchEditDrawer.js +122 -0
- package/lib/esm/components/BatchEditDrawer.js.map +1 -0
- package/lib/esm/components/BatchEditDrawer.module.css +193 -0
- package/lib/esm/components/CustomMultiSelect.d.ts +26 -0
- package/lib/esm/components/CustomMultiSelect.js +344 -0
- package/lib/esm/components/CustomMultiSelect.js.map +1 -0
- package/lib/esm/components/CustomMultiSelect.module.css +291 -0
- package/lib/esm/components/DynamicFormField.d.ts +21 -0
- package/lib/esm/components/DynamicFormField.js +149 -0
- package/lib/esm/components/DynamicFormField.js.map +1 -0
- package/lib/esm/components/DynamicFormField.module.css +169 -0
- package/lib/esm/components/EditInstanceForms.d.ts +15 -0
- package/lib/esm/components/EditInstanceForms.js +118 -0
- package/lib/esm/components/EditInstanceForms.js.map +1 -0
- package/lib/esm/components/FilterBadge.d.ts +20 -0
- package/lib/esm/components/FilterBadge.js +26 -0
- package/lib/esm/components/FilterBadge.js.map +1 -0
- package/lib/esm/components/FilterBadge.module.css +25 -0
- package/lib/esm/components/FormField.d.ts +18 -0
- package/lib/esm/components/FormField.js +9 -0
- package/lib/esm/components/FormField.js.map +1 -0
- package/lib/esm/components/FormField.module.css +57 -0
- package/lib/esm/components/InstanceDeletionDialog.d.ts +17 -0
- package/lib/esm/components/InstanceDeletionDialog.js +83 -0
- package/lib/esm/components/InstanceDeletionDialog.js.map +1 -0
- package/lib/esm/components/InstanceDeletionDialog.module.css +59 -0
- package/lib/esm/components/InstanceOverview.d.ts +43 -0
- package/lib/esm/components/InstanceOverview.js +236 -0
- package/lib/esm/components/InstanceOverview.js.map +1 -0
- package/lib/esm/components/InstanceOverview.module.css +35 -0
- package/lib/esm/components/InstanceView.d.ts +29 -0
- package/lib/esm/components/InstanceView.js +97 -0
- package/lib/esm/components/InstanceView.js.map +1 -0
- package/lib/esm/components/InstanceView.module.css +157 -0
- package/lib/esm/components/NodeBadge.d.ts +25 -0
- package/lib/esm/components/NodeBadge.js +29 -0
- package/lib/esm/components/NodeBadge.js.map +1 -0
- package/lib/esm/components/NodeValuesEditor.d.ts +15 -0
- package/lib/esm/components/NodeValuesEditor.js +29 -0
- package/lib/esm/components/NodeValuesEditor.js.map +1 -0
- package/lib/esm/components/NodeValuesEditor.module.css +4 -0
- package/lib/esm/components/ReactTable.d.ts +59 -0
- package/lib/esm/components/ReactTable.js +378 -0
- package/lib/esm/components/ReactTable.js.map +1 -0
- package/lib/esm/components/ReactTable.module.css +950 -0
- package/lib/esm/components/TextfieldEditor.d.ts +1 -2
- package/lib/esm/components/TextfieldEditor.js +2 -2
- package/lib/esm/components/TextfieldEditor.js.map +1 -1
- package/lib/esm/components/forms.module.css +96 -0
- package/lib/esm/host.d.ts +196 -0
- package/lib/esm/host.js +25 -0
- package/lib/esm/host.js.map +1 -0
- package/lib/esm/hostContext.d.ts +34 -0
- package/lib/esm/hostContext.js +95 -0
- package/lib/esm/hostContext.js.map +1 -0
- package/lib/esm/index.d.ts +60 -10
- package/lib/esm/index.js +60 -14
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/read.d.ts +48 -0
- package/lib/esm/read.js +96 -0
- package/lib/esm/read.js.map +1 -0
- package/lib/esm/search.d.ts +32 -0
- package/lib/esm/search.js +87 -0
- package/lib/esm/search.js.map +1 -0
- package/lib/esm/shape/contracts.d.ts +58 -0
- package/lib/esm/shape/contracts.js +15 -0
- package/lib/esm/shape/contracts.js.map +1 -0
- package/lib/esm/shape/naming.d.ts +30 -0
- package/lib/esm/shape/naming.js +52 -0
- package/lib/esm/shape/naming.js.map +1 -0
- package/lib/esm/shape/nodeDisplay.d.ts +36 -0
- package/lib/esm/shape/nodeDisplay.js +117 -0
- package/lib/esm/shape/nodeDisplay.js.map +1 -0
- package/lib/esm/shape/propertyTypes.d.ts +12 -0
- package/lib/esm/shape/propertyTypes.js +60 -0
- package/lib/esm/shape/propertyTypes.js.map +1 -0
- package/lib/esm/shape/propertyVisibility.d.ts +56 -0
- package/lib/esm/shape/propertyVisibility.js +112 -0
- package/lib/esm/shape/propertyVisibility.js.map +1 -0
- package/lib/esm/shape/shapeDependencies.d.ts +41 -0
- package/lib/esm/shape/shapeDependencies.js +141 -0
- package/lib/esm/shape/shapeDependencies.js.map +1 -0
- package/lib/esm/shape/tableFilters.d.ts +35 -0
- package/lib/esm/shape/tableFilters.js +171 -0
- package/lib/esm/shape/tableFilters.js.map +1 -0
- package/lib/esm/shape/useFormDependencies.d.ts +38 -0
- package/lib/esm/shape/useFormDependencies.js +178 -0
- package/lib/esm/shape/useFormDependencies.js.map +1 -0
- package/lib/esm/shape/validation.d.ts +15 -0
- package/lib/esm/shape/validation.js +106 -0
- package/lib/esm/shape/validation.js.map +1 -0
- package/lib/esm/tokens.css +25 -0
- package/lib/esm/types.d.ts +22 -0
- package/lib/esm/types.js +1 -0
- package/lib/esm/types.js.map +1 -0
- package/lib/esm/write.d.ts +51 -0
- package/lib/esm/write.js +99 -0
- package/lib/esm/write.js.map +1 -0
- package/package.json +44 -18
- package/src/backend.ts +1 -0
- package/src/columns.regression.test.ts +143 -0
- package/src/columns.test.ts +200 -0
- package/src/columns.ts +228 -0
- package/src/components/AddInstanceForms.module.css +12 -0
- package/src/components/AddInstanceForms.tsx +321 -0
- package/src/components/AvatarEditor.tsx +34 -0
- package/src/components/BatchEditDrawer.module.css +193 -0
- package/src/components/BatchEditDrawer.tsx +295 -0
- package/src/components/CheckboxEditor.module.css +6 -0
- package/src/components/CheckboxEditor.tsx +86 -0
- package/src/components/CustomMultiSelect.module.css +291 -0
- package/src/components/CustomMultiSelect.tsx +527 -0
- package/src/components/DateEditor.tsx +50 -0
- package/src/components/DynamicFormField.module.css +169 -0
- package/src/components/DynamicFormField.tsx +357 -0
- package/src/components/EditInstanceForms.tsx +260 -0
- package/src/components/FilterBadge.module.css +25 -0
- package/src/components/FilterBadge.tsx +70 -0
- package/src/components/FormField.module.css +57 -0
- package/src/components/FormField.tsx +59 -0
- package/src/components/ImageValueEditor.module.css +3 -0
- package/src/components/ImageValueEditor.tsx +82 -0
- package/src/components/InstanceDeletionDialog.module.css +59 -0
- package/src/components/InstanceDeletionDialog.tsx +175 -0
- package/src/components/InstanceOverview.module.css +35 -0
- package/src/components/InstanceOverview.tsx +385 -0
- package/src/components/InstanceView.module.css +157 -0
- package/src/components/InstanceView.tsx +212 -0
- package/src/components/NodeBadge.tsx +60 -0
- package/src/components/NodeValuesEditor.module.css +4 -0
- package/src/components/NodeValuesEditor.tsx +64 -0
- package/src/components/RadioButtonEditor.module.css +22 -0
- package/src/components/RadioButtonEditor.tsx +96 -0
- package/src/components/ReactTable.module.css +950 -0
- package/src/components/ReactTable.tsx +1013 -0
- package/src/components/SelectEditor.tsx +85 -0
- package/src/components/ShapeTable.module.css +394 -0
- package/src/components/SwitchEditor.tsx +60 -0
- package/src/components/TextareaEditor.tsx +55 -0
- package/src/components/TextfieldEditor.tsx +67 -0
- package/src/components/ToggleEditor.tsx +63 -0
- package/src/components/forms.module.css +96 -0
- package/src/host.ts +226 -0
- package/src/hostContext.tsx +114 -0
- package/src/index.ts +93 -0
- package/src/package.ts +21 -0
- package/src/read.ts +148 -0
- package/src/search.test.ts +174 -0
- package/src/search.ts +108 -0
- package/src/shape/contracts.ts +66 -0
- package/src/shape/naming.ts +52 -0
- package/src/shape/nodeDisplay.ts +114 -0
- package/src/shape/propertyTypes.ts +86 -0
- package/src/shape/propertyVisibility.ts +180 -0
- package/src/shape/shapeDependencies.ts +183 -0
- package/src/shape/tableFilters.ts +223 -0
- package/src/shape/types.ts +1158 -0
- package/src/shape/useFormDependencies.ts +257 -0
- package/src/shape/validation.ts +121 -0
- package/src/tokens.css +25 -0
- package/src/types.ts +20 -0
- package/src/utils/helper.ts +35 -0
- package/src/write.test.ts +174 -0
- package/src/write.ts +114 -0
- package/.changeset/README.md +0 -20
- package/.changeset/config.json +0 -14
- package/.github/workflows/changeset-check.yml +0 -44
- package/.github/workflows/ci.yml +0 -43
- package/.github/workflows/publish.yml +0 -158
- package/CHANGELOG.md +0 -30
- package/lib/esm/components/AvatarEditor.d.ts +0 -20
- package/lib/esm/components/AvatarEditor.js +0 -7
- package/lib/esm/components/AvatarEditor.js.map +0 -1
- package/lib/esm/components/CheckboxEditor.d.ts +0 -21
- package/lib/esm/components/CheckboxEditor.js +0 -44
- package/lib/esm/components/CheckboxEditor.js.map +0 -1
- package/lib/esm/components/DateEditor.d.ts +0 -20
- package/lib/esm/components/DateEditor.js +0 -22
- package/lib/esm/components/DateEditor.js.map +0 -1
- package/lib/esm/components/RadioButtonEditor.d.ts +0 -23
- package/lib/esm/components/RadioButtonEditor.js +0 -41
- package/lib/esm/components/RadioButtonEditor.js.map +0 -1
- package/lib/esm/components/SelectEditor.d.ts +0 -21
- package/lib/esm/components/SelectEditor.js +0 -30
- package/lib/esm/components/SelectEditor.js.map +0 -1
- package/lib/esm/components/SwitchEditor.d.ts +0 -21
- package/lib/esm/components/SwitchEditor.js +0 -23
- package/lib/esm/components/SwitchEditor.js.map +0 -1
- package/lib/esm/components/TextareaEditor.d.ts +0 -23
- package/lib/esm/components/TextareaEditor.js +0 -15
- package/lib/esm/components/TextareaEditor.js.map +0 -1
- package/lib/esm/components/ToggleEditor.d.ts +0 -22
- package/lib/esm/components/ToggleEditor.js +0 -23
- package/lib/esm/components/ToggleEditor.js.map +0 -1
- package/lib/esm/ontologies/lincd-ui.d.ts +0 -6
- package/lib/esm/ontologies/lincd-ui.js +0 -18
- package/lib/esm/ontologies/lincd-ui.js.map +0 -1
- package/tsconfig-cjs.json +0 -8
- 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,385 @@
|
|
|
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
|
+
/**
|
|
30
|
+
* Total row count of the whole dataset when `instances` is one server-fetched page.
|
|
31
|
+
* Omit it for client-side pagination over the full `instances` array. Forwarded to
|
|
32
|
+
* `ReactTable`, which switches to manual pagination when it is set.
|
|
33
|
+
*/
|
|
34
|
+
totalCount?: number;
|
|
35
|
+
properties: P;
|
|
36
|
+
shape?: NodeShapeWire;
|
|
37
|
+
isLoading?: boolean;
|
|
38
|
+
config?: ShapeInstancesQueryConfig;
|
|
39
|
+
setConfig?: (updated: ShapeInstancesQueryConfig) => void;
|
|
40
|
+
onSelectionChange?: (selectedRows: number[], selectedInstances: I[]) => void;
|
|
41
|
+
isPickMode?: boolean;
|
|
42
|
+
pickMaxCount?: number;
|
|
43
|
+
pickAlreadySelected?: { id: string; label: string; image?: string }[];
|
|
44
|
+
deletionProjectId?: string;
|
|
45
|
+
previewDeletion?: ReactTableProps['previewDeletion'];
|
|
46
|
+
executeDeletion?: ReactTableProps['executeDeletion'];
|
|
47
|
+
onInstancesDeleted?: ReactTableProps['onInstancesDeleted'];
|
|
48
|
+
onBatchUpdate?: (changes: Record<string, any>, uris: string[]) => Promise<BatchResult>;
|
|
49
|
+
tableMode?: TableMode;
|
|
50
|
+
onTableModeChange?: (mode: TableMode) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function InstanceOverview<
|
|
54
|
+
P extends PropertiesMap,
|
|
55
|
+
I extends InstanceRecord<P>,
|
|
56
|
+
>({
|
|
57
|
+
customColumns,
|
|
58
|
+
instances,
|
|
59
|
+
totalCount,
|
|
60
|
+
properties,
|
|
61
|
+
shape,
|
|
62
|
+
isLoading = false,
|
|
63
|
+
config,
|
|
64
|
+
setConfig,
|
|
65
|
+
onSelectionChange,
|
|
66
|
+
isPickMode = false,
|
|
67
|
+
pickMaxCount,
|
|
68
|
+
pickAlreadySelected,
|
|
69
|
+
deletionProjectId,
|
|
70
|
+
previewDeletion,
|
|
71
|
+
executeDeletion,
|
|
72
|
+
onInstancesDeleted,
|
|
73
|
+
onBatchUpdate,
|
|
74
|
+
tableMode,
|
|
75
|
+
onTableModeChange,
|
|
76
|
+
}: InstanceOverviewProps<P, I>) {
|
|
77
|
+
// Pick mode is a host flow: the host knows where the picker was opened from and what to
|
|
78
|
+
// do with the choice. This component only knows that a choice was made.
|
|
79
|
+
//
|
|
80
|
+
// There is no fallback. The fallback was CN's sessionStorage-plus-router round trip,
|
|
81
|
+
// which is exactly the app-specific knowledge that has no business in a component meant
|
|
82
|
+
// to run inside someone else's app — and holding on to it kept `routes.tsx` in the
|
|
83
|
+
// import graph, which drags in the whole application.
|
|
84
|
+
const host = useDataManagerHost();
|
|
85
|
+
|
|
86
|
+
const handlePickInstance = (instance: { id: string; label: string; image?: string }) =>
|
|
87
|
+
host.picking?.select([instance]);
|
|
88
|
+
|
|
89
|
+
const handlePickInstances = (
|
|
90
|
+
instances: { id: string; label: string; image?: string }[],
|
|
91
|
+
) => host.picking?.select(instances);
|
|
92
|
+
|
|
93
|
+
// URI-based selection — stable across sort/filter/pagination
|
|
94
|
+
const [selectedUris, setSelectedUris] = useState<Set<string>>(new Set());
|
|
95
|
+
|
|
96
|
+
const toggleSelectAll = useCallback(() => {
|
|
97
|
+
setSelectedUris((prev) => {
|
|
98
|
+
if (prev.size > 0) {
|
|
99
|
+
// Deselect all
|
|
100
|
+
return new Set();
|
|
101
|
+
}
|
|
102
|
+
// Select all visible instances
|
|
103
|
+
const allUris = new Set<string>();
|
|
104
|
+
(instances as any[]).forEach((inst) => {
|
|
105
|
+
if (inst?.id) allUris.add(inst.id);
|
|
106
|
+
});
|
|
107
|
+
return allUris;
|
|
108
|
+
});
|
|
109
|
+
}, [instances]);
|
|
110
|
+
|
|
111
|
+
const toggleRowSelection = useCallback((uri: string) => {
|
|
112
|
+
setSelectedUris((prev) => {
|
|
113
|
+
const next = new Set(prev);
|
|
114
|
+
if (next.has(uri)) {
|
|
115
|
+
next.delete(uri);
|
|
116
|
+
} else {
|
|
117
|
+
next.add(uri);
|
|
118
|
+
}
|
|
119
|
+
return next;
|
|
120
|
+
});
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
const isRowSelected = useCallback(
|
|
124
|
+
(uri: string) => selectedUris.has(uri),
|
|
125
|
+
[selectedUris]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const clearSelection = useCallback(() => {
|
|
129
|
+
setSelectedUris(new Set());
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
// Remove succeeded URIs from selection after batch ops
|
|
133
|
+
const removeFromSelection = useCallback((uris: string[]) => {
|
|
134
|
+
setSelectedUris((prev) => {
|
|
135
|
+
const next = new Set(prev);
|
|
136
|
+
for (const uri of uris) next.delete(uri);
|
|
137
|
+
return next;
|
|
138
|
+
});
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
const hasSelection = selectedUris.size > 0;
|
|
142
|
+
|
|
143
|
+
// Pre-select rows matching pickAlreadySelected when data loads
|
|
144
|
+
const preSelectDone = useRef(false);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (preSelectDone.current || !isPickMode || !pickAlreadySelected?.length || !instances?.length) return;
|
|
147
|
+
preSelectDone.current = true;
|
|
148
|
+
const alreadyIds = new Set(pickAlreadySelected.map((a) => a.id));
|
|
149
|
+
const matching = new Set<string>();
|
|
150
|
+
(instances as any[]).forEach((inst) => {
|
|
151
|
+
if (inst?.id && alreadyIds.has(inst.id)) matching.add(inst.id);
|
|
152
|
+
});
|
|
153
|
+
if (matching.size > 0) {
|
|
154
|
+
setSelectedUris(matching);
|
|
155
|
+
}
|
|
156
|
+
}, [instances, isPickMode, pickAlreadySelected]);
|
|
157
|
+
|
|
158
|
+
const [_config, _setConfig] = useState(
|
|
159
|
+
config || {
|
|
160
|
+
pageIndex: 0,
|
|
161
|
+
pageSize: 10,
|
|
162
|
+
filters: [],
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
// Config state can be handled by the parent or here. When the parent owns it, `config` is
|
|
166
|
+
// the live value and `_config` is only the initial snapshot — so the table must read
|
|
167
|
+
// `config` when present, or a parent-driven page change (server-side pagination) would
|
|
168
|
+
// never reach it.
|
|
169
|
+
const __setConfig = setConfig || _setConfig;
|
|
170
|
+
|
|
171
|
+
const handleNodeClick = (nodeValue: any, propertyName: string) => {
|
|
172
|
+
// Get the property details to access valueShape
|
|
173
|
+
const property = properties[propertyName];
|
|
174
|
+
if (property && property.valueShape && nodeValue.id) {
|
|
175
|
+
// Navigate to the view page for this node
|
|
176
|
+
host.navigate?.toInstance(property.valueShape.id, nodeValue.id);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Notify parent when selection changes (chatbot integration)
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (onSelectionChange) {
|
|
183
|
+
const selectedInstances = (instances as any[]).filter((inst) => inst?.id && selectedUris.has(inst.id));
|
|
184
|
+
onSelectionChange(
|
|
185
|
+
Array.from(selectedUris).map((uri) => (instances as any[]).findIndex((inst) => inst?.id === uri)).filter((i) => i >= 0),
|
|
186
|
+
selectedInstances
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}, [selectedUris]);
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
let initialColumns: ColumnDef<I>[] = [
|
|
193
|
+
{
|
|
194
|
+
id: 'selection',
|
|
195
|
+
cell: ({ row }) => (
|
|
196
|
+
<input
|
|
197
|
+
type="hidden"
|
|
198
|
+
onChange={() => toggleRowSelection((row.original as any)?.id)}
|
|
199
|
+
checked={isRowSelected((row.original as any)?.id)}
|
|
200
|
+
/>
|
|
201
|
+
),
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
let columnConfig;
|
|
206
|
+
|
|
207
|
+
const cell = (info) => {
|
|
208
|
+
console.log('info cell', info);
|
|
209
|
+
return info.getValue();
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
if (customColumns) {
|
|
213
|
+
columnConfig = customColumns.map((config) => {
|
|
214
|
+
return {
|
|
215
|
+
id: config.label || config.property.label,
|
|
216
|
+
accessorKey: config.label || config.property.label,
|
|
217
|
+
cell: cell,
|
|
218
|
+
accessorFn: config.renderCell,
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
initialColumns.push(...columnConfig);
|
|
222
|
+
} else {
|
|
223
|
+
//dynamically define the columns based on the properties of the shape
|
|
224
|
+
Object.entries(properties || {}).forEach(([propLabel, propShape]) => {
|
|
225
|
+
//skip rdf:type
|
|
226
|
+
if (propShape.id === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type') {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
columnConfig = {
|
|
230
|
+
id: propShape.id,
|
|
231
|
+
// accessorFn, NOT accessorKey: TanStack reads a key as a deep PATH, so a property
|
|
232
|
+
// labelled "Avg. basket" was looked up as row["Avg"][" basket"] and rendered blank
|
|
233
|
+
// while every other column on the same row was fine. Labels are written by people and
|
|
234
|
+
// routinely contain a dot; reading the row directly removes the trap.
|
|
235
|
+
accessorFn: (row: any) => row?.[propLabel],
|
|
236
|
+
cell: (info) => {
|
|
237
|
+
let value = info.getValue();
|
|
238
|
+
|
|
239
|
+
const parseLiteral = (value: any) => {
|
|
240
|
+
let literalText;
|
|
241
|
+
if (value instanceof Date) {
|
|
242
|
+
literalText = value.toLocaleString();
|
|
243
|
+
} else if (typeof value === 'boolean') {
|
|
244
|
+
literalText = value ? 'Yes' : 'No';
|
|
245
|
+
} else if (typeof value === 'number') {
|
|
246
|
+
literalText = value.toString();
|
|
247
|
+
} else if (typeof value === 'string') {
|
|
248
|
+
literalText = value;
|
|
249
|
+
} else {
|
|
250
|
+
literalText = value;
|
|
251
|
+
}
|
|
252
|
+
return <span className={style.literal}>{literalText}</span>;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const isNode = (value: any) => {
|
|
256
|
+
return value && typeof value === 'object' && value.id;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const parseValue = (value: any, forceLiteral: boolean = false) => {
|
|
260
|
+
// Handle ImageObject (has contentUrl)
|
|
261
|
+
if (value && typeof value === 'object' && value.contentUrl) {
|
|
262
|
+
return <ImageThumb src={value.contentUrl} size={32} alt={value.label || propLabel} clickToEnlarge={true} />;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (isNode(value)) {
|
|
266
|
+
const images = value.image;
|
|
267
|
+
const firstImage = Array.isArray(images) ? images[0] : images;
|
|
268
|
+
|
|
269
|
+
if (firstImage) {
|
|
270
|
+
const imgSrc = typeof firstImage === 'object' && firstImage.contentUrl
|
|
271
|
+
? String(firstImage.contentUrl)
|
|
272
|
+
: String(firstImage);
|
|
273
|
+
return <ImageThumb src={imgSrc} size={32} alt={value.label || propLabel} clickToEnlarge={true} />;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let identifier;
|
|
277
|
+
const otherProperties = Object.keys(value).filter(key => key !== 'id');
|
|
278
|
+
if (otherProperties.length > 0) {
|
|
279
|
+
for (const property of otherProperties) {
|
|
280
|
+
if (value[property]) {
|
|
281
|
+
identifier = value[property];
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if(!identifier) {
|
|
287
|
+
identifier = getNodeDisplay(value);
|
|
288
|
+
}
|
|
289
|
+
return forceLiteral ? (
|
|
290
|
+
identifier
|
|
291
|
+
) : (
|
|
292
|
+
<NodeBadge
|
|
293
|
+
text={identifier}
|
|
294
|
+
onClick={() => handleNodeClick(value, propLabel)}
|
|
295
|
+
clickable={true}
|
|
296
|
+
/>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return parseLiteral(value);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
if (Array.isArray(value)) {
|
|
303
|
+
const maxValues = 3;
|
|
304
|
+
const valuesToShow = value.slice(0, maxValues);
|
|
305
|
+
const leftOver = value.length - maxValues;
|
|
306
|
+
if (leftOver > 0) {
|
|
307
|
+
const title = `${leftOver} more values: ${value
|
|
308
|
+
.slice(maxValues)
|
|
309
|
+
.map((val) => parseValue(val, true))
|
|
310
|
+
.join(', ')}`;
|
|
311
|
+
valuesToShow.push(
|
|
312
|
+
<NodeBadge
|
|
313
|
+
text={`(${leftOver}...)`}
|
|
314
|
+
title={title}
|
|
315
|
+
clickable={false}
|
|
316
|
+
/>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return valuesToShow.map((val, index) => {
|
|
320
|
+
const parsed = parseValue(val);
|
|
321
|
+
const isCurrentLiteral =
|
|
322
|
+
!isNode(val) && !React.isValidElement(val);
|
|
323
|
+
const nextVal = valuesToShow[index + 1];
|
|
324
|
+
const isNextLiteral =
|
|
325
|
+
nextVal && !isNode(nextVal) && !React.isValidElement(nextVal);
|
|
326
|
+
const needsComma = isCurrentLiteral && isNextLiteral;
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<React.Fragment key={index}>
|
|
330
|
+
{parsed}
|
|
331
|
+
{needsComma ? ', ' : ' '}
|
|
332
|
+
</React.Fragment>
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
return parseValue(value);
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
header: propLabel
|
|
340
|
+
.replace(/([A-Z])/g, ' $1')
|
|
341
|
+
.replace(/_/g, ' ')
|
|
342
|
+
.replace(/^./, (char) => char.toUpperCase()),
|
|
343
|
+
footer: (props) => props.column.id,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
initialColumns.push(columnConfig);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const columns = React.useMemo<ColumnDef<I>[]>(() => initialColumns, [properties]);
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<ReactTable
|
|
354
|
+
data={instances}
|
|
355
|
+
columns={columns}
|
|
356
|
+
toggleSelectAll={toggleSelectAll}
|
|
357
|
+
isRowSelected={isRowSelected}
|
|
358
|
+
selectedUris={selectedUris}
|
|
359
|
+
toggleRowSelection={toggleRowSelection}
|
|
360
|
+
clearSelection={clearSelection}
|
|
361
|
+
removeFromSelection={removeFromSelection}
|
|
362
|
+
hasSelection={hasSelection}
|
|
363
|
+
config={config || _config}
|
|
364
|
+
setConfig={__setConfig}
|
|
365
|
+
totalCount={totalCount}
|
|
366
|
+
shape={shape}
|
|
367
|
+
properties={properties}
|
|
368
|
+
isLoading={isLoading}
|
|
369
|
+
isPickMode={isPickMode}
|
|
370
|
+
onPickInstance={handlePickInstance}
|
|
371
|
+
onPickInstances={handlePickInstances}
|
|
372
|
+
pickMaxCount={pickMaxCount}
|
|
373
|
+
pickAlreadySelected={pickAlreadySelected}
|
|
374
|
+
deletionProjectId={deletionProjectId}
|
|
375
|
+
previewDeletion={previewDeletion}
|
|
376
|
+
executeDeletion={executeDeletion}
|
|
377
|
+
onInstancesDeleted={onInstancesDeleted}
|
|
378
|
+
onBatchUpdate={onBatchUpdate}
|
|
379
|
+
tableMode={tableMode}
|
|
380
|
+
onTableModeChange={onTableModeChange}
|
|
381
|
+
/>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export default InstanceOverview;
|