@_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.
- 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 +37 -0
- package/lib/esm/components/InstanceOverview.js +233 -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 +50 -0
- package/lib/esm/components/ReactTable.js +370 -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 +64 -10
- package/lib/esm/index.js +63 -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 +374 -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 +991 -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 +99 -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/lib/esm/package.d.ts +0 -3
- package/lib/esm/package.js +0 -4
- package/lib/esm/package.js.map +0 -1
- package/tsconfig-cjs.json +0 -8
- package/tsconfig-esm.json +0 -9
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { PropertyShapeWire } from '@_linked/core/shapes/nodeShapeWire';
|
|
2
|
+
import { shacl } from '@_linked/core/ontologies/shacl';
|
|
3
|
+
import { xsd } from '@_linked/core/ontologies/xsd';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Classifies a SHACL property shape into a UI-level type.
|
|
7
|
+
* Used by form fields (DynamicFormField) and table filter widgets
|
|
8
|
+
* to determine the appropriate input control and filter operators.
|
|
9
|
+
*/
|
|
10
|
+
export type PropertyUiType =
|
|
11
|
+
| 'text'
|
|
12
|
+
| 'number'
|
|
13
|
+
| 'date'
|
|
14
|
+
| 'datetime'
|
|
15
|
+
| 'boolean'
|
|
16
|
+
| 'url'
|
|
17
|
+
| 'iri-single'
|
|
18
|
+
| 'iri-multi'
|
|
19
|
+
| 'enum-literal'
|
|
20
|
+
| 'enum-iri';
|
|
21
|
+
|
|
22
|
+
export function getPropertyUiType(property: PropertyShapeWire): PropertyUiType | null {
|
|
23
|
+
const nodeKind = property.nodeKind;
|
|
24
|
+
if (!nodeKind) return null;
|
|
25
|
+
|
|
26
|
+
// Enum: sh:in with values present.
|
|
27
|
+
//
|
|
28
|
+
// `sh:in`, the metamodel spelling. The DTO's `inValues` is still read for a host that
|
|
29
|
+
// hands one over, but nothing in this package produces it any more.
|
|
30
|
+
// (was: accepts BOTH forms. The DTO calls it `inValues` — `{id, label}` pairs with a derived
|
|
31
|
+
// display label); the metamodel calls it `in` and keeps members as members. The table
|
|
32
|
+
// now receives the metamodel form, and keying only on `inValues` silently downgraded
|
|
33
|
+
// every enum column to a free-text filter and a free-text batch-edit input.
|
|
34
|
+
const enumValues =
|
|
35
|
+
((property as {inValues?: unknown[]}).inValues) ??
|
|
36
|
+
((property as unknown as {in?: unknown[]}).in);
|
|
37
|
+
if (enumValues && enumValues.length > 0) {
|
|
38
|
+
if (nodeKind.id === shacl.Literal.id) return 'enum-literal';
|
|
39
|
+
return 'enum-iri';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Object / IRI properties
|
|
43
|
+
if (
|
|
44
|
+
nodeKind.id === shacl.IRI.id ||
|
|
45
|
+
nodeKind.id === shacl.BlankNode.id ||
|
|
46
|
+
nodeKind.id === shacl.BlankNodeOrIRI.id
|
|
47
|
+
) {
|
|
48
|
+
return property.maxCount === 1 ? 'iri-single' : 'iri-multi';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Literal properties — classify by datatype
|
|
52
|
+
if (nodeKind.id === shacl.Literal.id) {
|
|
53
|
+
const dt = property.datatype?.id;
|
|
54
|
+
if (!dt) return 'text';
|
|
55
|
+
|
|
56
|
+
if (dt === xsd.boolean.id) return 'boolean';
|
|
57
|
+
|
|
58
|
+
if (dt === xsd.date.id) return 'date';
|
|
59
|
+
|
|
60
|
+
if (dt === xsd.dateTime.id) return 'datetime';
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
dt === xsd.integer.id ||
|
|
64
|
+
dt === xsd.long?.id ||
|
|
65
|
+
dt === xsd.decimal?.id ||
|
|
66
|
+
dt === xsd.float?.id ||
|
|
67
|
+
dt === xsd.double?.id
|
|
68
|
+
) {
|
|
69
|
+
return 'number';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (dt === 'http://www.w3.org/2001/XMLSchema#anyURI') return 'url';
|
|
73
|
+
|
|
74
|
+
return 'text';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return 'text';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns whether a property UI type represents an integer (vs floating point).
|
|
82
|
+
*/
|
|
83
|
+
export function isIntegerType(property: PropertyShapeWire): boolean {
|
|
84
|
+
const dt = property.datatype?.id;
|
|
85
|
+
return dt === xsd.integer.id || dt === xsd.long?.id;
|
|
86
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { PropertyShapeWire } from '@_linked/core/shapes/nodeShapeWire';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `hidden` is a REMOVAL, not the weakest shown category.
|
|
5
|
+
*
|
|
6
|
+
* `optional` is still displayed — it fills the "add another field" picker and is rendered
|
|
7
|
+
* in `full` and in `form-edit` whenever the instance has a value. So mapping
|
|
8
|
+
* `linked_core:displayHidden` onto `optional` did nothing at all. Hidden needs its own
|
|
9
|
+
* value that every mode filters, including the "nothing qualified, show everything"
|
|
10
|
+
* fallback.
|
|
11
|
+
*/
|
|
12
|
+
export type PropertyVisibility = 'required' | 'used' | 'optional' | 'hidden';
|
|
13
|
+
|
|
14
|
+
export interface PropertyUsageData {
|
|
15
|
+
count: number;
|
|
16
|
+
total: number;
|
|
17
|
+
percentage: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Code analysis classification from analyze-shape-usage.ts */
|
|
21
|
+
export type CodeClassification =
|
|
22
|
+
| 'essential'
|
|
23
|
+
| 'common'
|
|
24
|
+
| 'contextual'
|
|
25
|
+
| 'display'
|
|
26
|
+
| 'unused';
|
|
27
|
+
|
|
28
|
+
export interface CodeUsageData {
|
|
29
|
+
classification: CodeClassification;
|
|
30
|
+
writeRefs: number;
|
|
31
|
+
readRefs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Per-shape code usage manifest (keyed by shape class name → property label) */
|
|
35
|
+
export type CodeUsageManifest = Record<string, Record<string, CodeUsageData>>;
|
|
36
|
+
|
|
37
|
+
export interface ClassifiedProperty {
|
|
38
|
+
property: PropertyShapeWire;
|
|
39
|
+
visibility: PropertyVisibility;
|
|
40
|
+
usage?: PropertyUsageData;
|
|
41
|
+
codeUsage?: CodeUsageData;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Classify properties using three signals (in priority order):
|
|
46
|
+
*
|
|
47
|
+
* 1. Shape constraints (SHACL): minCount > 0 → required
|
|
48
|
+
* 2. Code analysis: essential/common → used; contextual/display/unused → optional
|
|
49
|
+
* 3. Data usage (SPARQL): percentage >= threshold → used
|
|
50
|
+
*
|
|
51
|
+
* The code analysis signal is the primary differentiator for non-required
|
|
52
|
+
* properties — it reflects how the shape is actually used when submitting
|
|
53
|
+
* data in the project's code.
|
|
54
|
+
*/
|
|
55
|
+
export function classifyProperties(
|
|
56
|
+
properties: PropertyShapeWire[],
|
|
57
|
+
usageData: Record<string, PropertyUsageData> | null,
|
|
58
|
+
options?: {
|
|
59
|
+
usageThreshold?: number;
|
|
60
|
+
codeUsage?: Record<string, CodeUsageData> | null;
|
|
61
|
+
}
|
|
62
|
+
): ClassifiedProperty[] {
|
|
63
|
+
const threshold = options?.usageThreshold ?? 1;
|
|
64
|
+
const codeUsage = options?.codeUsage ?? null;
|
|
65
|
+
|
|
66
|
+
return properties.map((prop) => {
|
|
67
|
+
const usage = usageData?.[prop.label];
|
|
68
|
+
const code = codeUsage?.[prop.label];
|
|
69
|
+
// Display metadata the shape itself declares. Read structurally so this works for
|
|
70
|
+
// both the metamodel and the derived DTO, which has no home for these fields.
|
|
71
|
+
const display = prop as unknown as {
|
|
72
|
+
displayHidden?: boolean;
|
|
73
|
+
displayRank?: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
let visibility: PropertyVisibility;
|
|
77
|
+
|
|
78
|
+
// 0. The shape says not to render it. This outranks everything below, including
|
|
79
|
+
// minCount: a required property can still be one a person should not be shown.
|
|
80
|
+
if (display.displayHidden === true) {
|
|
81
|
+
visibility = 'hidden';
|
|
82
|
+
}
|
|
83
|
+
// 1. SHACL constraint: minCount > 0 means required
|
|
84
|
+
else if (prop.minCount > 0) {
|
|
85
|
+
visibility = 'required';
|
|
86
|
+
}
|
|
87
|
+
// 1b. A declared display rank is the shape stating the property matters. It is a
|
|
88
|
+
// first-party statement, so it outranks the inferred signals below.
|
|
89
|
+
else if (display.displayRank != null) {
|
|
90
|
+
visibility = 'used';
|
|
91
|
+
}
|
|
92
|
+
// 2. Code analysis: essential/common properties are "used"
|
|
93
|
+
else if (
|
|
94
|
+
code &&
|
|
95
|
+
(code.classification === 'essential' || code.classification === 'common')
|
|
96
|
+
) {
|
|
97
|
+
visibility = 'used';
|
|
98
|
+
}
|
|
99
|
+
// 3. Data usage: >= threshold% of instances have this property
|
|
100
|
+
else if (usage && usage.percentage >= threshold) {
|
|
101
|
+
visibility = 'used';
|
|
102
|
+
}
|
|
103
|
+
// 4. Code analysis: display properties are optional but may be shown in views
|
|
104
|
+
else {
|
|
105
|
+
visibility = 'optional';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { property: prop, visibility, usage, codeUsage: code };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type VisibilityMode = 'auto' | 'full' | 'form-add' | 'form-edit';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns true if a property value is considered "populated" (has meaningful data).
|
|
116
|
+
* Handles all data types: primitives, objects ({id, label}), arrays, numbers.
|
|
117
|
+
*/
|
|
118
|
+
function hasValue(val: any): boolean {
|
|
119
|
+
if (val === undefined || val === null) return false;
|
|
120
|
+
if (typeof val === 'string') return val !== '';
|
|
121
|
+
if (typeof val === 'number') return true; // 0 is a valid value
|
|
122
|
+
if (typeof val === 'boolean') return true;
|
|
123
|
+
if (Array.isArray(val)) return val.length > 0;
|
|
124
|
+
if (typeof val === 'object') return Object.keys(val).length > 0;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Filter properties to only those that should be visible in a given context.
|
|
130
|
+
*
|
|
131
|
+
* - auto: required + used (for tables); falls back to all if nothing qualifies
|
|
132
|
+
* - full: all properties
|
|
133
|
+
* - form-add: required + used (essential/common from code); falls back to all
|
|
134
|
+
* - form-edit: required + used + properties with data on this instance; falls back to all
|
|
135
|
+
*/
|
|
136
|
+
export function getVisibleProperties(
|
|
137
|
+
classified: ClassifiedProperty[],
|
|
138
|
+
mode: VisibilityMode,
|
|
139
|
+
instanceData?: Record<string, any>
|
|
140
|
+
): ClassifiedProperty[] {
|
|
141
|
+
// Removed before any mode gets a say, and before the fallback below can reinstate it.
|
|
142
|
+
const visible = classified.filter((c) => c.visibility !== 'hidden');
|
|
143
|
+
|
|
144
|
+
let result: ClassifiedProperty[];
|
|
145
|
+
|
|
146
|
+
switch (mode) {
|
|
147
|
+
case 'full':
|
|
148
|
+
return visible;
|
|
149
|
+
|
|
150
|
+
case 'auto':
|
|
151
|
+
result = visible.filter(
|
|
152
|
+
(c) => c.visibility === 'required' || c.visibility === 'used'
|
|
153
|
+
);
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case 'form-add':
|
|
157
|
+
result = visible.filter(
|
|
158
|
+
(c) => c.visibility === 'required' || c.visibility === 'used'
|
|
159
|
+
);
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case 'form-edit':
|
|
163
|
+
result = visible.filter((c) => {
|
|
164
|
+
if (c.visibility === 'required') return true;
|
|
165
|
+
if (c.visibility === 'used') return true;
|
|
166
|
+
if (instanceData && hasValue(instanceData[c.property.label]))
|
|
167
|
+
return true;
|
|
168
|
+
return false;
|
|
169
|
+
});
|
|
170
|
+
break;
|
|
171
|
+
|
|
172
|
+
default:
|
|
173
|
+
return visible;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Fallback: if filtering yields nothing, show all NON-HIDDEN properties. This handles
|
|
177
|
+
// shapes that haven't declared any minCount/required yet — but it must not be a back
|
|
178
|
+
// door that reinstates what the shape explicitly hid.
|
|
179
|
+
return result.length > 0 ? result : visible;
|
|
180
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { NodeShapeWire } from '@_linked/core/shapes/nodeShapeWire';
|
|
2
|
+
import { shacl } from '@_linked/core/ontologies/shacl';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A dependency link: a child property depends on a parent property.
|
|
6
|
+
* When the parent's value changes, the child should be narrowed or reset.
|
|
7
|
+
*/
|
|
8
|
+
export interface PropertyDependency {
|
|
9
|
+
/** The parent property label (e.g., "agent") */
|
|
10
|
+
parentLabel: string;
|
|
11
|
+
/** The dependent property label (e.g., "team") */
|
|
12
|
+
childLabel: string;
|
|
13
|
+
/** The shape URI of the parent's valueShape (e.g., Player shape URI) */
|
|
14
|
+
parentValueShapeId: string;
|
|
15
|
+
/** The label of the property on the parent's valueShape that links to the child's valueShape
|
|
16
|
+
* (e.g., "currentTeam" on the Player shape) */
|
|
17
|
+
linkingPropertyLabel: string;
|
|
18
|
+
/** The full predicate URI of the linking property — needed for SPARQL queries */
|
|
19
|
+
linkingPropertyPath: string;
|
|
20
|
+
/** The shape URI of the child's valueShape (e.g., Team shape URI) */
|
|
21
|
+
childValueShapeId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const IRI_NODE_KINDS = new Set([
|
|
25
|
+
shacl.IRI.id,
|
|
26
|
+
shacl.BlankNode.id,
|
|
27
|
+
shacl.BlankNodeOrIRI.id,
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function isIriProperty(p: NodeShapeWire['propertyShapes'][number]): boolean {
|
|
31
|
+
return !!p.nodeKind && IRI_NODE_KINDS.has(p.nodeKind.id) && !!p.valueShape?.id;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compute dependency relationships between IRI properties on a shape.
|
|
36
|
+
*
|
|
37
|
+
* Algorithm: For each pair of IRI properties (A, B) on the same shape,
|
|
38
|
+
* check if A's valueShape has a property whose valueShape matches B's valueShape.
|
|
39
|
+
* If so, B depends on A via that linking property.
|
|
40
|
+
*
|
|
41
|
+
* Only looks 1 level deep and breaks cycles: if A→B and B→A are both found,
|
|
42
|
+
* only the first-discovered direction is kept.
|
|
43
|
+
*/
|
|
44
|
+
export function computeDependencies(
|
|
45
|
+
shape: NodeShapeWire,
|
|
46
|
+
allShapes: Record<string, NodeShapeWire>
|
|
47
|
+
): PropertyDependency[] {
|
|
48
|
+
const candidates: PropertyDependency[] = [];
|
|
49
|
+
|
|
50
|
+
const iriProperties = (shape.propertyShapes || []).filter(isIriProperty);
|
|
51
|
+
|
|
52
|
+
for (const propA of iriProperties) {
|
|
53
|
+
// Look up the full shape definition for A's valueShape
|
|
54
|
+
const aValueShape = allShapes[propA.valueShape!.id];
|
|
55
|
+
if (!aValueShape?.propertyShapes) continue;
|
|
56
|
+
|
|
57
|
+
for (const propB of iriProperties) {
|
|
58
|
+
if (propA.label === propB.label) continue;
|
|
59
|
+
if (!propB.valueShape?.id) continue;
|
|
60
|
+
|
|
61
|
+
// Check if A's valueShape has a property pointing to B's valueShape
|
|
62
|
+
for (const linkProp of aValueShape.propertyShapes) {
|
|
63
|
+
if (linkProp.valueShape?.id === propB.valueShape!.id) {
|
|
64
|
+
const pathObj = Array.isArray(linkProp.path)
|
|
65
|
+
? linkProp.path[0]
|
|
66
|
+
: linkProp.path;
|
|
67
|
+
candidates.push({
|
|
68
|
+
parentLabel: propA.label,
|
|
69
|
+
childLabel: propB.label,
|
|
70
|
+
parentValueShapeId: propA.valueShape!.id,
|
|
71
|
+
linkingPropertyLabel: linkProp.label,
|
|
72
|
+
linkingPropertyPath: pathObj?.id || '',
|
|
73
|
+
childValueShapeId: propB.valueShape!.id,
|
|
74
|
+
});
|
|
75
|
+
break; // Only one linking path per A→B pair
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Break cycles: if A→B and B→A both exist, keep only the first one found.
|
|
82
|
+
// Also ensure no field ends up as a child of ALL other fields (deadlock).
|
|
83
|
+
return removeCycles(candidates);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Remove circular dependencies from the candidate list.
|
|
88
|
+
* When A→B and B→A both exist, keep only the first direction found.
|
|
89
|
+
* Then ensure no field is blocked by all other fields (prune so at least
|
|
90
|
+
* one field per connected component has no parents).
|
|
91
|
+
*/
|
|
92
|
+
function removeCycles(deps: PropertyDependency[]): PropertyDependency[] {
|
|
93
|
+
const kept: PropertyDependency[] = [];
|
|
94
|
+
const edges = new Set<string>(); // "parent->child" strings
|
|
95
|
+
|
|
96
|
+
for (const dep of deps) {
|
|
97
|
+
const forward = `${dep.parentLabel}->${dep.childLabel}`;
|
|
98
|
+
const reverse = `${dep.childLabel}->${dep.parentLabel}`;
|
|
99
|
+
|
|
100
|
+
// If the reverse direction is already kept, skip this one
|
|
101
|
+
if (edges.has(reverse)) continue;
|
|
102
|
+
|
|
103
|
+
edges.add(forward);
|
|
104
|
+
kept.push(dep);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Ensure at least one root (a field with no parents) exists.
|
|
108
|
+
// If every field is someone's child, remove the dependency that has the
|
|
109
|
+
// most children as parent (it's the most "primary" field).
|
|
110
|
+
const allChildren = new Set(kept.map((d) => d.childLabel));
|
|
111
|
+
const allParents = new Set(kept.map((d) => d.parentLabel));
|
|
112
|
+
const roots = [...allParents].filter((p) => !allChildren.has(p));
|
|
113
|
+
|
|
114
|
+
if (roots.length === 0 && kept.length > 0) {
|
|
115
|
+
// No root — every field is a child. Break the cycle by finding the
|
|
116
|
+
// field that is a child the fewest times and removing its parent deps.
|
|
117
|
+
const childCount = new Map<string, number>();
|
|
118
|
+
for (const d of kept) {
|
|
119
|
+
childCount.set(d.childLabel, (childCount.get(d.childLabel) || 0) + 1);
|
|
120
|
+
}
|
|
121
|
+
// Pick the field that appears as parent most often to be the root
|
|
122
|
+
const parentCount = new Map<string, number>();
|
|
123
|
+
for (const d of kept) {
|
|
124
|
+
parentCount.set(d.parentLabel, (parentCount.get(d.parentLabel) || 0) + 1);
|
|
125
|
+
}
|
|
126
|
+
let bestRoot = '';
|
|
127
|
+
let bestCount = -1;
|
|
128
|
+
for (const [label, count] of parentCount) {
|
|
129
|
+
if (count > bestCount) {
|
|
130
|
+
bestCount = count;
|
|
131
|
+
bestRoot = label;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Remove all deps where bestRoot is a child
|
|
135
|
+
return kept.filter((d) => d.childLabel !== bestRoot);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return kept;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build a lookup map: childLabel → PropertyDependency[]
|
|
143
|
+
* A child can depend on multiple parents (rare but possible).
|
|
144
|
+
*/
|
|
145
|
+
export function buildDependencyMap(
|
|
146
|
+
deps: PropertyDependency[]
|
|
147
|
+
): Map<string, PropertyDependency[]> {
|
|
148
|
+
const map = new Map<string, PropertyDependency[]>();
|
|
149
|
+
for (const dep of deps) {
|
|
150
|
+
const existing = map.get(dep.childLabel) || [];
|
|
151
|
+
existing.push(dep);
|
|
152
|
+
map.set(dep.childLabel, existing);
|
|
153
|
+
}
|
|
154
|
+
return map;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Extract the IDs that a child field should be narrowed to,
|
|
159
|
+
* based on the parent instance data and the linking property.
|
|
160
|
+
*/
|
|
161
|
+
export function extractNarrowingIds(
|
|
162
|
+
instanceData: Record<string, any>,
|
|
163
|
+
linkingPropertyLabel: string
|
|
164
|
+
): string[] {
|
|
165
|
+
const linkedValue = instanceData[linkingPropertyLabel];
|
|
166
|
+
if (!linkedValue) return [];
|
|
167
|
+
|
|
168
|
+
if (Array.isArray(linkedValue)) {
|
|
169
|
+
return linkedValue
|
|
170
|
+
.map((v) => (typeof v === 'object' ? v.id : v))
|
|
171
|
+
.filter(Boolean);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (typeof linkedValue === 'object' && linkedValue.id) {
|
|
175
|
+
return [linkedValue.id];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (typeof linkedValue === 'string') {
|
|
179
|
+
return [linkedValue];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import type { PropertyShapeWire } from '@_linked/core/shapes/nodeShapeWire';
|
|
2
|
+
import { humanizeEnumLabel } from './nodeDisplay.js';
|
|
3
|
+
import { getPropertyUiType, type PropertyUiType } from './propertyTypes.js';
|
|
4
|
+
|
|
5
|
+
// ─── Filter operators ────────────────────────────────────────────────
|
|
6
|
+
export type FilterOperator =
|
|
7
|
+
| 'contains'
|
|
8
|
+
| 'equals'
|
|
9
|
+
| 'startsWith'
|
|
10
|
+
| 'endsWith'
|
|
11
|
+
| 'gt'
|
|
12
|
+
| 'lt'
|
|
13
|
+
| 'gte'
|
|
14
|
+
| 'lte'
|
|
15
|
+
| 'before'
|
|
16
|
+
| 'after'
|
|
17
|
+
| 'is'
|
|
18
|
+
| 'isNot'
|
|
19
|
+
| 'in'; // multi-select enum
|
|
20
|
+
|
|
21
|
+
export interface FilterOperatorOption {
|
|
22
|
+
value: FilterOperator;
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Internal filter model (decoupled from TanStack) ─────────────────
|
|
27
|
+
export interface TableFilter {
|
|
28
|
+
id: string; // unique filter ID
|
|
29
|
+
columnId: string;
|
|
30
|
+
columnLabel: string;
|
|
31
|
+
operator: FilterOperator;
|
|
32
|
+
/** Single value or array of values for multi-select (enum OR) */
|
|
33
|
+
value: string | string[];
|
|
34
|
+
isActive: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Operator sets by UI type ────────────────────────────────────────
|
|
38
|
+
const STRING_OPERATORS: FilterOperatorOption[] = [
|
|
39
|
+
{ value: 'contains', label: 'contains' },
|
|
40
|
+
{ value: 'equals', label: 'equals' },
|
|
41
|
+
{ value: 'startsWith', label: 'starts with' },
|
|
42
|
+
{ value: 'endsWith', label: 'ends with' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const NUMBER_OPERATORS: FilterOperatorOption[] = [
|
|
46
|
+
{ value: 'equals', label: '=' },
|
|
47
|
+
{ value: 'gt', label: '>' },
|
|
48
|
+
{ value: 'lt', label: '<' },
|
|
49
|
+
{ value: 'gte', label: '>=' },
|
|
50
|
+
{ value: 'lte', label: '<=' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const DATE_OPERATORS: FilterOperatorOption[] = [
|
|
54
|
+
{ value: 'equals', label: 'equals' },
|
|
55
|
+
{ value: 'before', label: 'before' },
|
|
56
|
+
{ value: 'after', label: 'after' },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const BOOLEAN_OPERATORS: FilterOperatorOption[] = [
|
|
60
|
+
{ value: 'is', label: 'is' },
|
|
61
|
+
{ value: 'isNot', label: 'is not' },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const IRI_OPERATORS: FilterOperatorOption[] = [
|
|
65
|
+
{ value: 'contains', label: 'contains' },
|
|
66
|
+
{ value: 'equals', label: 'equals' },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const ENUM_OPERATORS: FilterOperatorOption[] = [
|
|
70
|
+
{ value: 'in', label: 'is one of' },
|
|
71
|
+
{ value: 'equals', label: 'equals' },
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns the available filter operators for a property based on its SHACL type.
|
|
76
|
+
*/
|
|
77
|
+
export function getOperatorsForProperty(property: PropertyShapeWire): FilterOperatorOption[] {
|
|
78
|
+
const uiType = getPropertyUiType(property);
|
|
79
|
+
return getOperatorsForUiType(uiType);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getOperatorsForUiType(uiType: PropertyUiType | null): FilterOperatorOption[] {
|
|
83
|
+
switch (uiType) {
|
|
84
|
+
case 'number':
|
|
85
|
+
return NUMBER_OPERATORS;
|
|
86
|
+
case 'date':
|
|
87
|
+
case 'datetime':
|
|
88
|
+
return DATE_OPERATORS;
|
|
89
|
+
case 'boolean':
|
|
90
|
+
return BOOLEAN_OPERATORS;
|
|
91
|
+
case 'iri-single':
|
|
92
|
+
case 'iri-multi':
|
|
93
|
+
return IRI_OPERATORS;
|
|
94
|
+
case 'enum-literal':
|
|
95
|
+
case 'enum-iri':
|
|
96
|
+
return ENUM_OPERATORS;
|
|
97
|
+
case 'text':
|
|
98
|
+
case 'url':
|
|
99
|
+
default:
|
|
100
|
+
return STRING_OPERATORS;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Apply a filter to a cell value. Returns true if the row should be included.
|
|
106
|
+
*/
|
|
107
|
+
export function applyFilter(cellValue: any, filter: TableFilter): boolean {
|
|
108
|
+
if (!filter.isActive) return true;
|
|
109
|
+
|
|
110
|
+
const { operator, value } = filter;
|
|
111
|
+
|
|
112
|
+
// Multi-select enum: "in" operator
|
|
113
|
+
if (operator === 'in' && Array.isArray(value)) {
|
|
114
|
+
if (value.length === 0) return true;
|
|
115
|
+
const cellStr = extractStringValue(cellValue);
|
|
116
|
+
return value.some((v) => cellStr.toLowerCase() === v.toLowerCase());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const filterVal = typeof value === 'string' ? value : String(value);
|
|
120
|
+
if (!filterVal) return true;
|
|
121
|
+
|
|
122
|
+
const cellStr = extractStringValue(cellValue);
|
|
123
|
+
|
|
124
|
+
switch (operator) {
|
|
125
|
+
case 'contains':
|
|
126
|
+
return cellStr.toLowerCase().includes(filterVal.toLowerCase());
|
|
127
|
+
case 'equals':
|
|
128
|
+
return cellStr.toLowerCase() === filterVal.toLowerCase();
|
|
129
|
+
case 'startsWith':
|
|
130
|
+
return cellStr.toLowerCase().startsWith(filterVal.toLowerCase());
|
|
131
|
+
case 'endsWith':
|
|
132
|
+
return cellStr.toLowerCase().endsWith(filterVal.toLowerCase());
|
|
133
|
+
|
|
134
|
+
case 'gt':
|
|
135
|
+
return toNumber(cellStr) > toNumber(filterVal);
|
|
136
|
+
case 'lt':
|
|
137
|
+
return toNumber(cellStr) < toNumber(filterVal);
|
|
138
|
+
case 'gte':
|
|
139
|
+
return toNumber(cellStr) >= toNumber(filterVal);
|
|
140
|
+
case 'lte':
|
|
141
|
+
return toNumber(cellStr) <= toNumber(filterVal);
|
|
142
|
+
|
|
143
|
+
case 'before':
|
|
144
|
+
return new Date(cellStr) < new Date(filterVal);
|
|
145
|
+
case 'after':
|
|
146
|
+
return new Date(cellStr) > new Date(filterVal);
|
|
147
|
+
|
|
148
|
+
case 'is':
|
|
149
|
+
return cellStr.toLowerCase() === filterVal.toLowerCase();
|
|
150
|
+
case 'isNot':
|
|
151
|
+
return cellStr.toLowerCase() !== filterVal.toLowerCase();
|
|
152
|
+
|
|
153
|
+
default:
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Apply all active filters to a row. Returns true if the row passes all filters (AND logic).
|
|
160
|
+
* Same-column filters with 'in' operator use OR within the column.
|
|
161
|
+
*/
|
|
162
|
+
export function applyAllFilters(
|
|
163
|
+
row: Record<string, any>,
|
|
164
|
+
filters: TableFilter[]
|
|
165
|
+
): boolean {
|
|
166
|
+
const activeFilters = filters.filter((f) => f.isActive);
|
|
167
|
+
if (activeFilters.length === 0) return true;
|
|
168
|
+
|
|
169
|
+
// Group filters by column for same-column OR handling
|
|
170
|
+
const byColumn = new Map<string, TableFilter[]>();
|
|
171
|
+
for (const f of activeFilters) {
|
|
172
|
+
const group = byColumn.get(f.columnId) || [];
|
|
173
|
+
group.push(f);
|
|
174
|
+
byColumn.set(f.columnId, group);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Each column group must pass (AND across columns)
|
|
178
|
+
for (const [columnId, columnFilters] of byColumn) {
|
|
179
|
+
const cellValue = row[columnId];
|
|
180
|
+
// Within a column, ANY filter passing counts (OR within column)
|
|
181
|
+
const columnPasses = columnFilters.some((f) => applyFilter(cellValue, f));
|
|
182
|
+
if (!columnPasses) return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generate a unique filter ID.
|
|
190
|
+
*/
|
|
191
|
+
let filterCounter = 0;
|
|
192
|
+
export function generateFilterId(): string {
|
|
193
|
+
return `filter-${++filterCounter}-${Date.now()}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Format a filter for display as a badge label.
|
|
198
|
+
*/
|
|
199
|
+
export function formatFilterLabel(filter: TableFilter): string {
|
|
200
|
+
const { columnLabel, operator, value } = filter;
|
|
201
|
+
if (operator === 'in' && Array.isArray(value)) {
|
|
202
|
+
return `${columnLabel}: ${value.map(humanizeEnumLabel).join(', ')}`;
|
|
203
|
+
}
|
|
204
|
+
const opLabel = [...STRING_OPERATORS, ...NUMBER_OPERATORS, ...DATE_OPERATORS, ...BOOLEAN_OPERATORS, ...ENUM_OPERATORS]
|
|
205
|
+
.find((o) => o.value === operator)?.label || operator;
|
|
206
|
+
return `${columnLabel} ${opLabel} ${value}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
210
|
+
function extractStringValue(val: any): string {
|
|
211
|
+
if (val == null) return '';
|
|
212
|
+
if (typeof val === 'string') return val;
|
|
213
|
+
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
|
214
|
+
if (typeof val === 'object') {
|
|
215
|
+
return val.label || val.name || val.id || String(val);
|
|
216
|
+
}
|
|
217
|
+
return String(val);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function toNumber(val: string): number {
|
|
221
|
+
const n = parseFloat(val);
|
|
222
|
+
return isNaN(n) ? 0 : n;
|
|
223
|
+
}
|