@_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,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Lifted from ViewInstance.module.css alongside the InstanceView organism.
|
|
3
|
+
*
|
|
4
|
+
* Only the rules that render an instance's VALUES. The page keeps its layout, header,
|
|
5
|
+
* actions and skeleton rules — those belong to the page, not to the value list.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.rows {
|
|
9
|
+
padding: var(--surface-gap) var(--space-2xl) var(--space-lg) var(--space-2xl);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.form {
|
|
13
|
+
display: flex;
|
|
14
|
+
min-height: 3.75rem;
|
|
15
|
+
align-items: center;
|
|
16
|
+
border-top: 1px solid var(--table-row-border);
|
|
17
|
+
justify-content: flex-start;
|
|
18
|
+
padding: var(--space-sm) 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.form:first-child {
|
|
22
|
+
border-top: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.form.stacked {
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
align-items: flex-start;
|
|
28
|
+
gap: var(--space-sm);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.form.stacked .label {
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.form.stacked .value {
|
|
36
|
+
width: 100%;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.label {
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
width: 30%;
|
|
43
|
+
min-width: 120px;
|
|
44
|
+
height: 100%;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
padding-right: var(--space-md);
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.label label {
|
|
51
|
+
color: var(--instance-view-gray-500, var(--color-gray-500));
|
|
52
|
+
font-size: var(--typography-font-size);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.value {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-wrap: wrap;
|
|
58
|
+
gap: var(--space-sm-plus);
|
|
59
|
+
width: 70%;
|
|
60
|
+
max-height: 8.75rem;
|
|
61
|
+
overflow-y: auto;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.value p {
|
|
65
|
+
color: var(--text-primary);
|
|
66
|
+
font-size: var(--typography-font-size);
|
|
67
|
+
margin: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.badgeContainer {
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
gap: var(--space-2xs) 0;
|
|
74
|
+
overflow-y: auto;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.expandedContainer {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-wrap: wrap;
|
|
80
|
+
gap: var(--space-sm);
|
|
81
|
+
overflow-y: auto;
|
|
82
|
+
margin-top: var(--space-sm);
|
|
83
|
+
padding: var(--space-sm);
|
|
84
|
+
border: 1px solid var(--surface-border);
|
|
85
|
+
border-radius: var(--radius-field);
|
|
86
|
+
background: var(--section-bg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.imageValue {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: var(--space-sm);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.imageLabel {
|
|
96
|
+
color: var(--instance-view-gray-600, var(--color-gray-600));
|
|
97
|
+
font-size: var(--typography-small-size);
|
|
98
|
+
word-break: break-all;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
[data-theme='dark'] .imageLabel {
|
|
102
|
+
color: var(--instance-view-gray-400, var(--color-gray-400));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
[data-theme='dark'] .label label {
|
|
106
|
+
color: var(--instance-view-gray-400, var(--color-gray-400));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.rows {
|
|
110
|
+
padding: var(--space-sm) var(--space-md);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.form {
|
|
114
|
+
padding: var(--space-md) 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.label {
|
|
118
|
+
width: 35%;
|
|
119
|
+
min-width: 100px;
|
|
120
|
+
padding-right: var(--space-sm);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.value {
|
|
124
|
+
width: 65%;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.rows {
|
|
128
|
+
padding: var(--space-xs) var(--space-sm);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.form {
|
|
132
|
+
flex-direction: column;
|
|
133
|
+
align-items: flex-start;
|
|
134
|
+
gap: var(--space-2xs);
|
|
135
|
+
padding: var(--space-sm) 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.label {
|
|
139
|
+
width: 100%;
|
|
140
|
+
min-width: unset;
|
|
141
|
+
padding-right: 0;
|
|
142
|
+
margin-bottom: 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.label label {
|
|
146
|
+
font-size: var(--typography-small-size);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.value {
|
|
150
|
+
width: 100%;
|
|
151
|
+
max-height: none;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.value p {
|
|
155
|
+
font-size: var(--typography-small-size);
|
|
156
|
+
}
|
|
157
|
+
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {useState} from 'react';
|
|
2
|
+
import {motion} from 'framer-motion';
|
|
3
|
+
import {cl} from '@_linked/react/utils/ClassNames';
|
|
4
|
+
import type {
|
|
5
|
+
PropertyShapeWire,
|
|
6
|
+
NodeShapeWire,
|
|
7
|
+
} from '@_linked/core/shapes/nodeShapeWire';
|
|
8
|
+
import {NodeBadge} from './NodeBadge.js';
|
|
9
|
+
import ImageThumb from '@_linked/primitives/components/ImageThumb';
|
|
10
|
+
import {getNodeDisplay} from '../shape/nodeDisplay.js';
|
|
11
|
+
import {
|
|
12
|
+
getTransition,
|
|
13
|
+
staggerContainer,
|
|
14
|
+
staggerItem,
|
|
15
|
+
useReducedMotion,
|
|
16
|
+
} from '@_linked/primitives/motion';
|
|
17
|
+
import style from './InstanceView.module.css';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read-only rendering of one instance's values.
|
|
21
|
+
*
|
|
22
|
+
* Lifted out of `ViewInstance` (436 lines), which rendered all of this inline. That made the
|
|
23
|
+
* single-instance view the one CRUD surface with no organism at all — nothing to extract,
|
|
24
|
+
* nothing to test, and nothing that could move into the package. The page keeps what a page
|
|
25
|
+
* should: routing, breadcrumbs, layout chrome, actions, the deletion dialog.
|
|
26
|
+
*
|
|
27
|
+
* Navigation is a callback, not a router call. Clicking a related node means "show me that
|
|
28
|
+
* one", and where that goes is the host's business.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export interface InstanceViewProps {
|
|
32
|
+
shape?: NodeShapeWire;
|
|
33
|
+
/** Property metadata by label — used to resolve which shape a related node belongs to. */
|
|
34
|
+
properties: Record<string, PropertyShapeWire>;
|
|
35
|
+
/** The instance's values, keyed by property label. */
|
|
36
|
+
subject: Record<string, any> | null;
|
|
37
|
+
/**
|
|
38
|
+
* A related node was clicked. Given the node and the property it came from, so the caller
|
|
39
|
+
* can resolve the target shape from `properties[propertyName].valueShape`.
|
|
40
|
+
*/
|
|
41
|
+
onNodeClick?: (node: {id: string; label?: string}, propertyName: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Above this many values, the rest collapse behind a "(n…)" badge. */
|
|
45
|
+
const MAX_VALUES = 7;
|
|
46
|
+
|
|
47
|
+
export function InstanceView({
|
|
48
|
+
shape,
|
|
49
|
+
properties,
|
|
50
|
+
subject,
|
|
51
|
+
onNodeClick,
|
|
52
|
+
}: InstanceViewProps) {
|
|
53
|
+
const [expandedArrays, setExpandedArrays] = useState<Set<string>>(new Set());
|
|
54
|
+
const reducedMotion = useReducedMotion();
|
|
55
|
+
|
|
56
|
+
const expand = (propertyName: string) =>
|
|
57
|
+
setExpandedArrays((prev) => new Set(prev).add(propertyName));
|
|
58
|
+
|
|
59
|
+
const handleNodeClick = (node: any, propertyName: string) => {
|
|
60
|
+
if (node?.id) onNodeClick?.(node, propertyName);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** An image source, whether the value carries one directly or via a nested ImageObject. */
|
|
64
|
+
const imageSourceOf = (value: any): string | undefined => {
|
|
65
|
+
const images = value?.image;
|
|
66
|
+
const first = Array.isArray(images) ? images[0] : images;
|
|
67
|
+
if (!first) return undefined;
|
|
68
|
+
return typeof first === 'object' && first.contentUrl
|
|
69
|
+
? String(first.contentUrl)
|
|
70
|
+
: String(first);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const renderArrayItem = (item: any, key: number, propertyName: string) => {
|
|
74
|
+
if (typeof item === 'object' && item?.contentUrl) {
|
|
75
|
+
return (
|
|
76
|
+
<div key={key}>
|
|
77
|
+
<ImageThumb src={item.contentUrl} size={40} alt={item.label || propertyName} />
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (typeof item === 'object' && item?.id) {
|
|
82
|
+
const imgSrc = imageSourceOf(item);
|
|
83
|
+
if (imgSrc) {
|
|
84
|
+
return (
|
|
85
|
+
<div key={key}>
|
|
86
|
+
<ImageThumb src={imgSrc} size={40} alt={item.label || propertyName} />
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return (
|
|
91
|
+
<div key={key}>
|
|
92
|
+
<NodeBadge
|
|
93
|
+
text={item.label || item.id}
|
|
94
|
+
onClick={() => handleNodeClick(item, propertyName)}
|
|
95
|
+
clickable
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return (
|
|
101
|
+
<div key={key}>
|
|
102
|
+
<NodeBadge text={String(item)} />
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const renderValue = (value: any, propertyName: string) => {
|
|
108
|
+
if (value === null || value === undefined) return <p>-</p>;
|
|
109
|
+
if (typeof value === 'boolean') return <p>{value ? 'Yes' : 'No'}</p>;
|
|
110
|
+
|
|
111
|
+
// An ImageObject: the value IS the image.
|
|
112
|
+
if (typeof value === 'object' && value.contentUrl) {
|
|
113
|
+
return (
|
|
114
|
+
<div className={style.imageValue}>
|
|
115
|
+
<ImageThumb src={value.contentUrl} size={64} alt={propertyName} />
|
|
116
|
+
<p className={style.imageLabel}>{value.label || value.contentUrl}</p>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// A related node: show its image if it has one, else a clickable badge.
|
|
122
|
+
if (typeof value === 'object' && value.id) {
|
|
123
|
+
const imgSrc = imageSourceOf(value);
|
|
124
|
+
if (imgSrc) {
|
|
125
|
+
return (
|
|
126
|
+
<div className={style.imageValue}>
|
|
127
|
+
<ImageThumb src={imgSrc} size={48} alt={value.label || propertyName} />
|
|
128
|
+
{value.label && <span className={style.imageLabel}>{value.label}</span>}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return (
|
|
133
|
+
<NodeBadge
|
|
134
|
+
text={getNodeDisplay(value)}
|
|
135
|
+
onClick={() => handleNodeClick(value, propertyName)}
|
|
136
|
+
clickable
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
if (value.length === 0) return <p>-</p>;
|
|
143
|
+
const isExpanded = expandedArrays.has(propertyName);
|
|
144
|
+
const visible = isExpanded ? value : value.slice(0, MAX_VALUES);
|
|
145
|
+
const leftOver = value.length - MAX_VALUES;
|
|
146
|
+
return (
|
|
147
|
+
<div className={style.badgeContainer}>
|
|
148
|
+
{visible.map((item, index) => renderArrayItem(item, index, propertyName))}
|
|
149
|
+
{!isExpanded && leftOver > 0 && (
|
|
150
|
+
<div>
|
|
151
|
+
<NodeBadge
|
|
152
|
+
text={`(${leftOver}...)`}
|
|
153
|
+
onClick={() => expand(propertyName)}
|
|
154
|
+
clickable
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return <p>{String(value)}</p>;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (!subject) return null;
|
|
166
|
+
|
|
167
|
+
// Only properties that actually have a value, and never the identifier — it names the
|
|
168
|
+
// record rather than describing it.
|
|
169
|
+
const populated = (shape?.propertyShapes ?? []).filter((prop) => {
|
|
170
|
+
if (prop.label === 'id') return false;
|
|
171
|
+
const value = subject[prop.label];
|
|
172
|
+
return value !== undefined && value !== null && value !== '';
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<motion.div
|
|
177
|
+
className={style.rows}
|
|
178
|
+
variants={staggerContainer}
|
|
179
|
+
initial="initial"
|
|
180
|
+
animate="animate"
|
|
181
|
+
>
|
|
182
|
+
{populated.map((prop) => {
|
|
183
|
+
const value = subject[prop.label];
|
|
184
|
+
const formattedLabel =
|
|
185
|
+
prop.label.charAt(0).toUpperCase() + prop.label.slice(1);
|
|
186
|
+
const isStacked = shouldStack(value);
|
|
187
|
+
return (
|
|
188
|
+
<motion.div
|
|
189
|
+
key={prop.label}
|
|
190
|
+
className={cl(style.form, isStacked && style.stacked)}
|
|
191
|
+
variants={staggerItem}
|
|
192
|
+
transition={getTransition(reducedMotion)}
|
|
193
|
+
>
|
|
194
|
+
<div className={style.label}>
|
|
195
|
+
<label>{formattedLabel}</label>
|
|
196
|
+
</div>
|
|
197
|
+
<div className={style.value}>{renderValue(value, prop.label)}</div>
|
|
198
|
+
</motion.div>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</motion.div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A value wide enough that label-beside-value stops reading well. */
|
|
206
|
+
function shouldStack(value: unknown): boolean {
|
|
207
|
+
if (Array.isArray(value)) return value.length > 3;
|
|
208
|
+
if (typeof value === 'string') return value.length > 120;
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default InstanceView;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {Badge} from '@_linked/primitives/components/Badge';
|
|
3
|
+
|
|
4
|
+
export interface NodeBadgeProps {
|
|
5
|
+
/** What identifies the node — a label where it has one, otherwise its IRI. */
|
|
6
|
+
text: string;
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
/** Whether the badge acts on a click. A read-only host may not navigate. */
|
|
9
|
+
clickable?: boolean;
|
|
10
|
+
title?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A chip standing for one node.
|
|
16
|
+
*
|
|
17
|
+
* The only badge this package needs, and it is a thin composition over the presentational
|
|
18
|
+
* primitive. It exists as its own component rather than as a bare `<Badge>` for one reason:
|
|
19
|
+
* "a related instance" is a shape-domain concept, and a table cell, a form field and a
|
|
20
|
+
* read-only view must render it identically. A caller reaching for the primitive directly
|
|
21
|
+
* would have to remember which variant and colour meant "node", and eventually two of them
|
|
22
|
+
* would disagree.
|
|
23
|
+
*
|
|
24
|
+
* `clickable` is separate from `onClick` deliberately. A host that cannot navigate still
|
|
25
|
+
* passes a handler in some code paths, and a chip that looks interactive and does nothing is
|
|
26
|
+
* worse than one that looks inert.
|
|
27
|
+
*/
|
|
28
|
+
export function NodeBadge({
|
|
29
|
+
text,
|
|
30
|
+
onClick,
|
|
31
|
+
clickable = false,
|
|
32
|
+
title,
|
|
33
|
+
className,
|
|
34
|
+
}: NodeBadgeProps) {
|
|
35
|
+
const interactive = clickable && !!onClick;
|
|
36
|
+
|
|
37
|
+
if (!interactive) {
|
|
38
|
+
return (
|
|
39
|
+
<Badge variant="soft" color="neutral" title={title} className={className}>
|
|
40
|
+
{text}
|
|
41
|
+
</Badge>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Badge variant="soft" color="neutral" title={title} className={className} asChild>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={(event) => {
|
|
50
|
+
// A node chip frequently sits inside a clickable row. Without this, choosing the
|
|
51
|
+
// chip also triggers the row.
|
|
52
|
+
event.stopPropagation();
|
|
53
|
+
onClick?.();
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
{text}
|
|
57
|
+
</button>
|
|
58
|
+
</Badge>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PropertyShapeWire, NodeShapeWire } from '@_linked/core/shapes/nodeShapeWire';
|
|
2
|
+
import CustomMultiSelect from './CustomMultiSelect.js';
|
|
3
|
+
import style from './NodeValuesEditor.module.css';
|
|
4
|
+
|
|
5
|
+
//TODO: create new NodeValuesEditor component, which takes property & of props
|
|
6
|
+
//TODO: NodeValuesEditor will look at the property shape and determine potential Values by itself
|
|
7
|
+
//TODO: it also checks if the property has `inList` and then uses inList.getContents() as potential values
|
|
8
|
+
//TODO: it also updates the value by using of[property.label]
|
|
9
|
+
// it renders the MultiSelect component and provides value & multiselect props
|
|
10
|
+
// it also provides potentialValues to MultiSelect, which will be the instances of property.valueShape.getLocalInstances()
|
|
11
|
+
// it renders <MultiSelect />
|
|
12
|
+
interface NodeValuesEditorProps {
|
|
13
|
+
property: PropertyShapeWire;
|
|
14
|
+
of;
|
|
15
|
+
/**
|
|
16
|
+
* The shape being edited. Passed down rather than read from the route: the picker needs
|
|
17
|
+
* it to say which shape the answer belongs to, and the form above already knows.
|
|
18
|
+
*/
|
|
19
|
+
sourceShape?: NodeShapeWire;
|
|
20
|
+
onBeforeNavigate?: () => Promise<string | void> | string | void;
|
|
21
|
+
narrowedIds?: string[] | null;
|
|
22
|
+
isNarrowing?: boolean;
|
|
23
|
+
}
|
|
24
|
+
const NodeValuesEditor = ({ property, of, sourceShape, onBeforeNavigate, narrowedIds, isNarrowing }: NodeValuesEditorProps) => {
|
|
25
|
+
let valueShape = property.valueShape;
|
|
26
|
+
// let shapeClass = getShapeClass(of.targetClass);
|
|
27
|
+
|
|
28
|
+
if (!valueShape) {
|
|
29
|
+
// valueShape = Thing;
|
|
30
|
+
} else {
|
|
31
|
+
}
|
|
32
|
+
// let instanceValues = (
|
|
33
|
+
// getShapeClass(valueShape.namedNode) as any
|
|
34
|
+
// ).getLocalInstances();
|
|
35
|
+
|
|
36
|
+
const values = of[property.label];
|
|
37
|
+
// let potentialValues = property.inList;
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={style.Root}>
|
|
42
|
+
<CustomMultiSelect
|
|
43
|
+
values={values}
|
|
44
|
+
property={property}
|
|
45
|
+
sourceShape={sourceShape}
|
|
46
|
+
onBeforeNavigate={onBeforeNavigate}
|
|
47
|
+
narrowedIds={narrowedIds}
|
|
48
|
+
isNarrowing={isNarrowing}
|
|
49
|
+
onChange={(selected) => {
|
|
50
|
+
// Write back to form data object (same mutation pattern as DynamicFormField)
|
|
51
|
+
// Include label so draft restore can display friendly names
|
|
52
|
+
of[property.label] =
|
|
53
|
+
selected.length === 0
|
|
54
|
+
? undefined
|
|
55
|
+
: property.maxCount === 1
|
|
56
|
+
? { id: selected[0].id, label: selected[0].label }
|
|
57
|
+
: selected.map((v) => ({ id: v.id, label: v.label }));
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default NodeValuesEditor;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.Root {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
gap: 1rem;
|
|
6
|
+
|
|
7
|
+
&.columnBased {
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
padding: 10px 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.radioButtonItem {
|
|
13
|
+
display: flex;
|
|
14
|
+
gap: 0.5rem;
|
|
15
|
+
align-items: center;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.radioButton {
|
|
19
|
+
width: 25px;
|
|
20
|
+
min-width: 25px;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import type { PropertyShapeWire } from '@_linked/core/shapes/nodeShapeWire';
|
|
3
|
+
import { Root, Item, RadioGroup } from '@_linked/primitives/components/RadioGroup';
|
|
4
|
+
import styles from './RadioButtonEditor.module.css';
|
|
5
|
+
import { cl } from '@_linked/react/utils/ClassNames';
|
|
6
|
+
|
|
7
|
+
export interface RadioButtonProps {
|
|
8
|
+
onValueChange?: (newValue) => void;
|
|
9
|
+
of?: Record<string, any>;
|
|
10
|
+
property?: PropertyShapeWire;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Converts the value returned by the shape, to the radio input string for the radio input
|
|
15
|
+
* @param shapePropertyValue
|
|
16
|
+
* @returns
|
|
17
|
+
*/
|
|
18
|
+
preConvertValueFn?: (shapePropertyValue: any) => string;
|
|
19
|
+
/**
|
|
20
|
+
* Converts the value of the radio input to a value that can be provided to the set method of the shape
|
|
21
|
+
* @param radioInputValue
|
|
22
|
+
* @returns
|
|
23
|
+
*/
|
|
24
|
+
postConvertValueFn?: (radioInputValue: string) => any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const RadioButtonEditor: React.FC<RadioButtonProps> = ({
|
|
28
|
+
onValueChange,
|
|
29
|
+
of,
|
|
30
|
+
property,
|
|
31
|
+
preConvertValueFn,
|
|
32
|
+
postConvertValueFn,
|
|
33
|
+
children,
|
|
34
|
+
className,
|
|
35
|
+
...props
|
|
36
|
+
}) => {
|
|
37
|
+
const [labelHasLongString, setLabelHasLongString] = useState(false);
|
|
38
|
+
|
|
39
|
+
const onChangeHandler = (newValue) => {
|
|
40
|
+
if (!of) return;
|
|
41
|
+
of[property.label] = newValue;
|
|
42
|
+
|
|
43
|
+
if (onValueChange) {
|
|
44
|
+
onValueChange(of[property.label]);
|
|
45
|
+
// onValueChange(
|
|
46
|
+
// options.map((option) => {
|
|
47
|
+
// console.log(option, 'option');
|
|
48
|
+
// of[property.label] = newValue;
|
|
49
|
+
// }),
|
|
50
|
+
// );
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
//check if radio button label has a long string
|
|
56
|
+
if (property.in) {
|
|
57
|
+
const containsLongString = property.in.some((list) => {
|
|
58
|
+
throw Error('TODO: property.in for RadioButtonEditor');
|
|
59
|
+
// const wordCount = list.value.split(' ').length;
|
|
60
|
+
// return wordCount > 5;
|
|
61
|
+
});
|
|
62
|
+
setLabelHasLongString(containsLongString);
|
|
63
|
+
}
|
|
64
|
+
}, [property.in]);
|
|
65
|
+
|
|
66
|
+
// function cl(radioButtonField: any, arg1: any): string {
|
|
67
|
+
// throw new Error('Function not implemented.');
|
|
68
|
+
// }
|
|
69
|
+
|
|
70
|
+
console.log('of.label', of[property.label]);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<>
|
|
74
|
+
<RadioGroup.Root
|
|
75
|
+
defaultValue="default"
|
|
76
|
+
aria-label="View density"
|
|
77
|
+
className={cl(styles.Root, labelHasLongString && styles.columnBased)}
|
|
78
|
+
>
|
|
79
|
+
{property.in ? (
|
|
80
|
+
property.in.map((item) => (
|
|
81
|
+
<label className={styles.radioButtonItem}>
|
|
82
|
+
<RadioGroup.Item
|
|
83
|
+
value={item.toString()}
|
|
84
|
+
className={styles.radioButton}
|
|
85
|
+
/>
|
|
86
|
+
TODO: item from property.in
|
|
87
|
+
{/* {item.value.charAt(0).toUpperCase() + item.value.slice(1)} */}
|
|
88
|
+
</label>
|
|
89
|
+
))
|
|
90
|
+
) : (
|
|
91
|
+
<p>unimplemented single radio button</p>
|
|
92
|
+
)}
|
|
93
|
+
</RadioGroup.Root>
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
};
|