@cratis/scene.engine 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/dist/cjs/editing/DescriptorCatalog.js +42 -0
  2. package/dist/cjs/editing/DescriptorCatalog.js.map +1 -0
  3. package/dist/cjs/editing/EditSession.js +44 -0
  4. package/dist/cjs/editing/EditSession.js.map +1 -0
  5. package/dist/cjs/editing/applyEdit.js +83 -0
  6. package/dist/cjs/editing/applyEdit.js.map +1 -0
  7. package/dist/cjs/editing/applyEffectiveConfiguration.js +62 -0
  8. package/dist/cjs/editing/applyEffectiveConfiguration.js.map +1 -0
  9. package/dist/cjs/editing/collectComponents.js +34 -0
  10. package/dist/cjs/editing/collectComponents.js.map +1 -0
  11. package/dist/cjs/editing/collectIconUsages.js +86 -0
  12. package/dist/cjs/editing/collectIconUsages.js.map +1 -0
  13. package/dist/cjs/editing/describeNode.js +63 -0
  14. package/dist/cjs/editing/describeNode.js.map +1 -0
  15. package/dist/cjs/editing/diagnoseIcons.js +67 -0
  16. package/dist/cjs/editing/diagnoseIcons.js.map +1 -0
  17. package/dist/cjs/editing/diagnostics.js +36 -0
  18. package/dist/cjs/editing/diagnostics.js.map +1 -0
  19. package/dist/cjs/editing/documentPaths.js +45 -0
  20. package/dist/cjs/editing/documentPaths.js.map +1 -0
  21. package/dist/cjs/editing/exposureEdits.js +106 -0
  22. package/dist/cjs/editing/exposureEdits.js.map +1 -0
  23. package/dist/cjs/editing/exposureGrants.js +115 -0
  24. package/dist/cjs/editing/exposureGrants.js.map +1 -0
  25. package/dist/cjs/editing/iconDiagnostics.js +103 -0
  26. package/dist/cjs/editing/iconDiagnostics.js.map +1 -0
  27. package/dist/cjs/editing/indexDocument.js +400 -0
  28. package/dist/cjs/editing/indexDocument.js.map +1 -0
  29. package/dist/cjs/editing/inspect.js +154 -0
  30. package/dist/cjs/editing/inspect.js.map +1 -0
  31. package/dist/cjs/editing/instanceEdits.js +267 -0
  32. package/dist/cjs/editing/instanceEdits.js.map +1 -0
  33. package/dist/cjs/editing/layoutTypeDescriptors.js +425 -0
  34. package/dist/cjs/editing/layoutTypeDescriptors.js.map +1 -0
  35. package/dist/cjs/editing/layoutTypeEdits.js +141 -0
  36. package/dist/cjs/editing/layoutTypeEdits.js.map +1 -0
  37. package/dist/cjs/editing/nodeIds.js +115 -0
  38. package/dist/cjs/editing/nodeIds.js.map +1 -0
  39. package/dist/cjs/editing/pathAccess.js +53 -0
  40. package/dist/cjs/editing/pathAccess.js.map +1 -0
  41. package/dist/cjs/editing/propertyEdits.js +125 -0
  42. package/dist/cjs/editing/propertyEdits.js.map +1 -0
  43. package/dist/cjs/editing/resolveEffectiveConfiguration.js +200 -0
  44. package/dist/cjs/editing/resolveEffectiveConfiguration.js.map +1 -0
  45. package/dist/cjs/editing/resolveTemplateChain.js +100 -0
  46. package/dist/cjs/editing/resolveTemplateChain.js.map +1 -0
  47. package/dist/cjs/editing/structureEdits.js +231 -0
  48. package/dist/cjs/editing/structureEdits.js.map +1 -0
  49. package/dist/cjs/editing/validateBinding.js +111 -0
  50. package/dist/cjs/editing/validateBinding.js.map +1 -0
  51. package/dist/cjs/editing/validateValue.js +114 -0
  52. package/dist/cjs/editing/validateValue.js.map +1 -0
  53. package/dist/cjs/icons/EffectiveIconCatalog.js +212 -0
  54. package/dist/cjs/icons/EffectiveIconCatalog.js.map +1 -0
  55. package/dist/cjs/icons/EffectiveIconEntry.js +22 -0
  56. package/dist/cjs/icons/EffectiveIconEntry.js.map +1 -0
  57. package/dist/cjs/icons/analyzeIconImpact.js +48 -0
  58. package/dist/cjs/icons/analyzeIconImpact.js.map +1 -0
  59. package/dist/cjs/icons/matchesIconQuery.js +22 -0
  60. package/dist/cjs/icons/matchesIconQuery.js.map +1 -0
  61. package/dist/cjs/icons/resolveIconLibraries.js +45 -0
  62. package/dist/cjs/icons/resolveIconLibraries.js.map +1 -0
  63. package/dist/cjs/index.js +67 -0
  64. package/dist/cjs/index.js.map +1 -1
  65. package/dist/esm/editing/DescriptorCatalog.d.ts +9 -0
  66. package/dist/esm/editing/DescriptorCatalog.d.ts.map +1 -0
  67. package/dist/esm/editing/DescriptorCatalog.js +38 -0
  68. package/dist/esm/editing/DescriptorCatalog.js.map +1 -0
  69. package/dist/esm/editing/DocumentIndex.d.ts +31 -0
  70. package/dist/esm/editing/DocumentIndex.d.ts.map +1 -0
  71. package/dist/esm/editing/DocumentIndex.js +2 -0
  72. package/dist/esm/editing/DocumentIndex.js.map +1 -0
  73. package/dist/esm/editing/EditOutcome.d.ts +7 -0
  74. package/dist/esm/editing/EditOutcome.d.ts.map +1 -0
  75. package/dist/esm/editing/EditOutcome.js +2 -0
  76. package/dist/esm/editing/EditOutcome.js.map +1 -0
  77. package/dist/esm/editing/EditSession.d.ts +20 -0
  78. package/dist/esm/editing/EditSession.d.ts.map +1 -0
  79. package/dist/esm/editing/EditSession.js +42 -0
  80. package/dist/esm/editing/EditSession.js.map +1 -0
  81. package/dist/esm/editing/EditingContext.d.ts +10 -0
  82. package/dist/esm/editing/EditingContext.d.ts.map +1 -0
  83. package/dist/esm/editing/EditingContext.js +2 -0
  84. package/dist/esm/editing/EditingContext.js.map +1 -0
  85. package/dist/esm/editing/TemplateChain.d.ts +13 -0
  86. package/dist/esm/editing/TemplateChain.d.ts.map +1 -0
  87. package/dist/esm/editing/TemplateChain.js +2 -0
  88. package/dist/esm/editing/TemplateChain.js.map +1 -0
  89. package/dist/esm/editing/applyEdit.d.ts +5 -0
  90. package/dist/esm/editing/applyEdit.d.ts.map +1 -0
  91. package/dist/esm/editing/applyEdit.js +81 -0
  92. package/dist/esm/editing/applyEdit.js.map +1 -0
  93. package/dist/esm/editing/applyEffectiveConfiguration.d.ts +3 -0
  94. package/dist/esm/editing/applyEffectiveConfiguration.d.ts.map +1 -0
  95. package/dist/esm/editing/applyEffectiveConfiguration.js +60 -0
  96. package/dist/esm/editing/applyEffectiveConfiguration.js.map +1 -0
  97. package/dist/esm/editing/collectComponents.d.ts +3 -0
  98. package/dist/esm/editing/collectComponents.d.ts.map +1 -0
  99. package/dist/esm/editing/collectComponents.js +32 -0
  100. package/dist/esm/editing/collectComponents.js.map +1 -0
  101. package/dist/esm/editing/collectIconUsages.d.ts +5 -0
  102. package/dist/esm/editing/collectIconUsages.d.ts.map +1 -0
  103. package/dist/esm/editing/collectIconUsages.js +84 -0
  104. package/dist/esm/editing/collectIconUsages.js.map +1 -0
  105. package/dist/esm/editing/describeNode.d.ts +11 -0
  106. package/dist/esm/editing/describeNode.d.ts.map +1 -0
  107. package/dist/esm/editing/describeNode.js +60 -0
  108. package/dist/esm/editing/describeNode.js.map +1 -0
  109. package/dist/esm/editing/diagnoseIcons.d.ts +4 -0
  110. package/dist/esm/editing/diagnoseIcons.d.ts.map +1 -0
  111. package/dist/esm/editing/diagnoseIcons.js +65 -0
  112. package/dist/esm/editing/diagnoseIcons.js.map +1 -0
  113. package/dist/esm/editing/diagnostics.d.ts +6 -0
  114. package/dist/esm/editing/diagnostics.d.ts.map +1 -0
  115. package/dist/esm/editing/diagnostics.js +32 -0
  116. package/dist/esm/editing/diagnostics.js.map +1 -0
  117. package/dist/esm/editing/documentPaths.d.ts +8 -0
  118. package/dist/esm/editing/documentPaths.d.ts.map +1 -0
  119. package/dist/esm/editing/documentPaths.js +41 -0
  120. package/dist/esm/editing/documentPaths.js.map +1 -0
  121. package/dist/esm/editing/exposureEdits.d.ts +5 -0
  122. package/dist/esm/editing/exposureEdits.d.ts.map +1 -0
  123. package/dist/esm/editing/exposureEdits.js +103 -0
  124. package/dist/esm/editing/exposureEdits.js.map +1 -0
  125. package/dist/esm/editing/exposureGrants.d.ts +21 -0
  126. package/dist/esm/editing/exposureGrants.d.ts.map +1 -0
  127. package/dist/esm/editing/exposureGrants.js +112 -0
  128. package/dist/esm/editing/exposureGrants.js.map +1 -0
  129. package/dist/esm/editing/given/a_layout_document.d.ts +14 -0
  130. package/dist/esm/editing/given/a_layout_document.d.ts.map +1 -0
  131. package/dist/esm/editing/given/a_layout_document.js +67 -0
  132. package/dist/esm/editing/given/a_layout_document.js.map +1 -0
  133. package/dist/esm/editing/given/a_scene_document.d.ts +20 -0
  134. package/dist/esm/editing/given/a_scene_document.d.ts.map +1 -0
  135. package/dist/esm/editing/given/a_scene_document.js +110 -0
  136. package/dist/esm/editing/given/a_scene_document.js.map +1 -0
  137. package/dist/esm/editing/iconDiagnostics.d.ts +11 -0
  138. package/dist/esm/editing/iconDiagnostics.d.ts.map +1 -0
  139. package/dist/esm/editing/iconDiagnostics.js +99 -0
  140. package/dist/esm/editing/iconDiagnostics.js.map +1 -0
  141. package/dist/esm/editing/index.d.ts +22 -0
  142. package/dist/esm/editing/index.d.ts.map +1 -0
  143. package/dist/esm/editing/index.js +22 -0
  144. package/dist/esm/editing/index.js.map +1 -0
  145. package/dist/esm/editing/indexDocument.d.ts +6 -0
  146. package/dist/esm/editing/indexDocument.d.ts.map +1 -0
  147. package/dist/esm/editing/indexDocument.js +396 -0
  148. package/dist/esm/editing/indexDocument.js.map +1 -0
  149. package/dist/esm/editing/inspect.d.ts +8 -0
  150. package/dist/esm/editing/inspect.d.ts.map +1 -0
  151. package/dist/esm/editing/inspect.js +151 -0
  152. package/dist/esm/editing/inspect.js.map +1 -0
  153. package/dist/esm/editing/instanceEdits.d.ts +9 -0
  154. package/dist/esm/editing/instanceEdits.d.ts.map +1 -0
  155. package/dist/esm/editing/instanceEdits.js +260 -0
  156. package/dist/esm/editing/instanceEdits.js.map +1 -0
  157. package/dist/esm/editing/layoutTypeDescriptors.d.ts +5 -0
  158. package/dist/esm/editing/layoutTypeDescriptors.d.ts.map +1 -0
  159. package/dist/esm/editing/layoutTypeDescriptors.js +421 -0
  160. package/dist/esm/editing/layoutTypeDescriptors.js.map +1 -0
  161. package/dist/esm/editing/layoutTypeEdits.d.ts +4 -0
  162. package/dist/esm/editing/layoutTypeEdits.d.ts.map +1 -0
  163. package/dist/esm/editing/layoutTypeEdits.js +139 -0
  164. package/dist/esm/editing/layoutTypeEdits.js.map +1 -0
  165. package/dist/esm/editing/nodeIds.d.ts +14 -0
  166. package/dist/esm/editing/nodeIds.d.ts.map +1 -0
  167. package/dist/esm/editing/nodeIds.js +102 -0
  168. package/dist/esm/editing/nodeIds.js.map +1 -0
  169. package/dist/esm/editing/pathAccess.d.ts +7 -0
  170. package/dist/esm/editing/pathAccess.d.ts.map +1 -0
  171. package/dist/esm/editing/pathAccess.js +48 -0
  172. package/dist/esm/editing/pathAccess.js.map +1 -0
  173. package/dist/esm/editing/propertyEdits.d.ts +15 -0
  174. package/dist/esm/editing/propertyEdits.d.ts.map +1 -0
  175. package/dist/esm/editing/propertyEdits.js +120 -0
  176. package/dist/esm/editing/propertyEdits.js.map +1 -0
  177. package/dist/esm/editing/resolveEffectiveConfiguration.d.ts +5 -0
  178. package/dist/esm/editing/resolveEffectiveConfiguration.d.ts.map +1 -0
  179. package/dist/esm/editing/resolveEffectiveConfiguration.js +198 -0
  180. package/dist/esm/editing/resolveEffectiveConfiguration.js.map +1 -0
  181. package/dist/esm/editing/resolveTemplateChain.d.ts +4 -0
  182. package/dist/esm/editing/resolveTemplateChain.d.ts.map +1 -0
  183. package/dist/esm/editing/resolveTemplateChain.js +98 -0
  184. package/dist/esm/editing/resolveTemplateChain.js.map +1 -0
  185. package/dist/esm/editing/structureEdits.d.ts +6 -0
  186. package/dist/esm/editing/structureEdits.d.ts.map +1 -0
  187. package/dist/esm/editing/structureEdits.js +227 -0
  188. package/dist/esm/editing/structureEdits.js.map +1 -0
  189. package/dist/esm/editing/validateBinding.d.ts +7 -0
  190. package/dist/esm/editing/validateBinding.d.ts.map +1 -0
  191. package/dist/esm/editing/validateBinding.js +108 -0
  192. package/dist/esm/editing/validateBinding.js.map +1 -0
  193. package/dist/esm/editing/validateValue.d.ts +8 -0
  194. package/dist/esm/editing/validateValue.d.ts.map +1 -0
  195. package/dist/esm/editing/validateValue.js +111 -0
  196. package/dist/esm/editing/validateValue.js.map +1 -0
  197. package/dist/esm/icons/EffectiveIconCatalog.d.ts +25 -0
  198. package/dist/esm/icons/EffectiveIconCatalog.d.ts.map +1 -0
  199. package/dist/esm/icons/EffectiveIconCatalog.js +210 -0
  200. package/dist/esm/icons/EffectiveIconCatalog.js.map +1 -0
  201. package/dist/esm/icons/EffectiveIconEntry.d.ts +9 -0
  202. package/dist/esm/icons/EffectiveIconEntry.d.ts.map +1 -0
  203. package/dist/esm/icons/EffectiveIconEntry.js +20 -0
  204. package/dist/esm/icons/EffectiveIconEntry.js.map +1 -0
  205. package/dist/esm/icons/IconCatalogSource.d.ts +6 -0
  206. package/dist/esm/icons/IconCatalogSource.d.ts.map +1 -0
  207. package/dist/esm/icons/IconCatalogSource.js +2 -0
  208. package/dist/esm/icons/IconCatalogSource.js.map +1 -0
  209. package/dist/esm/icons/IconDiagnostic.d.ts +9 -0
  210. package/dist/esm/icons/IconDiagnostic.d.ts.map +1 -0
  211. package/dist/esm/icons/IconDiagnostic.js +2 -0
  212. package/dist/esm/icons/IconDiagnostic.js.map +1 -0
  213. package/dist/esm/icons/IconImpactReport.d.ts +18 -0
  214. package/dist/esm/icons/IconImpactReport.d.ts.map +1 -0
  215. package/dist/esm/icons/IconImpactReport.js +2 -0
  216. package/dist/esm/icons/IconImpactReport.js.map +1 -0
  217. package/dist/esm/icons/IconLibraryResolution.d.ts +7 -0
  218. package/dist/esm/icons/IconLibraryResolution.d.ts.map +1 -0
  219. package/dist/esm/icons/IconLibraryResolution.js +2 -0
  220. package/dist/esm/icons/IconLibraryResolution.js.map +1 -0
  221. package/dist/esm/icons/IconQuery.d.ts +7 -0
  222. package/dist/esm/icons/IconQuery.d.ts.map +1 -0
  223. package/dist/esm/icons/IconQuery.js +2 -0
  224. package/dist/esm/icons/IconQuery.js.map +1 -0
  225. package/dist/esm/icons/IconResolution.d.ts +14 -0
  226. package/dist/esm/icons/IconResolution.d.ts.map +1 -0
  227. package/dist/esm/icons/IconResolution.js +2 -0
  228. package/dist/esm/icons/IconResolution.js.map +1 -0
  229. package/dist/esm/icons/IconSearchResult.d.ts +7 -0
  230. package/dist/esm/icons/IconSearchResult.d.ts.map +1 -0
  231. package/dist/esm/icons/IconSearchResult.js +2 -0
  232. package/dist/esm/icons/IconSearchResult.js.map +1 -0
  233. package/dist/esm/icons/IconUsage.d.ts +6 -0
  234. package/dist/esm/icons/IconUsage.d.ts.map +1 -0
  235. package/dist/esm/icons/IconUsage.js +2 -0
  236. package/dist/esm/icons/IconUsage.js.map +1 -0
  237. package/dist/esm/icons/ResolvedIconLibrary.d.ts +8 -0
  238. package/dist/esm/icons/ResolvedIconLibrary.d.ts.map +1 -0
  239. package/dist/esm/icons/ResolvedIconLibrary.js +2 -0
  240. package/dist/esm/icons/ResolvedIconLibrary.js.map +1 -0
  241. package/dist/esm/icons/analyzeIconImpact.d.ts +5 -0
  242. package/dist/esm/icons/analyzeIconImpact.d.ts.map +1 -0
  243. package/dist/esm/icons/analyzeIconImpact.js +46 -0
  244. package/dist/esm/icons/analyzeIconImpact.js.map +1 -0
  245. package/dist/esm/icons/index.d.ts +14 -0
  246. package/dist/esm/icons/index.d.ts.map +1 -0
  247. package/dist/esm/icons/index.js +14 -0
  248. package/dist/esm/icons/index.js.map +1 -0
  249. package/dist/esm/icons/matchesIconQuery.d.ts +4 -0
  250. package/dist/esm/icons/matchesIconQuery.d.ts.map +1 -0
  251. package/dist/esm/icons/matchesIconQuery.js +20 -0
  252. package/dist/esm/icons/matchesIconQuery.js.map +1 -0
  253. package/dist/esm/icons/resolveIconLibraries.d.ts +4 -0
  254. package/dist/esm/icons/resolveIconLibraries.d.ts.map +1 -0
  255. package/dist/esm/icons/resolveIconLibraries.js +43 -0
  256. package/dist/esm/icons/resolveIconLibraries.js.map +1 -0
  257. package/dist/esm/index.d.ts +2 -0
  258. package/dist/esm/index.d.ts.map +1 -1
  259. package/dist/esm/index.js +21 -0
  260. package/dist/esm/index.js.map +1 -1
  261. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  262. package/editing/DescriptorCatalog.ts +51 -0
  263. package/editing/DocumentIndex.ts +73 -0
  264. package/editing/EditOutcome.ts +21 -0
  265. package/editing/EditSession.ts +50 -0
  266. package/editing/EditingContext.ts +34 -0
  267. package/editing/TemplateChain.ts +36 -0
  268. package/editing/applyEdit.ts +69 -0
  269. package/editing/applyEffectiveConfiguration.ts +53 -0
  270. package/editing/collectComponents.ts +36 -0
  271. package/editing/collectIconUsages.ts +78 -0
  272. package/editing/describeNode.ts +61 -0
  273. package/editing/diagnoseIcons.ts +60 -0
  274. package/editing/diagnostics.ts +30 -0
  275. package/editing/documentPaths.ts +40 -0
  276. package/editing/exposureEdits.ts +119 -0
  277. package/editing/exposureGrants.ts +159 -0
  278. package/editing/given/a_layout_document.ts +88 -0
  279. package/editing/given/a_scene_document.ts +135 -0
  280. package/editing/iconDiagnostics.ts +90 -0
  281. package/editing/index.ts +24 -0
  282. package/editing/indexDocument.ts +237 -0
  283. package/editing/inspect.ts +165 -0
  284. package/editing/instanceEdits.ts +251 -0
  285. package/editing/layoutTypeDescriptors.ts +191 -0
  286. package/editing/layoutTypeEdits.ts +135 -0
  287. package/editing/nodeIds.ts +111 -0
  288. package/editing/pathAccess.ts +81 -0
  289. package/editing/propertyEdits.ts +127 -0
  290. package/editing/resolveEffectiveConfiguration.ts +202 -0
  291. package/editing/resolveTemplateChain.ts +109 -0
  292. package/editing/structureEdits.ts +234 -0
  293. package/editing/validateBinding.ts +103 -0
  294. package/editing/validateValue.ts +130 -0
  295. package/icons/EffectiveIconCatalog.ts +199 -0
  296. package/icons/EffectiveIconEntry.ts +36 -0
  297. package/icons/IconCatalogSource.ts +23 -0
  298. package/icons/IconDiagnostic.ts +45 -0
  299. package/icons/IconImpactReport.ts +63 -0
  300. package/icons/IconLibraryResolution.ts +22 -0
  301. package/icons/IconQuery.ts +27 -0
  302. package/icons/IconResolution.ts +25 -0
  303. package/icons/IconSearchResult.ts +21 -0
  304. package/icons/IconUsage.ts +21 -0
  305. package/icons/ResolvedIconLibrary.ts +32 -0
  306. package/icons/analyzeIconImpact.ts +41 -0
  307. package/icons/index.ts +16 -0
  308. package/icons/matchesIconQuery.ts +19 -0
  309. package/icons/resolveIconLibraries.ts +47 -0
  310. package/index.ts +2 -0
  311. package/package.json +3 -3
