@manta-eu/react 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/dist/components/channel-icon/index.d.ts +1 -0
  2. package/dist/components/channel-icon/index.d.ts.map +1 -1
  3. package/dist/components/channel-icon/index.js +1 -1
  4. package/dist/components/channel-icon/index.js.map +1 -1
  5. package/dist/components/planning/ActioningView.d.ts +1 -2
  6. package/dist/components/planning/ActioningView.d.ts.map +1 -1
  7. package/dist/components/planning/ActioningView.js +19 -44
  8. package/dist/components/planning/ActioningView.js.map +1 -1
  9. package/dist/components/planning/ManualActionCard.d.ts +4 -0
  10. package/dist/components/planning/ManualActionCard.d.ts.map +1 -1
  11. package/dist/components/planning/ManualActionCard.js +7 -5
  12. package/dist/components/planning/ManualActionCard.js.map +1 -1
  13. package/dist/components/planning/ManualActionList.d.ts +4 -0
  14. package/dist/components/planning/ManualActionList.d.ts.map +1 -1
  15. package/dist/components/planning/ManualActionList.js +8 -2
  16. package/dist/components/planning/ManualActionList.js.map +1 -1
  17. package/dist/components/planning/actioning-view-utils.d.ts +9 -0
  18. package/dist/components/planning/actioning-view-utils.d.ts.map +1 -1
  19. package/dist/components/planning/actioning-view-utils.js +19 -1
  20. package/dist/components/planning/actioning-view-utils.js.map +1 -1
  21. package/dist/components/tooltip/index.d.ts +7 -0
  22. package/dist/components/tooltip/index.d.ts.map +1 -1
  23. package/dist/components/tooltip/index.js +10 -1
  24. package/dist/components/tooltip/index.js.map +1 -1
  25. package/dist/globals.css +19 -6
  26. package/dist/graph/entity-link.d.ts +14 -0
  27. package/dist/graph/entity-link.d.ts.map +1 -0
  28. package/dist/graph/entity-link.js +12 -0
  29. package/dist/graph/entity-link.js.map +1 -0
  30. package/dist/graph/graph-list-utils.d.ts +1 -0
  31. package/dist/graph/graph-list-utils.d.ts.map +1 -1
  32. package/dist/graph/graph-list-utils.js +1 -0
  33. package/dist/graph/graph-list-utils.js.map +1 -1
  34. package/dist/graph/graph-model.d.ts.map +1 -1
  35. package/dist/graph/graph-model.js +14 -5
  36. package/dist/graph/graph-model.js.map +1 -1
  37. package/dist/graph/nodes/BrandAssetAnalysisView.d.ts.map +1 -1
  38. package/dist/graph/nodes/BrandAssetAnalysisView.js +2 -1
  39. package/dist/graph/nodes/BrandAssetAnalysisView.js.map +1 -1
  40. package/dist/graph/nodes/BrandFontSuggestionView.d.ts.map +1 -1
  41. package/dist/graph/nodes/BrandFontSuggestionView.js +2 -1
  42. package/dist/graph/nodes/BrandFontSuggestionView.js.map +1 -1
  43. package/dist/graph/nodes/BrandGuidelinesView.js +1 -1
  44. package/dist/graph/nodes/BrandGuidelinesView.js.map +1 -1
  45. package/dist/graph/nodes/NodePropertyTable.d.ts +4 -2
  46. package/dist/graph/nodes/NodePropertyTable.d.ts.map +1 -1
  47. package/dist/graph/nodes/NodePropertyTable.js +12 -59
  48. package/dist/graph/nodes/NodePropertyTable.js.map +1 -1
  49. package/dist/graph/nodes/node-property-row.d.ts +30 -0
  50. package/dist/graph/nodes/node-property-row.d.ts.map +1 -0
  51. package/dist/graph/nodes/node-property-row.js +35 -0
  52. package/dist/graph/nodes/node-property-row.js.map +1 -0
  53. package/dist/graph/property-formatters.d.ts +1 -0
  54. package/dist/graph/property-formatters.d.ts.map +1 -1
  55. package/dist/graph/property-formatters.js +4 -1
  56. package/dist/graph/property-formatters.js.map +1 -1
  57. package/dist/graph/special-nodes/AttributeList.d.ts +27 -0
  58. package/dist/graph/special-nodes/AttributeList.d.ts.map +1 -0
  59. package/dist/graph/special-nodes/AttributeList.js +57 -0
  60. package/dist/graph/special-nodes/AttributeList.js.map +1 -0
  61. package/dist/graph/special-nodes/EntityPreview.d.ts +5 -0
  62. package/dist/graph/special-nodes/EntityPreview.d.ts.map +1 -0
  63. package/dist/graph/special-nodes/EntityPreview.js +19 -0
  64. package/dist/graph/special-nodes/EntityPreview.js.map +1 -0
  65. package/dist/graph/special-nodes/EntityValueChip.d.ts +11 -0
  66. package/dist/graph/special-nodes/EntityValueChip.d.ts.map +1 -0
  67. package/dist/graph/special-nodes/EntityValueChip.js +40 -0
  68. package/dist/graph/special-nodes/EntityValueChip.js.map +1 -0
  69. package/dist/graph/special-nodes/GenericView.d.ts +2 -1
  70. package/dist/graph/special-nodes/GenericView.d.ts.map +1 -1
  71. package/dist/graph/special-nodes/GenericView.js +23 -69
  72. package/dist/graph/special-nodes/GenericView.js.map +1 -1
  73. package/dist/graph/special-nodes/card-facts.d.ts +52 -0
  74. package/dist/graph/special-nodes/card-facts.d.ts.map +1 -0
  75. package/dist/graph/special-nodes/card-facts.js +189 -0
  76. package/dist/graph/special-nodes/card-facts.js.map +1 -0
  77. package/dist/graph/special-nodes/property-icons.d.ts +9 -0
  78. package/dist/graph/special-nodes/property-icons.d.ts.map +1 -0
  79. package/dist/graph/special-nodes/property-icons.js +39 -0
  80. package/dist/graph/special-nodes/property-icons.js.map +1 -0
  81. package/dist/graph/status-utils.d.ts +16 -0
  82. package/dist/graph/status-utils.d.ts.map +1 -1
  83. package/dist/graph/status-utils.js +35 -0
  84. package/dist/graph/status-utils.js.map +1 -1
  85. package/dist/graph/types.d.ts +5 -0
  86. package/dist/graph/types.d.ts.map +1 -1
  87. package/dist/hooks/use-approve-anyway.d.ts +12 -0
  88. package/dist/hooks/use-approve-anyway.d.ts.map +1 -0
  89. package/dist/hooks/use-approve-anyway.js +81 -0
  90. package/dist/hooks/use-approve-anyway.js.map +1 -0
  91. package/dist/hooks/use-connection-problems.d.ts.map +1 -1
  92. package/dist/hooks/useValidateActionPlan.d.ts.map +1 -1
  93. package/dist/ldo-ui.d.ts +2 -1
  94. package/dist/ldo-ui.d.ts.map +1 -1
  95. package/dist/ldo-ui.js +1 -0
  96. package/dist/ldo-ui.js.map +1 -1
  97. package/dist/lib/manual-actions.d.ts +2 -1
  98. package/dist/lib/manual-actions.d.ts.map +1 -1
  99. package/dist/lib/manual-actions.js +3 -2
  100. package/dist/lib/manual-actions.js.map +1 -1
  101. package/dist/providers/ActiveChatProvider.js +3 -1
  102. package/dist/providers/ActiveChatProvider.js.map +1 -1
  103. package/dist/providers/ChatManagerProvider.d.ts.map +1 -1
  104. package/dist/providers/ChatManagerProvider.js +3 -1
  105. package/dist/providers/ChatManagerProvider.js.map +1 -1
  106. package/dist/providers/chat-socket-auth.d.ts +7 -2
  107. package/dist/providers/chat-socket-auth.d.ts.map +1 -1
  108. package/dist/providers/chat-socket-auth.js +87 -16
  109. package/dist/providers/chat-socket-auth.js.map +1 -1
  110. package/dist/providers/index.d.ts.map +1 -1
  111. package/dist/providers/index.js +8 -1
  112. package/dist/providers/index.js.map +1 -1
  113. package/dist/routes/activity/detail/ActivityChanges.d.ts +2 -0
  114. package/dist/routes/activity/detail/ActivityChanges.d.ts.map +1 -1
  115. package/dist/routes/activity/detail/ActivityChanges.js +8 -7
  116. package/dist/routes/activity/detail/ActivityChanges.js.map +1 -1
  117. package/dist/routes/activity/detail/ActivityDetail.d.ts +1 -0
  118. package/dist/routes/activity/detail/ActivityDetail.d.ts.map +1 -1
  119. package/dist/routes/activity/detail/ActivityDetail.js +5 -4
  120. package/dist/routes/activity/detail/ActivityDetail.js.map +1 -1
  121. package/dist/routes/activity/detail/ActivityStepsRail.d.ts +1 -0
  122. package/dist/routes/activity/detail/ActivityStepsRail.d.ts.map +1 -1
  123. package/dist/routes/activity/detail/ActivityStepsRail.js +3 -2
  124. package/dist/routes/activity/detail/ActivityStepsRail.js.map +1 -1
  125. package/dist/routes/activity/detail/ActivityTimeline.js +2 -2
  126. package/dist/routes/activity/detail/ActivityTimeline.js.map +1 -1
  127. package/dist/routes/activity/detail/NoChangesMessage.d.ts +6 -0
  128. package/dist/routes/activity/detail/NoChangesMessage.d.ts.map +1 -0
  129. package/dist/routes/activity/detail/NoChangesMessage.js +10 -0
  130. package/dist/routes/activity/detail/NoChangesMessage.js.map +1 -0
  131. package/dist/routes/data-explorer/components/RawTriples.js +1 -1
  132. package/dist/routes/data-explorer/components/RawTriples.js.map +1 -1
  133. package/dist/routes/data-explorer/components/RecentTypes.js +1 -1
  134. package/dist/routes/data-explorer/components/RecentTypes.js.map +1 -1
  135. package/dist/routes/data-explorer/components/class-detail/Instances.js +1 -1
  136. package/dist/routes/data-explorer/components/class-detail/Instances.js.map +1 -1
  137. package/dist/routes/data-explorer/components/namespace-detail/NamespaceClassRow.js +1 -1
  138. package/dist/routes/data-explorer/components/namespace-detail/NamespaceClassRow.js.map +1 -1
  139. package/dist/routes/data-explorer/components/ontology-coverage/CoverageTermColumns.js +1 -1
  140. package/dist/routes/data-explorer/components/ontology-coverage/CoverageTermColumns.js.map +1 -1
  141. package/dist/routes/data-explorer/components/ontology-coverage/UnobservedRuleVocabulary.js +1 -1
  142. package/dist/routes/data-explorer/components/ontology-coverage/UnobservedRuleVocabulary.js.map +1 -1
  143. package/dist/routes/data-explorer/components/record-detail/ChatPreview.js +2 -2
  144. package/dist/routes/data-explorer/components/record-detail/ChatPreview.js.map +1 -1
  145. package/dist/routes/integrations/components/connect-forms/DataConnectionConnectForm.d.ts.map +1 -1
  146. package/dist/routes/integrations/components/connect-forms/DataConnectionConnectForm.js +13 -6
  147. package/dist/routes/integrations/components/connect-forms/DataConnectionConnectForm.js.map +1 -1
  148. package/dist/routes/integrations/components/connect-forms/data-connection-fields.d.ts +3 -0
  149. package/dist/routes/integrations/components/connect-forms/data-connection-fields.d.ts.map +1 -1
  150. package/dist/routes/integrations/components/connect-forms/data-connection-fields.js +9 -2
  151. package/dist/routes/integrations/components/connect-forms/data-connection-fields.js.map +1 -1
  152. package/dist/routes/plans/components/ActionPlanPage.d.ts.map +1 -1
  153. package/dist/routes/plans/components/ActionPlanPage.js +4 -7
  154. package/dist/routes/plans/components/ActionPlanPage.js.map +1 -1
  155. package/dist/routes/plans/components/PlanDetailRouteContent.d.ts.map +1 -1
  156. package/dist/routes/plans/components/PlanDetailRouteContent.js +2 -1
  157. package/dist/routes/plans/components/PlanDetailRouteContent.js.map +1 -1
  158. package/package.json +12 -7
  159. package/src/app-subpaths.json +1 -0
  160. package/src/components/channel-icon/index.tsx +1 -1
  161. package/src/components/planning/ActioningView.tsx +62 -70
  162. package/src/components/planning/ManualActionCard.tsx +66 -14
  163. package/src/components/planning/ManualActionList.tsx +30 -11
  164. package/src/components/planning/actioning-view-utils.ts +27 -1
  165. package/src/components/tooltip/index.tsx +35 -2
  166. package/src/graph/entity-link.tsx +20 -0
  167. package/src/graph/graph-list-utils.ts +2 -0
  168. package/src/graph/graph-model.ts +15 -6
  169. package/src/graph/nodes/BrandAssetAnalysisView.tsx +3 -1
  170. package/src/graph/nodes/BrandFontSuggestionView.tsx +7 -1
  171. package/src/graph/nodes/BrandGuidelinesView.tsx +1 -1
  172. package/src/graph/nodes/NodePropertyTable.tsx +32 -143
  173. package/src/graph/nodes/node-property-row.tsx +154 -0
  174. package/src/graph/property-formatters.ts +5 -1
  175. package/src/graph/special-nodes/AttributeList.tsx +177 -0
  176. package/src/graph/special-nodes/EntityPreview.tsx +27 -0
  177. package/src/graph/special-nodes/EntityValueChip.tsx +85 -0
  178. package/src/graph/special-nodes/GenericView.tsx +49 -139
  179. package/src/graph/special-nodes/card-facts.ts +278 -0
  180. package/src/graph/special-nodes/property-icons.ts +59 -0
  181. package/src/graph/status-utils.ts +40 -0
  182. package/src/graph/types.ts +4 -0
  183. package/src/hooks/use-approve-anyway.ts +100 -0
  184. package/src/ldo-ui.ts +8 -0
  185. package/src/lib/manual-actions.ts +3 -2
  186. package/src/providers/ActiveChatProvider.tsx +3 -1
  187. package/src/providers/ChatManagerProvider.tsx +3 -1
  188. package/src/providers/chat-socket-auth.ts +95 -16
  189. package/src/providers/index.tsx +16 -1
  190. package/src/routes/activity/detail/ActivityChanges.tsx +21 -5
  191. package/src/routes/activity/detail/ActivityDetail.tsx +24 -5
  192. package/src/routes/activity/detail/ActivityStepsRail.tsx +4 -2
  193. package/src/routes/activity/detail/ActivityTimeline.tsx +2 -2
  194. package/src/routes/activity/detail/NoChangesMessage.tsx +20 -0
  195. package/src/routes/data-explorer/components/RawTriples.tsx +1 -1
  196. package/src/routes/data-explorer/components/RecentTypes.tsx +1 -1
  197. package/src/routes/data-explorer/components/class-detail/Instances.tsx +1 -1
  198. package/src/routes/data-explorer/components/namespace-detail/NamespaceClassRow.tsx +1 -1
  199. package/src/routes/data-explorer/components/ontology-coverage/CoverageTermColumns.tsx +1 -1
  200. package/src/routes/data-explorer/components/ontology-coverage/UnobservedRuleVocabulary.tsx +1 -1
  201. package/src/routes/data-explorer/components/record-detail/ChatPreview.tsx +2 -2
  202. package/src/routes/integrations/components/connect-forms/DataConnectionConnectForm.tsx +38 -12
  203. package/src/routes/integrations/components/connect-forms/data-connection-fields.ts +11 -2
  204. package/src/routes/plans/components/ActionPlanPage.tsx +9 -8
  205. package/src/routes/plans/components/PlanDetailRouteContent.tsx +4 -3
  206. package/dist/components/planning/ExecutionActivityRow.d.ts +0 -9
  207. package/dist/components/planning/ExecutionActivityRow.d.ts.map +0 -1
  208. package/dist/components/planning/ExecutionActivityRow.js +0 -53
  209. package/dist/components/planning/ExecutionActivityRow.js.map +0 -1
  210. package/dist/components/planning/ExecutionSectionItem.d.ts +0 -20
  211. package/dist/components/planning/ExecutionSectionItem.d.ts.map +0 -1
  212. package/dist/components/planning/ExecutionSectionItem.js +0 -66
  213. package/dist/components/planning/ExecutionSectionItem.js.map +0 -1
  214. package/dist/components/planning/ExecutionSections.d.ts +0 -16
  215. package/dist/components/planning/ExecutionSections.d.ts.map +0 -1
  216. package/dist/components/planning/ExecutionSections.js +0 -32
  217. package/dist/components/planning/ExecutionSections.js.map +0 -1
  218. package/dist/components/planning/action-plan-icons.d.ts +0 -3
  219. package/dist/components/planning/action-plan-icons.d.ts.map +0 -1
  220. package/dist/components/planning/action-plan-icons.js +0 -25
  221. package/dist/components/planning/action-plan-icons.js.map +0 -1
  222. package/dist/components/planning/execution-section-model.d.ts +0 -24
  223. package/dist/components/planning/execution-section-model.d.ts.map +0 -1
  224. package/dist/components/planning/execution-section-model.js +0 -150
  225. package/dist/components/planning/execution-section-model.js.map +0 -1
  226. package/dist/routes/data-explorer/components/TruncateTooltip.d.ts +0 -7
  227. package/dist/routes/data-explorer/components/TruncateTooltip.d.ts.map +0 -1
  228. package/dist/routes/data-explorer/components/TruncateTooltip.js +0 -9
  229. package/dist/routes/data-explorer/components/TruncateTooltip.js.map +0 -1
  230. package/src/components/planning/ExecutionActivityRow.tsx +0 -89
  231. package/src/components/planning/ExecutionSectionItem.tsx +0 -184
  232. package/src/components/planning/ExecutionSections.tsx +0 -97
  233. package/src/components/planning/action-plan-icons.tsx +0 -27
  234. package/src/components/planning/execution-section-model.ts +0 -190
  235. package/src/routes/data-explorer/components/TruncateTooltip.tsx +0 -25