@@ -0,0 +1,40 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { DocumentPath } from './DocumentIndex';
5
+
6
+ /**
7
+ * Reads the value at a document path.
8
+ */
9
+ export function getAtPath(root: unknown, path: DocumentPath): unknown {
10
+ let current = root;
11
+ for (const key of path) {
12
+ if (current === null || typeof current !== 'object') return undefined;
13
+ current = (current as Record<string | number, unknown>)[key];
14
+ }
15
+
16
+ return current;
17
+ }
18
+
19
+ /**
20
+ * Reads the array at a path, creating it - and any objects on the way - when it is not there yet. Mutates `root`,
21
+ * which callers hold a copy of.
22
+ */
23
+ export function ensureArrayAt(root: unknown, path: DocumentPath): unknown[] {
24
+ let current = root as Record<string | number, unknown>;
25
+ path.forEach((key, position) => {
26
+ if (current[key] === undefined) current[key] = position === path.length - 1 ? [] : {};
27
+ current = current[key] as Record<string | number, unknown>;
28
+ });
29
+
30
+ return current as unknown as unknown[];
31
+ }
32
+
33
+ /**
34
+ * The paths a change to `path` must be made at: the path itself and, for a node inside a freeform element, the same
35
+ * place in every other size-class variant's copy.
36
+ */
37
+ export function mirroredPaths(mirror: { prefix: DocumentPath; others: DocumentPath[] } | undefined, path: DocumentPath): DocumentPath[] {
38
+ if (!mirror || mirror.prefix.some((key, position) => path[position] !== key)) return [path];
39
+ return [path, ...mirror.others.map(other => [...other, ...path.slice(mirror.prefix.length)])];
40
+ }
@@ -0,0 +1,119 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import {
5
+ CollectionOperation, DiagnosticCode, ExposedProperty, ExposePropertyEdit, PropertyValueType, SceneDiagnostic,
6
+ SceneDocument, UnexposePropertyEdit,
7
+ } from '@cratis/scene.model';
8
+ import { findComponentDescriptor } from './DescriptorCatalog';
9
+ import { errorDiagnostic } from './diagnostics';
10
+ import { EditSession } from './EditSession';
11
+ import { grantKey } from './exposureGrants';
12
+ import { cloneData } from './pathAccess';
13
+
14
+ function ownsDeclaration(session: EditSession, owner: string, diagnostics: SceneDiagnostic[]): boolean {
15
+ const level = session.chain.levels.at(-1);
16
+ if (!level || level.owner !== owner) {
17
+ diagnostics.push(errorDiagnostic(DiagnosticCode.NodeNotEditable, `Exposure is declared by the layout or template that owns the component. '${owner}' is not what is being edited.`));
18
+ return false;
19
+ }
20
+
21
+ return true;
22
+ }
23
+
24
+ function validateExposure(session: EditSession, property: ExposedProperty, diagnostics: SceneDiagnostic[]): boolean {
25
+ const context = { component: property.component, path: property.path };
26
+ const level = session.chain.levels.at(-1)!;
27
+ const reExposed = property.reExposes !== undefined;
28
+
29
+ if (!reExposed && !level.elements.some(candidate => candidate.id === property.component)) {
30
+ diagnostics.push(errorDiagnostic(DiagnosticCode.ExposureTargetMissing, `'${level.owner}' has no component '${property.component}'.`, context));
31
+ return false;
32
+ }
33
+
34
+ const grant = reExposed ? session.scopeGrants.get(grantKey(property.component, property.path)) : undefined;
35
+ if (reExposed && (!grant || grant.owner !== property.reExposes)) {
36
+ diagnostics.push(errorDiagnostic(DiagnosticCode.ReExposureBroken, `'${property.reExposes}' does not expose '${property.path}' on '${property.component}' to '${level.owner}', so it cannot be passed on.`, context));
37
+ return false;
38
+ }
39
+
40
+ const element = level.elements.find(candidate => candidate.id === property.component);
41
+ const descriptor = grant
42
+ ? grant.descriptor
43
+ : findComponentDescriptor(session.context.catalog, element!.componentName)?.properties.find(candidate => candidate.path === property.path);
44
+ if (!descriptor) {
45
+ diagnostics.push(errorDiagnostic(DiagnosticCode.ExposureTargetMissing, `There is no editable property '${property.path}' on '${property.component}'.`, context));
46
+ return false;
47
+ }
48
+
49
+ const operations = property.operations ?? [];
50
+ if (descriptor.valueType !== PropertyValueType.Collection) {
51
+ if (operations.length > 0 || property.editableFields !== undefined) {
52
+ diagnostics.push(errorDiagnostic(DiagnosticCode.InvalidEdit, `'${descriptor.label}' is not a collection, so operations and editable fields do not apply.`, context));
53
+ return false;
54
+ }
55
+
56
+ return true;
57
+ }
58
+
59
+ const known = Object.values(CollectionOperation) as string[];
60
+ const unknown = operations.find(operation => !known.includes(operation));
61
+ if (unknown !== undefined) {
62
+ diagnostics.push(errorDiagnostic(DiagnosticCode.InvalidEdit, `'${unknown}' is not a collection operation.`, context));
63
+ return false;
64
+ }
65
+
66
+ const fields = descriptor.item?.properties.map(candidate => candidate.path) ?? [];
67
+ const unknownField = property.editableFields?.find(field => !fields.includes(field));
68
+ if (unknownField !== undefined) {
69
+ diagnostics.push(errorDiagnostic(DiagnosticCode.UnknownCollectionField, `Items of '${descriptor.label}' have no field '${unknownField}'.`, context));
70
+ return false;
71
+ }
72
+
73
+ if (grant && (operations.some(operation => !grant.operations.includes(operation))
74
+ || (grant.editableFields !== undefined && (property.editableFields ?? fields).some(field => !grant.editableFields!.includes(field))))) {
75
+ diagnostics.push(errorDiagnostic(DiagnosticCode.ExposureWidensOwner, `'${property.reExposes}' exposed less of '${descriptor.label}' than this re-exposes.`, context));
76
+ return false;
77
+ }
78
+
79
+ return true;
80
+ }
81
+
82
+ /**
83
+ * Applies an exposure declaration to a copy of the document.
84
+ *
85
+ * Exposure only narrows: the property has to be one the component's descriptor supports, a collection's
86
+ * operations and fields have to exist, and a re-exposure cannot grant more than the owner it passes on.
87
+ */
88
+ export function applyExposeProperty(session: EditSession, edit: ExposePropertyEdit, clone: SceneDocument, diagnostics: SceneDiagnostic[]): void {
89
+ if (!ownsDeclaration(session, edit.owner, diagnostics) || !validateExposure(session, edit.property, diagnostics)) return;
90
+
91
+ let declaration = clone.exposures.find(candidate => candidate.owner === edit.owner);
92
+ if (!declaration) {
93
+ declaration = { owner: edit.owner, properties: [] };
94
+ clone.exposures.push(declaration);
95
+ }
96
+
97
+ const same = (candidate: ExposedProperty) => candidate.component === edit.property.component
98
+ && candidate.path === edit.property.path && candidate.reExposes === edit.property.reExposes;
99
+ const position = declaration.properties.findIndex(same);
100
+ if (position >= 0) declaration.properties[position] = cloneData(edit.property);
101
+ else declaration.properties.push(cloneData(edit.property));
102
+ }
103
+
104
+ /**
105
+ * Applies an exposure withdrawal to a copy of the document. Values already saved against it are not touched.
106
+ */
107
+ export function applyUnexposeProperty(session: EditSession, edit: UnexposePropertyEdit, clone: SceneDocument, diagnostics: SceneDiagnostic[]): void {
108
+ if (!ownsDeclaration(session, edit.owner, diagnostics)) return;
109
+
110
+ const declaration = clone.exposures.find(candidate => candidate.owner === edit.owner);
111
+ const remaining = declaration?.properties.filter(candidate => !(candidate.component === edit.component && candidate.path === edit.path));
112
+ if (!declaration || remaining!.length === declaration.properties.length) {
113
+ diagnostics.push(errorDiagnostic(DiagnosticCode.ExposureTargetMissing, `'${edit.owner}' does not expose '${edit.path}' on '${edit.component}'.`, { component: edit.component, path: edit.path }));
114
+ return;
115
+ }
116
+
117
+ if (remaining!.length === 0) clone.exposures.splice(clone.exposures.indexOf(declaration), 1);
118
+ else declaration.properties = remaining!;
119
+ }
@@ -0,0 +1,159 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import {
5
+ CollectionOperation, DiagnosticCode, ExposedProperty, PropertyDescriptor, PropertyValueType, SceneDiagnostic,
6
+ } from '@cratis/scene.model';
7
+ import { DescriptorCatalog, findComponentDescriptor } from './DescriptorCatalog';
8
+ import { errorDiagnostic } from './diagnostics';
9
+ import { TemplateChain } from './TemplateChain';
10
+
11
+ /**
12
+ * What one instance may configure on one property: the owner's exposure, narrowed by every re-exposure between the
13
+ * owner and that instance.
14
+ */
15
+ export interface ExposureGrant {
16
+ /** The id of the element that owns the property. */
17
+ component: string;
18
+
19
+ path: string;
20
+ descriptor: PropertyDescriptor;
21
+
22
+ /** The name of the layout or template that owns the element. */
23
+ owner: string;
24
+
25
+ /** The index of the owner in the chain. */
26
+ ownerLevel: number;
27
+
28
+ /** For a collection: what may be done to it. Empty for any other property. */
29
+ operations: CollectionOperation[];
30
+
31
+ /** For a collection: the item fields that may be changed; absent means all of them. */
32
+ editableFields?: string[];
33
+
34
+ label?: string;
35
+ }
36
+
37
+ /**
38
+ * The outcome of working out who may configure what.
39
+ */
40
+ export interface ExposureGrants {
41
+ /** Every valid exposure, one per owner-declared property, at the level of its owner. */
42
+ exposed: ExposureGrant[];
43
+
44
+ /** For each contributing instance id, what it may configure, keyed by {@link grantKey}. */
45
+ byInstance: Map<string, Map<string, ExposureGrant>>;
46
+
47
+ diagnostics: SceneDiagnostic[];
48
+ }
49
+
50
+ /**
51
+ * The key a grant is stored under.
52
+ */
53
+ export function grantKey(component: string, path: string): string {
54
+ return `${component}\u0000${path}`;
55
+ }
56
+
57
+ function narrow(grant: ExposureGrant, reExposure: ExposedProperty): { grant: ExposureGrant; widened: boolean } {
58
+ const requested = reExposure.operations ?? [];
59
+ const operations = requested.filter(operation => grant.operations.includes(operation));
60
+ const fields = grant.editableFields === undefined
61
+ ? reExposure.editableFields
62
+ : (reExposure.editableFields === undefined ? grant.editableFields : reExposure.editableFields.filter(field => grant.editableFields!.includes(field)));
63
+
64
+ const widened = operations.length < requested.length
65
+ || (grant.editableFields !== undefined && reExposure.editableFields !== undefined && reExposure.editableFields.some(field => !grant.editableFields!.includes(field)));
66
+
67
+ return { grant: { ...grant, operations, editableFields: fields, label: reExposure.label ?? grant.label }, widened };
68
+ }
69
+
70
+ /**
71
+ * Works out, for every instance in a chain, which properties it may configure.
72
+ *
73
+ * An owner's exposure reaches the instance directly inside it. It reaches further only through explicit
74
+ * re-exposure by every level in between, and each re-exposure can only narrow what it passes on - a nested
75
+ * template that lists more operations or fields than its owner granted gets the owner's, not its own.
76
+ *
77
+ * @param chain The chain to analyze.
78
+ * @param catalog The descriptors that say what each component supports.
79
+ */
80
+ export function computeExposureGrants(chain: TemplateChain, catalog: DescriptorCatalog): ExposureGrants {
81
+ const result: ExposureGrants = { exposed: [], byInstance: new Map(), diagnostics: [] };
82
+ for (const chainLevel of chain.levels) result.byInstance.set(chainLevel.instance, new Map());
83
+
84
+ const consumed = new Set<ExposedProperty>();
85
+
86
+ chain.levels.forEach((owner, ownerLevel) => {
87
+ for (const exposure of owner.exposures.filter(candidate => candidate.reExposes === undefined)) {
88
+ const element = owner.elements.find(candidate => candidate.id === exposure.component);
89
+ if (!element) {
90
+ result.diagnostics.push(errorDiagnostic(
91
+ DiagnosticCode.ExposureTargetMissing,
92
+ `'${owner.owner}' exposes '${exposure.path}' on '${exposure.component}', but it has no such component.`,
93
+ { component: exposure.component, path: exposure.path }));
94
+ continue;
95
+ }
96
+
97
+ const descriptor = findComponentDescriptor(catalog, element.componentName);
98
+ if (!descriptor) {
99
+ result.diagnostics.push(errorDiagnostic(
100
+ DiagnosticCode.MissingComponentDescriptor,
101
+ `'${owner.owner}' exposes '${exposure.path}' on '${exposure.component}', but nothing describes the component '${element.componentName}'.`,
102
+ { component: exposure.component, path: exposure.path }));
103
+ continue;
104
+ }
105
+
106
+ const property = descriptor.properties.find(candidate => candidate.path === exposure.path);
107
+ if (!property) {
108
+ result.diagnostics.push(errorDiagnostic(
109
+ DiagnosticCode.ExposureTargetMissing,
110
+ `'${owner.owner}' exposes '${exposure.path}' on '${exposure.component}', but '${element.componentName}' no longer has that property.`,
111
+ { component: exposure.component, path: exposure.path }));
112
+ continue;
113
+ }
114
+
115
+ const isCollection = property.valueType === PropertyValueType.Collection;
116
+ let current: ExposureGrant = {
117
+ component: exposure.component,
118
+ path: exposure.path,
119
+ descriptor: property,
120
+ owner: owner.owner,
121
+ ownerLevel,
122
+ operations: isCollection ? [...new Set(exposure.operations ?? [])] : [],
123
+ editableFields: isCollection ? exposure.editableFields : undefined,
124
+ label: exposure.label,
125
+ };
126
+ result.exposed.push(current);
127
+
128
+ for (let contributor = ownerLevel + 1; contributor < chain.levels.length; contributor++) {
129
+ result.byInstance.get(chain.levels[contributor].instance)!.set(grantKey(exposure.component, exposure.path), current);
130
+
131
+ const reExposure = chain.levels[contributor].exposures.find(candidate =>
132
+ candidate.reExposes === owner.owner && candidate.component === exposure.component && candidate.path === exposure.path);
133
+ if (!reExposure) break;
134
+
135
+ consumed.add(reExposure);
136
+ const narrowed = narrow(current, reExposure);
137
+ if (narrowed.widened) {
138
+ result.diagnostics.push(errorDiagnostic(
139
+ DiagnosticCode.ExposureWidensOwner,
140
+ `'${chain.levels[contributor].owner}' re-exposes more of '${exposure.path}' than '${owner.owner}' exposed; only what the owner granted is passed on.`,
141
+ { component: exposure.component, path: exposure.path }));
142
+ }
143
+
144
+ current = narrowed.grant;
145
+ }
146
+ }
147
+ });
148
+
149
+ for (const chainLevel of chain.levels) {
150
+ for (const reExposure of chainLevel.exposures.filter(candidate => candidate.reExposes !== undefined && !consumed.has(candidate))) {
151
+ result.diagnostics.push(errorDiagnostic(
152
+ DiagnosticCode.ReExposureBroken,
153
+ `'${chainLevel.owner}' re-exposes '${reExposure.path}' on '${reExposure.component}' from '${reExposure.reExposes}', which does not expose it to this chain.`,
154
+ { component: reExposure.component, path: reExposure.path }));
155
+ }
156
+ }
157
+
158
+ return result;
159
+ }
@@ -0,0 +1,88 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import {
5
+ EditingScopeKind, FlowContainerKind, HeightSizeClass, Orientation, SceneDocument, SceneElement, WidthSizeClass,
6
+ } from '@cratis/scene.model';
7
+ import { EditingContext } from '../../index';
8
+ import { catalog, component, shell } from './a_scene_document';
9
+
10
+ /** A panel-shaped element: everything a `FrameworkElement` has, plus its own properties and children. */
11
+ export function panel(id: string, own: Record<string, unknown>, children: SceneElement[] = []): SceneElement {
12
+ const { componentName: _componentName, slots: _slots, ...frameworkElement } = component(id, 'ignored');
13
+ return { ...frameworkElement, children, ...own } as unknown as SceneElement;
14
+ }
15
+
16
+ export const stackPanel = (id: string, children: SceneElement[], spacing = 0) => panel(id, { orientation: Orientation.Vertical, spacing }, children);
17
+ export const wrapPanel = (id: string, children: SceneElement[], itemWidth?: number) => panel(id, { orientation: Orientation.Horizontal, itemWidth }, children);
18
+
19
+ export const table = (id: string) => component(id, 'test:table');
20
+
21
+ const compact = { width: WidthSizeClass.Compact, height: HeightSizeClass.Compact };
22
+ const regular = { width: WidthSizeClass.Regular, height: HeightSizeClass.Regular };
23
+
24
+ export const mainSlot = 'screenTemplate:Page#slot:main';
25
+ export const sideSlot = 'screenTemplate:Page#slot:side';
26
+ export const canvasSlot = 'screenTemplate:Page#slot:canvas';
27
+
28
+ /**
29
+ * A template with a flow column in `main` (a row of `a`, `b` above a growing `c`), a freeform `canvas` with `f`
30
+ * placed in two size-class variants, and a `side` slot holding a stack panel of `g` and `h` and a lone `i`. A
31
+ * screen `Home` fills it.
32
+ */
33
+ export function createLayoutDocument(): SceneDocument {
34
+ return {
35
+ layouts: [{ ...shell, slots: [{ name: 'content' }] }],
36
+ screenTemplates: [{
37
+ name: 'Page',
38
+ fitsSlot: 'content',
39
+ slots: [
40
+ {
41
+ name: 'main',
42
+ arrangement: {
43
+ root: {
44
+ kind: FlowContainerKind.Column, gap: 8,
45
+ children: [
46
+ { kind: FlowContainerKind.Row, gap: 4, children: [{ content: table('a') }, { content: table('b') }] },
47
+ { content: table('c'), grow: 1 },
48
+ ],
49
+ },
50
+ } as never,
51
+ },
52
+ {
53
+ name: 'canvas',
54
+ arrangement: {
55
+ variants: [
56
+ { sizeClass: compact, placements: [{ element: stackPanel('freeStack', [table('f')]), x: 0, y: 0, width: 100, height: 50 }] },
57
+ { sizeClass: regular, placements: [{ element: stackPanel('freeStack', [table('f')]), x: 10, y: 10, width: 200, height: 80 }] },
58
+ ],
59
+ } as never,
60
+ },
61
+ { name: 'side' },
62
+ ],
63
+ content: { side: [stackPanel('stack', [table('g'), table('h')], 6), table('i')] },
64
+ }],
65
+ dialogTemplates: [],
66
+ screens: [{ name: 'Home', layout: 'Shell', screenTemplate: 'Page', forms: [], contributions: [], slotContent: { side: [table('j')] } }],
67
+ exposures: [],
68
+ instanceContributions: [],
69
+ };
70
+ }
71
+
72
+ export function pageContext(extra: Partial<EditingContext> = {}): EditingContext {
73
+ return { catalog, scope: { kind: EditingScopeKind.ScreenTemplate, name: 'Page', layout: 'Shell' }, ...extra };
74
+ }
75
+
76
+ export function homeContext(extra: Partial<EditingContext> = {}): EditingContext {
77
+ return { catalog, scope: { kind: EditingScopeKind.Screen, name: 'Home' }, ...extra };
78
+ }
79
+
80
+ /** Freezes a document deeply, so any edit that mutates it throws instead of passing quietly. */
81
+ export function deepFreeze<T>(value: T): T {
82
+ if (value !== null && typeof value === 'object') {
83
+ Object.freeze(value);
84
+ for (const child of Object.values(value)) deepFreeze(child);
85
+ }
86
+
87
+ return value;
88
+ }
@@ -0,0 +1,135 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import {
5
+ CollectionOperation, ComponentDescriptor, EditingScope, EditingScopeKind, ExternalComponent, FlowContainerKind,
6
+ HorizontalAlignment, Layout, PropertyValueType, QueryResultShape, SceneDocument, ScreenTemplate, Screen,
7
+ VerticalAlignment, Visibility,
8
+ } from '@cratis/scene.model';
9
+ import { createDescriptorCatalog, EditingContext } from '../../index';
10
+
11
+ export function component(id: string, componentName: string, properties: Record<string, unknown> = {}, slots: ExternalComponent['slots'] = {}): ExternalComponent {
12
+ return {
13
+ id, name: id, componentName, properties, slots,
14
+ visibility: Visibility.Visible, isEnabled: true, opacity: 1, size: {}, zIndex: 0, minimumSize: {}, maximumSize: {},
15
+ margin: { left: 0, top: 0, right: 0, bottom: 0 },
16
+ horizontalAlignment: HorizontalAlignment.Stretch, verticalAlignment: VerticalAlignment.Stretch,
17
+ };
18
+ }
19
+
20
+ /** A navigation bar with a fixed Home item and a collection consumers can extend, like the real one. */
21
+ export const navigationBarDescriptor: ComponentDescriptor = {
22
+ component: 'test:navigationBar',
23
+ displayName: 'Navigation bar',
24
+ properties: [
25
+ {
26
+ path: 'items', label: 'Items', group: 'Content', valueType: PropertyValueType.Collection,
27
+ item: {
28
+ label: 'Item',
29
+ properties: [
30
+ { path: 'label', label: 'Label', group: 'Content', valueType: PropertyValueType.String, constraints: { required: true } },
31
+ { path: 'icon', label: 'Icon', group: 'Content', valueType: PropertyValueType.Icon, editorKind: 'iconPicker' },
32
+ { path: 'destination', label: 'Destination', group: 'Content', valueType: PropertyValueType.Destination },
33
+ ],
34
+ },
35
+ constraints: { maximumItems: 6 },
36
+ },
37
+ { path: 'title', label: 'Title', group: 'Content', valueType: PropertyValueType.String, default: 'Menu' },
38
+ { path: 'density', label: 'Density', group: 'Look', valueType: PropertyValueType.Enum, choices: [{ value: 'compact', label: 'Compact' }, { value: 'roomy', label: 'Roomy' }] },
39
+ ],
40
+ };
41
+
42
+ export const tableDescriptor: ComponentDescriptor = {
43
+ component: 'test:table',
44
+ properties: [
45
+ { path: 'query', label: 'Query', group: 'Data', valueType: PropertyValueType.QueryReference, editorKind: 'queryBinding', constraints: { resultShapes: [QueryResultShape.Collection] } },
46
+ { path: 'pageSize', label: 'Page size', group: 'Data', valueType: PropertyValueType.Number, constraints: { minimum: 1, maximum: 100, integer: true } },
47
+ { path: 'locked', label: 'Locked', group: 'Data', valueType: PropertyValueType.String, readOnly: true },
48
+ ],
49
+ };
50
+
51
+ export const homeItem = { id: 'home', label: 'Home', destination: { screen: 'Home' } };
52
+
53
+ export const shell: Layout = { name: 'Shell', slots: [{ name: 'header' }, { name: 'content' }] };
54
+
55
+ /**
56
+ * Two nested templates and two screens: `Module` (with a navigation bar) fits the shell, `Feature` fits `Module`'s
57
+ * body, and both `Invoices` and `Customers` fill `Feature`.
58
+ */
59
+ export function createSceneDocument(): SceneDocument {
60
+ const module: ScreenTemplate = {
61
+ name: 'Module',
62
+ fitsSlot: 'content',
63
+ slots: [{ name: 'chrome' }, { name: 'body' }],
64
+ content: { chrome: [component('navbar', 'test:navigationBar', { items: [homeItem] })], body: [] },
65
+ };
66
+ const feature: ScreenTemplate = {
67
+ name: 'Feature',
68
+ fitsSlot: 'Module.body',
69
+ slots: [{ name: 'main' }],
70
+ content: {
71
+ main: [],
72
+ },
73
+ arrangement: {
74
+ root: {
75
+ kind: FlowContainerKind.Column, gap: 8, grow: 1,
76
+ children: [
77
+ { kind: FlowContainerKind.Row, gap: 4, children: [{ slotName: 'main' }] },
78
+ ],
79
+ },
80
+ } as ScreenTemplate['arrangement'],
81
+ };
82
+ const invoices: Screen = {
83
+ name: 'Invoices', layout: 'Shell', screenTemplate: 'Feature', forms: [], contributions: [],
84
+ slotContent: { main: [component('invoiceTable', 'test:table', { pageSize: 25 })] },
85
+ };
86
+ const customers: Screen = {
87
+ name: 'Customers', layout: 'Shell', screenTemplate: 'Feature', forms: [], contributions: [],
88
+ slotContent: { main: [component('customerTable', 'test:table')] },
89
+ };
90
+
91
+ return {
92
+ layouts: [shell],
93
+ screenTemplates: [module, feature],
94
+ dialogTemplates: [],
95
+ screens: [invoices, customers],
96
+ exposures: [],
97
+ instanceContributions: [],
98
+ };
99
+ }
100
+
101
+ /** The same document with `Module` exposing the navigation bar's items and title to what sits inside it. */
102
+ export function createExposedDocument(): SceneDocument {
103
+ const document = createSceneDocument();
104
+ document.exposures.push({
105
+ owner: 'Module',
106
+ properties: [
107
+ { component: 'navbar', path: 'items', operations: [CollectionOperation.Add, CollectionOperation.Remove, CollectionOperation.Reorder, CollectionOperation.EditFields], editableFields: ['label', 'icon', 'destination'] },
108
+ { component: 'navbar', path: 'title' },
109
+ ],
110
+ });
111
+ return document;
112
+ }
113
+
114
+ /** The same document with `Feature` re-exposing what `Module` exposed, so screens reach it. */
115
+ export function createReExposedDocument(): SceneDocument {
116
+ const document = createExposedDocument();
117
+ document.exposures.push({
118
+ owner: 'Feature',
119
+ properties: [
120
+ { component: 'navbar', path: 'items', reExposes: 'Module', operations: [CollectionOperation.Add, CollectionOperation.EditFields] },
121
+ { component: 'navbar', path: 'title', reExposes: 'Module' },
122
+ ],
123
+ });
124
+ return document;
125
+ }
126
+
127
+ export const catalog = createDescriptorCatalog([navigationBarDescriptor, tableDescriptor]);
128
+
129
+ export function scopeOf(kind: EditingScopeKind, name: string, layout?: string): EditingScope {
130
+ return { kind, name, layout };
131
+ }
132
+
133
+ export function contextFor(kind: EditingScopeKind, name: string, extra: Partial<EditingContext> = {}): EditingContext {
134
+ return { catalog, scope: scopeOf(kind, name, 'Shell'), ...extra };
135
+ }
@@ -0,0 +1,90 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { CollectionItemDescriptor, DiagnosticCode, DiagnosticSeverity, PropertyDescriptor, PropertyValueType, SceneDiagnostic, isIconReference } from '@cratis/scene.model';
5
+ import { EffectiveIconCatalog } from '../icons/EffectiveIconCatalog';
6
+ import { IconDiagnosticKind } from '../icons/IconDiagnostic';
7
+ import { DiagnosticContext } from './diagnostics';
8
+
9
+ const codes: Record<IconDiagnosticKind, DiagnosticCode> = {
10
+ 'missing-library': DiagnosticCode.MissingIconLibrary,
11
+ 'missing-icon': DiagnosticCode.MissingIcon,
12
+ 'missing-variant': DiagnosticCode.MissingIconVariant,
13
+ 'incompatible-version': DiagnosticCode.IncompatibleIconLibrary,
14
+ 'catalog-unavailable': DiagnosticCode.IconCatalogUnavailable,
15
+ };
16
+
17
+ /**
18
+ * Checks one icon value against the effective icon catalog.
19
+ *
20
+ * Nothing is rewritten or dropped: this only reports. A value whose library, icon or variant is missing, or
21
+ * whose library is at an incompatible version, produces a diagnostic of the given severity - an error when
22
+ * the value is being *set* (the edit is refused), a warning when it is already stored (it is kept, and
23
+ * applies again if the icon comes back). A value that cannot be judged yet, because its library's catalog has
24
+ * not been loaded, produces an `IconNotVerified` warning rather than silently passing.
25
+ *
26
+ * @param catalog The effective catalog of the profile being edited.
27
+ * @param value The value; anything that is not an icon reference is left to `validateValue`.
28
+ * @param severity How serious a definite problem is.
29
+ * @param context What the diagnostic is about.
30
+ */
31
+ export function iconProblem(catalog: EffectiveIconCatalog, value: unknown, severity: DiagnosticSeverity, context: DiagnosticContext): SceneDiagnostic | undefined {
32
+ if (!isIconReference(value)) return undefined;
33
+
34
+ const resolution = catalog.lookup(value);
35
+ if (resolution === undefined) {
36
+ return {
37
+ code: DiagnosticCode.IconNotVerified,
38
+ severity: DiagnosticSeverity.Warning,
39
+ message: `The catalog of the icon library '${value.library}' is not loaded, so '${value.key}' could not be verified.`,
40
+ ...context,
41
+ };
42
+ }
43
+
44
+ return resolution.isResolved ? undefined : { code: codes[resolution.diagnostic.kind], severity, message: `${resolution.diagnostic.message}.`, ...context };
45
+ }
46
+
47
+ /**
48
+ * Checks the icon fields of collection items.
49
+ *
50
+ * @param itemDescriptor What the items look like.
51
+ * @param items The items, each with its id and field values.
52
+ */
53
+ export function iconProblemsOfItems(
54
+ catalog: EffectiveIconCatalog,
55
+ itemDescriptor: CollectionItemDescriptor | undefined,
56
+ items: { id: string; values: Record<string, unknown>; instance?: string }[],
57
+ severity: DiagnosticSeverity,
58
+ context: DiagnosticContext
59
+ ): SceneDiagnostic[] {
60
+ const iconFields = (itemDescriptor?.properties ?? []).filter((field) => field.valueType === PropertyValueType.Icon);
61
+ return items.flatMap((item) =>
62
+ iconFields.flatMap((field) => {
63
+ const problem = iconProblem(catalog, item.values[field.path], severity, { ...context, itemId: item.id, ...(item.instance === undefined ? {} : { instance: item.instance }) });
64
+ return problem ? [{ ...problem, message: `'${field.label}' of '${item.id}': ${problem.message}` }] : [];
65
+ })
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Checks a property's value - an icon, or a collection whose items have icon fields - against the effective icon
71
+ * catalog. Any other kind of property has nothing to check.
72
+ *
73
+ * @param descriptor The property.
74
+ * @param value The value; for a collection, the flat items (`{ id, ...fields }`) as they are stored.
75
+ */
76
+ export function iconProblemsOfProperty(catalog: EffectiveIconCatalog, descriptor: PropertyDescriptor, value: unknown, severity: DiagnosticSeverity, context: DiagnosticContext): SceneDiagnostic[] {
77
+ if (descriptor.valueType === PropertyValueType.Icon) {
78
+ const problem = iconProblem(catalog, value, severity, { ...context, path: descriptor.path });
79
+ return problem ? [{ ...problem, message: `'${descriptor.label}': ${problem.message}` }] : [];
80
+ }
81
+
82
+ if (descriptor.valueType === PropertyValueType.Collection && Array.isArray(value)) {
83
+ const items = value
84
+ .filter((item): item is Record<string, unknown> => item !== null && typeof item === 'object')
85
+ .map(({ id, ...values }) => ({ id: String(id), values }));
86
+ return iconProblemsOfItems(catalog, descriptor.item, items, severity, { ...context, path: descriptor.path });
87
+ }
88
+
89
+ return [];
90
+ }
@@ -0,0 +1,24 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ export * from './diagnostics';
5
+ export * from './nodeIds';
6
+ export * from './layoutTypeDescriptors';
7
+ export * from './DescriptorCatalog';
8
+ export * from './validateValue';
9
+ export * from './validateBinding';
10
+ export * from './TemplateChain';
11
+ export * from './collectComponents';
12
+ export * from './resolveTemplateChain';
13
+ export * from './exposureGrants';
14
+ export * from './resolveEffectiveConfiguration';
15
+ export * from './applyEffectiveConfiguration';
16
+ export * from './DocumentIndex';
17
+ export * from './indexDocument';
18
+ export * from './EditingContext';
19
+ export * from './inspect';
20
+ export * from './EditOutcome';
21
+ export * from './applyEdit';
22
+ export * from './iconDiagnostics';
23
+ export * from './diagnoseIcons';
24
+ export * from './collectIconUsages';