@@ -0,0 +1,278 @@
1
+ import { isFullIri } from "@manta-eu/client/shared/rdf-namespaces";
2
+ import { isPresent } from "@manta-eu/lib/present";
3
+ import { compactUri, expandPrefix } from "@manta-eu/rdf-lib/namespaces";
4
+ import type { LucideIcon } from "lucide-react";
5
+ import { groupBy, isNotNil, uniq } from "ramda";
6
+ import {
7
+ getCurrencyCode,
8
+ isDisplayablePreviewKey,
9
+ normalizePredicateKey,
10
+ selectPriorityPreviewFields,
11
+ type PreviewFieldHints,
12
+ } from "../graph-list-utils";
13
+ import { classifyPropertyDisplay, type PropertyDisplay } from "../nodes/node-property-display";
14
+ import { ontologyPropertyIri } from "../ontology-labels";
15
+ import { formatDisplayValue, isIsoDateTime, parseNumeric } from "../property-formatters";
16
+ import { rdfTypeColors } from "../rdf-type-colors";
17
+ import type { NodeProperty, NodeRelation } from "../types";
18
+ import { getChannelFromId } from "./node-data";
19
+ import { propertyIcon, relationIcon } from "./property-icons";
20
+ import type { Channel } from "#components/channel-icon/index";
21
+ import { formatPropertyLabel } from "#lib/iri-labels";
22
+ import type { OntologyTerm } from "#lib/ontology-terms";
23
+ import { searchItems } from "#lib/search";
24
+
25
+ export const explainCardMaxFacts = 5;
26
+ export const defaultCardMaxFacts = 4;
27
+ export const attributeSearchThreshold = 8;
28
+ export const collapsedChipLimit = 3;
29
+
30
+ const defaultColor = "#6b7280";
31
+ const importantKeys = new Set(["name", "title", "label", "description", "id", "identifier"]);
32
+
33
+ export type CardFact = {
34
+ key: string;
35
+ predicate?: string;
36
+ label: string;
37
+ value: string;
38
+ display: PropertyDisplay;
39
+ icon: LucideIcon;
40
+ entityIris: string[];
41
+ entityColor?: string;
42
+ highlighted: boolean;
43
+ channel?: Channel;
44
+ };
45
+
46
+ export function typeColor(type?: string): string {
47
+ if (!type) return defaultColor;
48
+ return rdfTypeColors[type] ?? rdfTypeColors[expandPrefix(type)] ?? defaultColor;
49
+ }
50
+
51
+ // Only numbers and timestamps are reformatted; text such as "FR" stays as stored.
52
+ export function formatFactValue(property: NodeProperty): string {
53
+ if (parseNumeric(property.value) === null && !isIsoDateTime(property.value)) return property.value;
54
+
55
+ return formatDisplayValue(property.value, {
56
+ predicate: property.iri ?? property.key,
57
+ predicateLabel: property.label ?? property.key,
58
+ valueScale: property.valueScale,
59
+ });
60
+ }
61
+
62
+ function looksRaw(label: string): boolean {
63
+ return !/\s/.test(label) && (/^[a-z]/.test(label) || /[a-z][A-Z]/.test(label) || /[:/#]/.test(label));
64
+ }
65
+
66
+ export function readableLabel(property: NodeProperty): string {
67
+ const raw = property.label || property.iri || property.key;
68
+
69
+ // Only a raw property name or IRI is re-cased; a real label like "ROAS" stays as written.
70
+ return looksRaw(raw) ? formatPropertyLabel(raw) : raw;
71
+ }
72
+
73
+ export function iriChannel(iri: string | undefined): Channel | undefined {
74
+ return iri ? getChannelFromId(compactUri(iri)) : undefined;
75
+ }
76
+
77
+ function vocabularyPrefix(iri: string): string | undefined {
78
+ const compact = compactUri(iri);
79
+ return compact === iri ? undefined : /^([A-Za-z][\w-]*):/.exec(compact)?.[1];
80
+ }
81
+
82
+ // A platform with a logo shows it next to the label; another vocabulary than schema.org or the card's own type
83
+ // keeps its prefix as text.
84
+ export function sourcedLabel(label: string, predicate: string | undefined, type?: string): string {
85
+ if (!predicate || isPresent(iriChannel(predicate))) return label;
86
+
87
+ const prefix = vocabularyPrefix(predicate);
88
+ if (!prefix || prefix === "schema" || (type && prefix === vocabularyPrefix(expandPrefix(type)))) return label;
89
+
90
+ return `${prefix.charAt(0).toUpperCase()}${prefix.slice(1)}:${label}`;
91
+ }
92
+
93
+ type LabelSource = (label: string, predicate: string | undefined) => string;
94
+
95
+ const plainLabel: LabelSource = (label) => label;
96
+
97
+ function termFor(terms: Map<string, OntologyTerm>, predicate: string | undefined): OntologyTerm | undefined {
98
+ const iri = ontologyPropertyIri(predicate);
99
+ return iri ? terms.get(iri) : undefined;
100
+ }
101
+
102
+ // Whole words, so "confidence" or "width" never pass for an "id".
103
+ export function keyWords(key: string): string[] {
104
+ return key
105
+ .split(/[^A-Za-z0-9]+|(?<=[a-z0-9])(?=[A-Z])/)
106
+ .filter(Boolean)
107
+ .map((word) => word.toLowerCase());
108
+ }
109
+
110
+ export function relationFacts(
111
+ relations: NodeRelation[],
112
+ terms: Map<string, OntologyTerm>,
113
+ labelFor: LabelSource = plainLabel,
114
+ ): CardFact[] {
115
+ const byPredicate = groupBy(
116
+ (relation: NodeRelation) => relation.predicate,
117
+ relations.filter((relation) => isFullIri(relation.target)),
118
+ );
119
+
120
+ return Object.entries(byPredicate).map(([predicate, group]) => {
121
+ const targets = uniq((group ?? []).map((relation) => relation.target));
122
+ const term = termFor(terms, predicate);
123
+ const range = term?.rangeIncludes[0];
124
+ const allOntologyTerms = targets.every((target) => isPresent(termFor(terms, target)?.label));
125
+
126
+ return {
127
+ key: `${predicate} entities`,
128
+ predicate,
129
+ label: labelFor(readableLabel({ key: predicate, value: "", iri: predicate, label: term?.label }), predicate),
130
+ value: targets.join(", "),
131
+ display: { kind: "text", text: targets.join(", ") },
132
+ icon: allOntologyTerms ? propertyIcon({ range, display: { kind: "text", text: "" } }) : relationIcon(range),
133
+ entityIris: targets,
134
+ entityColor: typeColor(range),
135
+ highlighted: false,
136
+ channel: iriChannel(predicate),
137
+ };
138
+ });
139
+ }
140
+
141
+ function propertyFact(
142
+ property: NodeProperty,
143
+ jsonLd: Record<string, unknown>,
144
+ terms: Map<string, OntologyTerm>,
145
+ labelFor: LabelSource,
146
+ ) {
147
+ const label = readableLabel(property);
148
+ const value = formatFactValue(property);
149
+ const raw = jsonLd[property.iri ?? ""] ?? property.value;
150
+ const display = classifyPropertyDisplay({ label, formatted: value, raw });
151
+ const range = termFor(terms, property.iri)?.rangeIncludes[0];
152
+
153
+ return {
154
+ key: property.iri ?? property.key,
155
+ label: labelFor(label, property.iri),
156
+ value,
157
+ display,
158
+ icon: propertyIcon({ range, display, isDate: isIsoDateTime(property.value) }),
159
+ entityIris: [],
160
+ highlighted: false,
161
+ channel: iriChannel(property.iri),
162
+ } satisfies CardFact;
163
+ }
164
+
165
+ // graph-model records each IRI value as a relation, rendered as a chip; the property keeps only its other values.
166
+ function withoutRelationValues(
167
+ property: NodeProperty,
168
+ relations: NodeRelation[],
169
+ jsonLd: Record<string, unknown>,
170
+ ): NodeProperty[] {
171
+ const targets = new Set(
172
+ relations.filter((relation) => relation.predicate === property.iri).map((relation) => relation.target),
173
+ );
174
+ if (targets.size === 0) return [property];
175
+
176
+ const raw = jsonLd[property.iri ?? ""];
177
+ const values = (Array.isArray(raw) ? raw : [raw]).filter(
178
+ (value): value is string | number | boolean => isNotNil(value) && typeof value !== "object",
179
+ );
180
+ const rest = values.map(String).filter((value) => !targets.has(value));
181
+
182
+ return isPresent(rest) ? [{ ...property, value: rest.join(" | ") }] : [];
183
+ }
184
+
185
+ export type BuildCardFactsArgs = {
186
+ properties: NodeProperty[];
187
+ relations: NodeRelation[];
188
+ jsonLd: Record<string, unknown>;
189
+ terms: Map<string, OntologyTerm>;
190
+ title?: string;
191
+ type?: string;
192
+ // Only the fallback card names a foreign vocabulary; typed cards keep their labels as written.
193
+ prefixLabels?: boolean;
194
+ previewHints?: PreviewFieldHints;
195
+ };
196
+
197
+ export function buildCardFacts(args: BuildCardFactsArgs): { facts: CardFact[]; previewCount: number } {
198
+ const { relations, jsonLd, terms, title, type, prefixLabels = false, previewHints } = args;
199
+ const labelFor: LabelSource = prefixLabels ? (label, predicate) => sourcedLabel(label, predicate, type) : plainLabel;
200
+
201
+ const properties = args.properties.flatMap((property) => withoutRelationValues(property, relations, jsonLd));
202
+ const linkedFacts = relationFacts(relations, terms, labelFor);
203
+ function notTitle(fact: CardFact) {
204
+ return title === undefined || fact.value !== title;
205
+ }
206
+
207
+ if (previewHints) {
208
+ const displayable = properties
209
+ .filter((property) => isDisplayablePreviewKey(property.key))
210
+ .map((property) => ({ ...property, label: readableLabel(property) }));
211
+ const fields = selectPriorityPreviewFields(displayable, previewHints, getCurrencyCode([], properties), {
212
+ limit: displayable.length,
213
+ keepUnmatched: true,
214
+ });
215
+
216
+ const explainFacts = fields.map((field): CardFact => {
217
+ const display = classifyPropertyDisplay({ label: field.label, formatted: field.value, raw: field.rawValue });
218
+
219
+ return {
220
+ key: field.id,
221
+ label: labelFor(field.label, field.predicate),
222
+ value: field.value,
223
+ display,
224
+ icon: propertyIcon({ display, isDate: isIsoDateTime(field.rawValue ?? "") }),
225
+ entityIris: [],
226
+ highlighted: field.isHighlighted ?? false,
227
+ channel: iriChannel(field.predicate),
228
+ };
229
+ });
230
+
231
+ const highlight = previewHints.highlightPredicate
232
+ ? normalizePredicateKey(previewHints.highlightPredicate)
233
+ : undefined;
234
+ function isHighlighted(fact: CardFact) {
235
+ return isPresent(highlight) && isPresent(fact.predicate) && normalizePredicateKey(fact.predicate) === highlight;
236
+ }
237
+ const linked = linkedFacts.map((fact) => (isHighlighted(fact) ? { ...fact, highlighted: true } : fact));
238
+
239
+ return {
240
+ facts: [
241
+ ...linked.filter(isHighlighted),
242
+ ...explainFacts,
243
+ ...linked.filter((fact) => !isHighlighted(fact)),
244
+ ].filter(notTitle),
245
+ previewCount: explainCardMaxFacts,
246
+ };
247
+ }
248
+
249
+ function isImportant(property: NodeProperty) {
250
+ return keyWords(property.key).some((word) => importantKeys.has(word));
251
+ }
252
+ const important = properties.filter(isImportant).map((property) => propertyFact(property, jsonLd, terms, labelFor));
253
+ const other = properties
254
+ .filter((property) => !isImportant(property))
255
+ .map((property) => propertyFact(property, jsonLd, terms, labelFor));
256
+
257
+ return { facts: [...important, ...linkedFacts, ...other].filter(notTitle), previewCount: defaultCardMaxFacts };
258
+ }
259
+
260
+ export function isAttributeSearchable(expanded: boolean, factCount: number): boolean {
261
+ return expanded && factCount > attributeSearchThreshold;
262
+ }
263
+
264
+ export function visibleChips(iris: string[], expanded: boolean): { shown: string[]; hiddenCount: number } {
265
+ const shown = expanded ? iris : iris.slice(0, collapsedChipLimit);
266
+
267
+ return { shown, hiddenCount: iris.length - shown.length };
268
+ }
269
+
270
+ // A linked-entity row is found by its label: its value is the targets' IRIs, which say nothing a reader sees.
271
+ export function searchFacts(facts: CardFact[], query: string): CardFact[] {
272
+ const documents = facts.map((fact) => ({
273
+ fact,
274
+ text: isPresent(fact.entityIris) ? fact.label : `${fact.label} ${fact.value}`,
275
+ }));
276
+
277
+ return searchItems(documents, query, { keys: ["text"], threshold: 0.35 }).map((document) => document.fact);
278
+ }
@@ -0,0 +1,59 @@
1
+ import { RangeKind, rangeKind } from "@manta-eu/ldo/range-kinds";
2
+ import { expandPrefix } from "@manta-eu/rdf-lib/namespaces";
3
+ import {
4
+ AlignLeft,
5
+ Building2,
6
+ Calendar,
7
+ CircleDollarSign,
8
+ Hash,
9
+ Link2,
10
+ type LucideIcon,
11
+ Palette,
12
+ Percent,
13
+ Tags,
14
+ ToggleLeft,
15
+ Type,
16
+ User,
17
+ } from "lucide-react";
18
+ import { match } from "ts-pattern";
19
+ import type { PropertyDisplay } from "../nodes/node-property-display";
20
+
21
+ const kindIcons: Record<RangeKind, LucideIcon> = {
22
+ [RangeKind.Text]: Type,
23
+ [RangeKind.Number]: Hash,
24
+ [RangeKind.Boolean]: ToggleLeft,
25
+ [RangeKind.Date]: Calendar,
26
+ [RangeKind.Url]: Link2,
27
+ [RangeKind.Money]: CircleDollarSign,
28
+ [RangeKind.Person]: User,
29
+ [RangeKind.Organization]: Building2,
30
+ };
31
+
32
+ function displayIcon(display: PropertyDisplay): LucideIcon {
33
+ return match(display)
34
+ .with({ kind: "number" }, () => Hash)
35
+ .with({ kind: "boolean" }, () => ToggleLeft)
36
+ .with({ kind: "color" }, () => Palette)
37
+ .with({ kind: "ratio" }, () => Percent)
38
+ .with({ kind: "tags" }, () => Tags)
39
+ .with({ kind: "prose" }, () => AlignLeft)
40
+ .with({ kind: "text" }, () => Type)
41
+ .exhaustive();
42
+ }
43
+
44
+ export function propertyIcon(args: { range?: string; display?: PropertyDisplay; isDate?: boolean }): LucideIcon {
45
+ const { range, display, isDate = false } = args;
46
+
47
+ const kind = range ? rangeKind(expandPrefix(range)) : undefined;
48
+ if (kind) return kindIcons[kind];
49
+
50
+ if (isDate) return Calendar;
51
+
52
+ return display ? displayIcon(display) : Type;
53
+ }
54
+
55
+ export function relationIcon(range?: string): LucideIcon {
56
+ const kind = range ? rangeKind(expandPrefix(range)) : undefined;
57
+
58
+ return kind === RangeKind.Person || kind === RangeKind.Organization ? kindIcons[kind] : Link2;
59
+ }
@@ -1,4 +1,5 @@
1
1
  import type { NodeProperty } from "./types";
2
+ import type { BadgeVariant } from "#components/badge/index";
2
3
 
3
4
  export function titleCase(s: string): string {
4
5
  return s.replace(/\b\w/g, (c) => c.toUpperCase());
@@ -18,6 +19,45 @@ export const statusColors: Record<string, { bg: string; text: string }> = {
18
19
  deleted: { bg: "#ef4444", text: "#ffffff" },
19
20
  };
20
21
 
22
+ export enum NodeStatus {
23
+ Active = "active",
24
+ Enabled = "enabled",
25
+ Serving = "serving",
26
+ Paused = "paused",
27
+ Pending = "pending",
28
+ Draft = "draft",
29
+ Archived = "archived",
30
+ Disabled = "disabled",
31
+ Removed = "removed",
32
+ Error = "error",
33
+ Deleted = "deleted",
34
+ }
35
+
36
+ const knownStatuses: ReadonlySet<string> = new Set(Object.values(NodeStatus));
37
+
38
+ export function isNodeStatus(value: string): value is NodeStatus {
39
+ return knownStatuses.has(value);
40
+ }
41
+
42
+ const statusBadgeVariants: Record<NodeStatus, BadgeVariant> = {
43
+ [NodeStatus.Active]: "success",
44
+ [NodeStatus.Serving]: "success",
45
+ [NodeStatus.Enabled]: "copy",
46
+ [NodeStatus.Draft]: "copy",
47
+ [NodeStatus.Paused]: "warning",
48
+ [NodeStatus.Pending]: "warning",
49
+ [NodeStatus.Archived]: "copy",
50
+ [NodeStatus.Disabled]: "copy",
51
+ [NodeStatus.Removed]: "destructive",
52
+ [NodeStatus.Error]: "destructive",
53
+ [NodeStatus.Deleted]: "destructive",
54
+ };
55
+
56
+ export function statusBadgeVariant(status: string): BadgeVariant {
57
+ const normalized = status.trim().toLowerCase();
58
+ return isNodeStatus(normalized) ? statusBadgeVariants[normalized] : "copy";
59
+ }
60
+
21
61
  export function getStatusFromProperties(properties: NodeProperty[]): string | undefined {
22
62
  return properties.find((p) => {
23
63
  const key = p.key.toLowerCase().trim();
@@ -10,6 +10,9 @@ export type NodeProperty = {
10
10
  valueScale?: number;
11
11
  };
12
12
 
13
+ // A property whose value is another entity in the graph; the canvas draws it as an edge instead.
14
+ export type NodeRelation = { predicate: string; target: string };
15
+
13
16
  export type RDFNodeData = {
14
17
  label: string;
15
18
  type?: string;
@@ -17,6 +20,7 @@ export type RDFNodeData = {
17
20
  isHub: boolean;
18
21
  jsonLd: Record<string, unknown>;
19
22
  properties: NodeProperty[];
23
+ relations?: NodeRelation[];
20
24
  childIds: string[];
21
25
  parentId?: string;
22
26
  isCollapsed: boolean;
@@ -0,0 +1,100 @@
1
+ import { useExecuteAgentActionAdditionMutation } from "@manta-eu/client";
2
+ import { isPresent } from "@manta-eu/lib/present";
3
+ import { isValidFullIri, namespaces } from "@manta-eu/rdf-lib/namespaces";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import { DataFactory } from "ldkit/rdf";
6
+ import { sparql } from "ldkit/sparql";
7
+ import { useCallback, useMemo, useRef, useState } from "react";
8
+ import { toast } from "sonner";
9
+ import type { ManualAction } from "./use-manual-actions";
10
+ import { useSparqlSelect } from "./use-sparql-construct";
11
+ import { buildActionDocument, mintSubjectIri } from "#lib/action-document";
12
+ import { useMantaClient } from "#providers/MantaClientProvider";
13
+ import { invalidateActionPlanQueries } from "#shared/query-keys";
14
+
15
+ const df = new DataFactory();
16
+ const assetAnalysis = `${namespaces.brand}AssetAnalysis`;
17
+ const assetDecision = `${namespaces.brand}AssetDecision`;
18
+ const decideAssetShape = `${namespaces.brand}DecideAssetShape`;
19
+
20
+ export type ApproveAnywayHandle = {
21
+ /** Sends the approval; `reviewKey` names the review it answers, so a later review of the same analysis asks again. */
22
+ approve: (analysisIri: string, reviewKey: string) => void;
23
+ approving: ReadonlySet<string>;
24
+ approved: ReadonlySet<string>;
25
+ };
26
+
27
+ /** Maps each manual action about a brand analysis to that analysis, the one a person can approve anyway. */
28
+ export function useApprovableAnalyses(actions: readonly ManualAction[]): Map<string, string> {
29
+ const aboutIris = useMemo(() => {
30
+ const iris = actions.flatMap(({ aboutIri }) => (isPresent(aboutIri) && isValidFullIri(aboutIri) ? [aboutIri] : []));
31
+ return [...new Set(iris)].sort();
32
+ }, [actions]);
33
+
34
+ const values = aboutIris.map((iri) => sparql`${df.namedNode(iri)}`).join(" ");
35
+ const query = isPresent(aboutIris)
36
+ ? `SELECT ?about WHERE { VALUES ?about { ${values} } ?about a ${sparql`${df.namedNode(assetAnalysis)}`} . }`
37
+ : "";
38
+
39
+ const { data } = useSparqlSelect({
40
+ query,
41
+ queryKey: ["ldo-ui", "approvable-analyses", aboutIris],
42
+ enabled: isPresent(aboutIris),
43
+ });
44
+
45
+ return useMemo(() => {
46
+ const analyses = new Set((data ?? []).flatMap((row) => row.get("about")?.value ?? []));
47
+ const approvable = new Map<string, string>();
48
+ for (const action of actions) {
49
+ if (isPresent(action.aboutIri) && analyses.has(action.aboutIri)) {
50
+ approvable.set(action.subjectIri, action.aboutIri);
51
+ }
52
+ }
53
+ return approvable;
54
+ }, [actions, data]);
55
+ }
56
+
57
+ /** Records a person's approval of an analysis that was not confirmed on-brand, so the rules publish it anyway. */
58
+ export function useApproveAnyway(planGraphIri: string): ApproveAnywayHandle {
59
+ const queryClient = useQueryClient();
60
+ const { mantaClient } = useMantaClient();
61
+ const { mutateAsync: startAddition } = useExecuteAgentActionAdditionMutation(mantaClient);
62
+ const inFlight = useRef(new Set<string>());
63
+ const [approving, setApproving] = useState<ReadonlySet<string>>(() => new Set());
64
+ const [approved, setApproved] = useState<ReadonlySet<string>>(() => new Set());
65
+
66
+ const approve = useCallback(
67
+ (analysisIri: string, reviewKey: string) => {
68
+ if (inFlight.current.has(analysisIri)) return;
69
+ inFlight.current.add(analysisIri);
70
+ setApproving(new Set(inFlight.current));
71
+ const subjectIri = mintSubjectIri(assetDecision);
72
+ const properties = buildActionDocument(
73
+ { subjectIri, rdfType: assetDecision },
74
+ {
75
+ [`${namespaces.brand}decisionOnAsset`]: { "@id": analysisIri },
76
+ [`${namespaces.brand}decision`]: "approved",
77
+ },
78
+ );
79
+ void startAddition({
80
+ input: { subjectIri, rdfType: assetDecision, actionShape: decideAssetShape, properties },
81
+ })
82
+ .then(({ executeActionAddition }) => {
83
+ if (!executeActionAddition.success) throw new Error(executeActionAddition.error ?? "The approval failed");
84
+ setApproved((previous) => new Set(previous).add(reviewKey));
85
+ toast.success("Approved. It is uploaded, and this review closes, when the plan next runs.");
86
+ void invalidateActionPlanQueries(queryClient, planGraphIri);
87
+ })
88
+ .catch((error: unknown) => {
89
+ toast.error(error instanceof Error ? error.message : "The approval failed");
90
+ })
91
+ .finally(() => {
92
+ inFlight.current.delete(analysisIri);
93
+ setApproving(new Set(inFlight.current));
94
+ });
95
+ },
96
+ [startAddition, queryClient, planGraphIri],
97
+ );
98
+
99
+ return { approve, approving, approved };
100
+ }
package/src/ldo-ui.ts CHANGED
@@ -268,6 +268,7 @@ export type {
268
268
  EntityView,
269
269
  EntityViewProps,
270
270
  LayoutDirection,
271
+ NodeRelation,
271
272
  OnNodeContextMenu,
272
273
  RDFNode,
273
274
  RDFNodeData,
@@ -275,6 +276,13 @@ export type {
275
276
  SampleInstance,
276
277
  } from "./graph/types";
277
278
  export { jsonLdToRdfGraph } from "./graph/jsonld-bridge";
279
+ export {
280
+ EntityLinkProvider,
281
+ useEntityLink,
282
+ type EntityLinkComponent,
283
+ type EntityLinkProps,
284
+ type EntityLinkProviderProps,
285
+ } from "./graph/entity-link";
278
286
  export { rdfGraphToNodesAndEdges, rdfGraphToSummaryNodesAndEdges } from "./graph/graph-model";
279
287
  export {
280
288
  applyDagreLayout,
@@ -16,6 +16,7 @@ export function getPendingManualActions(actions: readonly ManualAction[] | undef
16
16
  return (actions ?? []).filter(isPendingManualAction);
17
17
  }
18
18
 
19
- export function getResolvedManualActions(actions: readonly ManualAction[] | undefined): ManualAction[] {
20
- return (actions ?? []).filter(isResolvedManualAction);
19
+ /** Names the review an approval answers: the step, as of the analysis it currently reports. */
20
+ export function approvalReviewKey(action: Pick<ManualAction, "subjectIri" | "observedValue">): string {
21
+ return `${action.subjectIri}|${action.observedValue ?? ""}`;
21
22
  }
@@ -109,7 +109,9 @@ function ThinkChatSession(props: ThinkChatSessionProps) {
109
109
  sub: [{ agent: "ThinkChat", name: conversationId }],
110
110
  host,
111
111
  query: socketAuth.query,
112
- ...("cacheTtl" in socketAuth ? { cacheTtl: socketAuth.cacheTtl } : {}),
112
+ ...("enabled" in socketAuth
113
+ ? { enabled: socketAuth.enabled, shouldReconnectOnClose: socketAuth.shouldReconnectOnClose }
114
+ : {}),
113
115
  onClose: handleAgentClose,
114
116
  onMessage: handleAgentMessage,
115
117
  });
@@ -97,7 +97,9 @@ export function ChatManagerProvider(props: ChatManagerProviderProps) {
97
97
  basePath: chatManagerBasePath,
98
98
  host: mantaBaseUrl.host,
99
99
  query: socketAuth.query,
100
- ...("cacheTtl" in socketAuth ? { cacheTtl: socketAuth.cacheTtl } : {}),
100
+ ...("enabled" in socketAuth
101
+ ? { enabled: socketAuth.enabled, shouldReconnectOnClose: socketAuth.shouldReconnectOnClose }
102
+ : {}),
101
103
  });
102
104
 
103
105
  const listConversations = useCallback(