@cratis/scene.engine 4.26.0 → 4.28.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 (193) hide show
  1. package/dist/cjs/editing/resolveEffectiveConfiguration.js +8 -0
  2. package/dist/cjs/editing/resolveEffectiveConfiguration.js.map +1 -1
  3. package/dist/cjs/index.js +38 -0
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/navigation/NavigationDiagnosticCode.js +5 -0
  6. package/dist/cjs/navigation/NavigationDiagnosticCode.js.map +1 -1
  7. package/dist/cjs/navigation/NavigationEditKind.js +16 -0
  8. package/dist/cjs/navigation/NavigationEditKind.js.map +1 -0
  9. package/dist/cjs/navigation/NavigationItemOrigin.js +14 -0
  10. package/dist/cjs/navigation/NavigationItemOrigin.js.map +1 -0
  11. package/dist/cjs/navigation/NavigationSourcesKind.js +16 -0
  12. package/dist/cjs/navigation/NavigationSourcesKind.js.map +1 -0
  13. package/dist/cjs/navigation/diagnoseNavigation.js +8 -1
  14. package/dist/cjs/navigation/diagnoseNavigation.js.map +1 -1
  15. package/dist/cjs/navigation/diagnoseNavigationRegions.js +99 -0
  16. package/dist/cjs/navigation/diagnoseNavigationRegions.js.map +1 -0
  17. package/dist/cjs/navigation/effectiveNavigation.js +133 -0
  18. package/dist/cjs/navigation/effectiveNavigation.js.map +1 -0
  19. package/dist/cjs/navigation/navigationEdits.js +230 -0
  20. package/dist/cjs/navigation/navigationEdits.js.map +1 -0
  21. package/dist/cjs/navigation/navigationRegions.js +42 -0
  22. package/dist/cjs/navigation/navigationRegions.js.map +1 -0
  23. package/dist/cjs/structure/ApplicationStructureIndex.js +10 -0
  24. package/dist/cjs/structure/ApplicationStructureIndex.js.map +1 -1
  25. package/dist/cjs/toolbar/SliceDialogSource.js +14 -0
  26. package/dist/cjs/toolbar/SliceDialogSource.js.map +1 -0
  27. package/dist/cjs/toolbar/ToolbarDiagnosticCode.js +16 -0
  28. package/dist/cjs/toolbar/ToolbarDiagnosticCode.js.map +1 -0
  29. package/dist/cjs/toolbar/ToolbarDiagnosticSeverity.js +14 -0
  30. package/dist/cjs/toolbar/ToolbarDiagnosticSeverity.js.map +1 -0
  31. package/dist/cjs/toolbar/sliceDialogFor.js +38 -0
  32. package/dist/cjs/toolbar/sliceDialogFor.js.map +1 -0
  33. package/dist/cjs/toolbar/toolbarButtons.js +93 -0
  34. package/dist/cjs/toolbar/toolbarButtons.js.map +1 -0
  35. package/dist/cjs/toolbar/validateToolbarButtons.js +59 -0
  36. package/dist/cjs/toolbar/validateToolbarButtons.js.map +1 -0
  37. package/dist/cjs/validateNavigationDestinations.js +2 -0
  38. package/dist/cjs/validateNavigationDestinations.js.map +1 -1
  39. package/dist/esm/editing/resolveEffectiveConfiguration.d.ts.map +1 -1
  40. package/dist/esm/editing/resolveEffectiveConfiguration.js +8 -0
  41. package/dist/esm/editing/resolveEffectiveConfiguration.js.map +1 -1
  42. package/dist/esm/index.d.ts +1 -0
  43. package/dist/esm/index.d.ts.map +1 -1
  44. package/dist/esm/index.js +13 -0
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/navigation/EffectiveNavItem.d.ts +14 -0
  47. package/dist/esm/navigation/EffectiveNavItem.d.ts.map +1 -0
  48. package/dist/esm/navigation/EffectiveNavItem.js +2 -0
  49. package/dist/esm/navigation/EffectiveNavItem.js.map +1 -0
  50. package/dist/esm/navigation/EffectiveNavigationRegion.d.ts +11 -0
  51. package/dist/esm/navigation/EffectiveNavigationRegion.d.ts.map +1 -0
  52. package/dist/esm/navigation/EffectiveNavigationRegion.js +2 -0
  53. package/dist/esm/navigation/EffectiveNavigationRegion.js.map +1 -0
  54. package/dist/esm/navigation/NavigationDiagnosticCode.d.ts +6 -1
  55. package/dist/esm/navigation/NavigationDiagnosticCode.d.ts.map +1 -1
  56. package/dist/esm/navigation/NavigationDiagnosticCode.js +5 -0
  57. package/dist/esm/navigation/NavigationDiagnosticCode.js.map +1 -1
  58. package/dist/esm/navigation/NavigationEdit.d.ts +13 -0
  59. package/dist/esm/navigation/NavigationEdit.d.ts.map +1 -0
  60. package/dist/esm/navigation/NavigationEdit.js +2 -0
  61. package/dist/esm/navigation/NavigationEdit.js.map +1 -0
  62. package/dist/esm/navigation/NavigationEditKind.d.ts +7 -0
  63. package/dist/esm/navigation/NavigationEditKind.d.ts.map +1 -0
  64. package/dist/esm/navigation/NavigationEditKind.js +14 -0
  65. package/dist/esm/navigation/NavigationEditKind.js.map +1 -0
  66. package/dist/esm/navigation/NavigationEditOutcome.d.ts +7 -0
  67. package/dist/esm/navigation/NavigationEditOutcome.d.ts.map +1 -0
  68. package/dist/esm/navigation/NavigationEditOutcome.js +2 -0
  69. package/dist/esm/navigation/NavigationEditOutcome.js.map +1 -0
  70. package/dist/esm/navigation/NavigationEditPlan.d.ts +6 -0
  71. package/dist/esm/navigation/NavigationEditPlan.d.ts.map +1 -0
  72. package/dist/esm/navigation/NavigationEditPlan.js +2 -0
  73. package/dist/esm/navigation/NavigationEditPlan.js.map +1 -0
  74. package/dist/esm/navigation/NavigationGraph.d.ts +4 -1
  75. package/dist/esm/navigation/NavigationGraph.d.ts.map +1 -1
  76. package/dist/esm/navigation/NavigationItemOrigin.d.ts +5 -0
  77. package/dist/esm/navigation/NavigationItemOrigin.d.ts.map +1 -0
  78. package/dist/esm/navigation/NavigationItemOrigin.js +12 -0
  79. package/dist/esm/navigation/NavigationItemOrigin.js.map +1 -0
  80. package/dist/esm/navigation/NavigationSources.d.ts +6 -0
  81. package/dist/esm/navigation/NavigationSources.d.ts.map +1 -0
  82. package/dist/esm/navigation/NavigationSources.js +2 -0
  83. package/dist/esm/navigation/NavigationSources.js.map +1 -0
  84. package/dist/esm/navigation/NavigationSourcesKind.d.ts +7 -0
  85. package/dist/esm/navigation/NavigationSourcesKind.d.ts.map +1 -0
  86. package/dist/esm/navigation/NavigationSourcesKind.js +14 -0
  87. package/dist/esm/navigation/NavigationSourcesKind.js.map +1 -0
  88. package/dist/esm/navigation/ResolvedNavigationTarget.d.ts +6 -0
  89. package/dist/esm/navigation/ResolvedNavigationTarget.d.ts.map +1 -0
  90. package/dist/esm/navigation/ResolvedNavigationTarget.js +2 -0
  91. package/dist/esm/navigation/ResolvedNavigationTarget.js.map +1 -0
  92. package/dist/esm/navigation/diagnoseNavigation.d.ts.map +1 -1
  93. package/dist/esm/navigation/diagnoseNavigation.js +8 -1
  94. package/dist/esm/navigation/diagnoseNavigation.js.map +1 -1
  95. package/dist/esm/navigation/diagnoseNavigationRegions.d.ts +8 -0
  96. package/dist/esm/navigation/diagnoseNavigationRegions.d.ts.map +1 -0
  97. package/dist/esm/navigation/diagnoseNavigationRegions.js +97 -0
  98. package/dist/esm/navigation/diagnoseNavigationRegions.js.map +1 -0
  99. package/dist/esm/navigation/effectiveNavigation.d.ts +5 -0
  100. package/dist/esm/navigation/effectiveNavigation.d.ts.map +1 -0
  101. package/dist/esm/navigation/effectiveNavigation.js +131 -0
  102. package/dist/esm/navigation/effectiveNavigation.js.map +1 -0
  103. package/dist/esm/navigation/index.d.ts +14 -0
  104. package/dist/esm/navigation/index.d.ts.map +1 -1
  105. package/dist/esm/navigation/index.js +14 -0
  106. package/dist/esm/navigation/index.js.map +1 -1
  107. package/dist/esm/navigation/navigationEdits.d.ts +11 -0
  108. package/dist/esm/navigation/navigationEdits.d.ts.map +1 -0
  109. package/dist/esm/navigation/navigationEdits.js +225 -0
  110. package/dist/esm/navigation/navigationEdits.js.map +1 -0
  111. package/dist/esm/navigation/navigationRegions.d.ts +9 -0
  112. package/dist/esm/navigation/navigationRegions.d.ts.map +1 -0
  113. package/dist/esm/navigation/navigationRegions.js +34 -0
  114. package/dist/esm/navigation/navigationRegions.js.map +1 -0
  115. package/dist/esm/structure/ApplicationStructureIndex.d.ts +1 -0
  116. package/dist/esm/structure/ApplicationStructureIndex.d.ts.map +1 -1
  117. package/dist/esm/structure/ApplicationStructureIndex.js +10 -0
  118. package/dist/esm/structure/ApplicationStructureIndex.js.map +1 -1
  119. package/dist/esm/toolbar/ResolvedToolbarButton.d.ts +13 -0
  120. package/dist/esm/toolbar/ResolvedToolbarButton.d.ts.map +1 -0
  121. package/dist/esm/toolbar/ResolvedToolbarButton.js +2 -0
  122. package/dist/esm/toolbar/ResolvedToolbarButton.js.map +1 -0
  123. package/dist/esm/toolbar/SliceDialog.d.ts +8 -0
  124. package/dist/esm/toolbar/SliceDialog.d.ts.map +1 -0
  125. package/dist/esm/toolbar/SliceDialog.js +2 -0
  126. package/dist/esm/toolbar/SliceDialog.js.map +1 -0
  127. package/dist/esm/toolbar/SliceDialogSource.d.ts +5 -0
  128. package/dist/esm/toolbar/SliceDialogSource.d.ts.map +1 -0
  129. package/dist/esm/toolbar/SliceDialogSource.js +12 -0
  130. package/dist/esm/toolbar/SliceDialogSource.js.map +1 -0
  131. package/dist/esm/toolbar/ToolbarDiagnostic.d.ts +9 -0
  132. package/dist/esm/toolbar/ToolbarDiagnostic.d.ts.map +1 -0
  133. package/dist/esm/toolbar/ToolbarDiagnostic.js +2 -0
  134. package/dist/esm/toolbar/ToolbarDiagnostic.js.map +1 -0
  135. package/dist/esm/toolbar/ToolbarDiagnosticCode.d.ts +6 -0
  136. package/dist/esm/toolbar/ToolbarDiagnosticCode.d.ts.map +1 -0
  137. package/dist/esm/toolbar/ToolbarDiagnosticCode.js +14 -0
  138. package/dist/esm/toolbar/ToolbarDiagnosticCode.js.map +1 -0
  139. package/dist/esm/toolbar/ToolbarDiagnosticSeverity.d.ts +5 -0
  140. package/dist/esm/toolbar/ToolbarDiagnosticSeverity.d.ts.map +1 -0
  141. package/dist/esm/toolbar/ToolbarDiagnosticSeverity.js +12 -0
  142. package/dist/esm/toolbar/ToolbarDiagnosticSeverity.js.map +1 -0
  143. package/dist/esm/toolbar/index.d.ts +10 -0
  144. package/dist/esm/toolbar/index.d.ts.map +1 -0
  145. package/dist/esm/toolbar/index.js +10 -0
  146. package/dist/esm/toolbar/index.js.map +1 -0
  147. package/dist/esm/toolbar/sliceDialogFor.d.ts +5 -0
  148. package/dist/esm/toolbar/sliceDialogFor.d.ts.map +1 -0
  149. package/dist/esm/toolbar/sliceDialogFor.js +35 -0
  150. package/dist/esm/toolbar/sliceDialogFor.js.map +1 -0
  151. package/dist/esm/toolbar/toolbarButtons.d.ts +10 -0
  152. package/dist/esm/toolbar/toolbarButtons.d.ts.map +1 -0
  153. package/dist/esm/toolbar/toolbarButtons.js +89 -0
  154. package/dist/esm/toolbar/toolbarButtons.js.map +1 -0
  155. package/dist/esm/toolbar/validateToolbarButtons.d.ts +5 -0
  156. package/dist/esm/toolbar/validateToolbarButtons.d.ts.map +1 -0
  157. package/dist/esm/toolbar/validateToolbarButtons.js +57 -0
  158. package/dist/esm/toolbar/validateToolbarButtons.js.map +1 -0
  159. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  160. package/dist/esm/validateNavigationDestinations.js +2 -0
  161. package/dist/esm/validateNavigationDestinations.js.map +1 -1
  162. package/editing/resolveEffectiveConfiguration.ts +13 -0
  163. package/index.ts +1 -0
  164. package/navigation/EffectiveNavItem.ts +37 -0
  165. package/navigation/EffectiveNavigationRegion.ts +26 -0
  166. package/navigation/NavigationDiagnosticCode.ts +15 -0
  167. package/navigation/NavigationEdit.ts +36 -0
  168. package/navigation/NavigationEditKind.ts +19 -0
  169. package/navigation/NavigationEditOutcome.ts +18 -0
  170. package/navigation/NavigationEditPlan.ts +15 -0
  171. package/navigation/NavigationGraph.ts +10 -1
  172. package/navigation/NavigationItemOrigin.ts +13 -0
  173. package/navigation/NavigationSources.ts +15 -0
  174. package/navigation/NavigationSourcesKind.ts +19 -0
  175. package/navigation/ResolvedNavigationTarget.ts +19 -0
  176. package/navigation/diagnoseNavigation.ts +7 -1
  177. package/navigation/diagnoseNavigationRegions.ts +122 -0
  178. package/navigation/effectiveNavigation.ts +105 -0
  179. package/navigation/index.ts +14 -0
  180. package/navigation/navigationEdits.ts +176 -0
  181. package/navigation/navigationRegions.ts +39 -0
  182. package/package.json +3 -3
  183. package/structure/ApplicationStructureIndex.ts +11 -0
  184. package/toolbar/ResolvedToolbarButton.ts +34 -0
  185. package/toolbar/SliceDialog.ts +21 -0
  186. package/toolbar/SliceDialogSource.ts +13 -0
  187. package/toolbar/ToolbarDiagnostic.ts +22 -0
  188. package/toolbar/ToolbarDiagnosticCode.ts +17 -0
  189. package/toolbar/ToolbarDiagnosticSeverity.ts +13 -0
  190. package/toolbar/index.ts +12 -0
  191. package/toolbar/sliceDialogFor.ts +31 -0
  192. package/toolbar/toolbarButtons.ts +83 -0
  193. package/toolbar/validateToolbarButtons.ts +61 -0
@@ -0,0 +1,176 @@
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 { ApplicationLogo, NavigationModel, NavigationRegion, NavigationRegionKind, NavigationRegionMode, NavItem } from '@cratis/scene.model';
5
+ import { NavigationEdit } from './NavigationEdit';
6
+ import { NavigationEditKind } from './NavigationEditKind';
7
+ import { NavigationEditOutcome } from './NavigationEditOutcome';
8
+ import { NavigationEditPlan } from './NavigationEditPlan';
9
+ import { applicationItems, collectItemIds, findModuleNavigation, findNavigationRegion, modeOfModuleRegion } from './navigationRegions';
10
+
11
+ const noMode = 'The application is the base of the navigation and has no region mode.';
12
+ const logoLevel = 'The logo is defined at the application level only.';
13
+ const flatTopbar = 'A topbar is flat; only a sidebar item has sub-items.';
14
+
15
+ /**
16
+ * Plans a change of a module region's mode. Switching to `inherit` clears the module's items (NAV3); switching from
17
+ * `inherit` to `override` with `copy` first copies the application's items so the module starts from them (NAV5).
18
+ * @param navigation The navigation as it is.
19
+ * @param module The module whose region changes; the application has no mode and is refused.
20
+ * @param region The region.
21
+ * @param mode The mode to switch to.
22
+ * @param options `copy` copies the application items when switching from inherit to override.
23
+ */
24
+ export function switchRegionMode(
25
+ navigation: NavigationModel,
26
+ module: string | undefined,
27
+ region: NavigationRegionKind,
28
+ mode: NavigationRegionMode,
29
+ options: { copy?: boolean } = {},
30
+ ): NavigationEditPlan {
31
+ if (module === undefined) return { edits: [], rejected: noMode };
32
+
33
+ const current = modeOfModuleRegion(navigation, module, region);
34
+ if (current === mode) return { edits: [] };
35
+
36
+ const setMode: NavigationEdit = { kind: NavigationEditKind.SetRegionMode, module, region, mode };
37
+ if (mode === NavigationRegionMode.Inherit) {
38
+ return { edits: [{ kind: NavigationEditKind.SetRegionItems, module, region, items: [] }, setMode] };
39
+ }
40
+
41
+ if (mode === NavigationRegionMode.Override && current === NavigationRegionMode.Inherit && options.copy === true) {
42
+ return { edits: [{ kind: NavigationEditKind.SetRegionItems, module, region, items: structuredClone(applicationItems(navigation.application, region)) }, setMode] };
43
+ }
44
+
45
+ return { edits: [setMode] };
46
+ }
47
+
48
+ /**
49
+ * Plans adding an item to a region, after the ones already there. Adding to an inheriting module region first
50
+ * switches it to `extend` (NAV6), so the application's items stay and the new item follows them.
51
+ * @param navigation The navigation as it is.
52
+ * @param module The module that gets the item; undefined for the application.
53
+ * @param region The region.
54
+ * @param item The item to add.
55
+ * @param parent The id of the item it becomes a sub-item of; sidebar only.
56
+ */
57
+ export function addNavItem(
58
+ navigation: NavigationModel,
59
+ module: string | undefined,
60
+ region: NavigationRegionKind,
61
+ item: NavItem,
62
+ parent?: string,
63
+ ): NavigationEditPlan {
64
+ if (parent !== undefined && region !== NavigationRegionKind.Sidebar) return { edits: [], rejected: flatTopbar };
65
+
66
+ const own = ownItems(navigation, module, region);
67
+ const ids = collectItemIds(own, region);
68
+ if (ids.includes(item.id)) return { edits: [], rejected: `Navigation item id '${item.id}' is already used in this region.` };
69
+ if (parent !== undefined && !ids.includes(parent)) return { edits: [], rejected: `There is no item '${parent}' to add a sub-item to.` };
70
+
71
+ const add: NavigationEdit = { kind: NavigationEditKind.AddItem, module, region, item: structuredClone(item) };
72
+ if (parent !== undefined) add.parent = parent;
73
+ if (module !== undefined && modeOfModuleRegion(navigation, module, region) === NavigationRegionMode.Inherit) {
74
+ return { edits: [{ kind: NavigationEditKind.SetRegionMode, module, region, mode: NavigationRegionMode.Extend }, add] };
75
+ }
76
+
77
+ return { edits: [add] };
78
+ }
79
+
80
+ /**
81
+ * Plans setting the logo. The logo is defined at the application level only, so a module is refused.
82
+ * @param navigation The navigation as it is.
83
+ * @param module The module asking; only undefined (the application) is allowed.
84
+ * @param logo The logo.
85
+ */
86
+ export function setApplicationLogo(navigation: NavigationModel, module: string | undefined, logo: ApplicationLogo): NavigationEditPlan {
87
+ void navigation;
88
+ return module === undefined ? { edits: [{ kind: NavigationEditKind.SetLogo, logo: { ...logo } }] } : { edits: [], rejected: logoLevel };
89
+ }
90
+
91
+ /**
92
+ * Applies navigation edits to a copy of the model. Either every edit applies, or none does.
93
+ * @param navigation The navigation as it is; never changed.
94
+ * @param edits The edits, in order.
95
+ */
96
+ export function applyNavigationEdits(navigation: NavigationModel, edits: NavigationEdit[]): NavigationEditOutcome {
97
+ const model = structuredClone(navigation);
98
+ for (const edit of edits) {
99
+ const rejected = apply(model, edit);
100
+ if (rejected !== undefined) return { applied: false, model: navigation, rejected };
101
+ }
102
+
103
+ return { applied: true, model };
104
+ }
105
+
106
+ function ownItems(navigation: NavigationModel, module: string | undefined, region: NavigationRegionKind): NavItem[] {
107
+ return module === undefined
108
+ ? applicationItems(navigation.application, region)
109
+ : findNavigationRegion(findModuleNavigation(navigation, module)?.regions, region)?.items ?? [];
110
+ }
111
+
112
+ function regionOf(model: NavigationModel, module: string | undefined, region: NavigationRegionKind): NavigationRegion {
113
+ let regions: NavigationRegion[];
114
+ if (module === undefined) {
115
+ regions = model.application.regions;
116
+ } else {
117
+ let block = findModuleNavigation(model, module);
118
+ if (!block) {
119
+ block = { module, regions: [] };
120
+ model.modules.push(block);
121
+ }
122
+
123
+ regions = block.regions;
124
+ }
125
+
126
+ let found = findNavigationRegion(regions, region);
127
+ if (!found) {
128
+ found = { region, items: [] };
129
+ regions.push(found);
130
+ }
131
+
132
+ return found;
133
+ }
134
+
135
+ function apply(model: NavigationModel, edit: NavigationEdit): string | undefined {
136
+ switch (edit.kind) {
137
+ case NavigationEditKind.SetRegionMode:
138
+ if (edit.module === undefined) return noMode;
139
+ regionOf(model, edit.module, edit.region!).mode = edit.mode;
140
+ return undefined;
141
+ case NavigationEditKind.SetRegionItems:
142
+ regionOf(model, edit.module, edit.region!).items = structuredClone(edit.items ?? []);
143
+ return undefined;
144
+ case NavigationEditKind.AddItem: {
145
+ const target = regionOf(model, edit.module, edit.region!);
146
+ const item = structuredClone(edit.item!);
147
+ if (collectItemIds(target.items, target.region).includes(item.id)) return `Navigation item id '${item.id}' is already used in this region.`;
148
+ if (edit.parent === undefined) {
149
+ target.items.push(item);
150
+ return undefined;
151
+ }
152
+
153
+ if (target.region !== NavigationRegionKind.Sidebar) return flatTopbar;
154
+ const owner = findItem(target.items, edit.parent);
155
+ if (!owner) return `There is no item '${edit.parent}' to add a sub-item to.`;
156
+ (owner.children ??= []).push(item);
157
+ return undefined;
158
+ }
159
+ case NavigationEditKind.SetLogo:
160
+ if (edit.module !== undefined) return logoLevel;
161
+ model.application.logo = { ...edit.logo! };
162
+ return undefined;
163
+ default:
164
+ return `'${String((edit as NavigationEdit).kind)}' is not a navigation edit.`;
165
+ }
166
+ }
167
+
168
+ function findItem(items: NavItem[], id: string): NavItem | undefined {
169
+ for (const item of items) {
170
+ if (item.id === id) return item;
171
+ const nested = findItem(item.children ?? [], id);
172
+ if (nested) return nested;
173
+ }
174
+
175
+ return undefined;
176
+ }
@@ -0,0 +1,39 @@
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 { ApplicationNavigation, ModuleNavigation, NavigationModel, NavigationRegion, NavigationRegionKind, NavigationRegionMode, NavItem } from '@cratis/scene.model';
5
+
6
+ /** The regions in the order every result lists them. */
7
+ export const navigationRegionOrder: NavigationRegionKind[] = [NavigationRegionKind.Topbar, NavigationRegionKind.Sidebar];
8
+
9
+ /** The slot an item renders into when it names none: the primary outlet. */
10
+ export const primarySlot = 'content';
11
+
12
+ /**
13
+ * Finds the items a level defines for a region.
14
+ * @param regions The regions of the application or of a module.
15
+ * @param region The region to look for.
16
+ */
17
+ export function findNavigationRegion(regions: NavigationRegion[] | undefined, region: NavigationRegionKind): NavigationRegion | undefined {
18
+ return regions?.find(candidate => candidate.region === region);
19
+ }
20
+
21
+ /** Finds what a module declares; undefined when it declares nothing. */
22
+ export function findModuleNavigation(navigation: NavigationModel, module: string): ModuleNavigation | undefined {
23
+ return navigation.modules.find(candidate => candidate.module === module);
24
+ }
25
+
26
+ /** The mode of a module's region: `inherit` when the module does not declare the region or its mode. */
27
+ export function modeOfModuleRegion(navigation: NavigationModel, module: string, region: NavigationRegionKind): NavigationRegionMode {
28
+ return findNavigationRegion(findModuleNavigation(navigation, module)?.regions, region)?.mode ?? NavigationRegionMode.Inherit;
29
+ }
30
+
31
+ /** The application-level items of a region. */
32
+ export function applicationItems(application: ApplicationNavigation, region: NavigationRegionKind): NavItem[] {
33
+ return findNavigationRegion(application.regions, region)?.items ?? [];
34
+ }
35
+
36
+ /** Every id in a list of items, depth first. Sub-items count only in the sidebar. */
37
+ export function collectItemIds(items: NavItem[], region: NavigationRegionKind): string[] {
38
+ return items.flatMap(item => [item.id, ...(region === NavigationRegionKind.Sidebar ? collectItemIds(item.children ?? [], region) : [])]);
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/scene.engine",
3
- "version": "4.26.0",
3
+ "version": "4.28.0",
4
4
  "description": "The Scene runtime: walks a Scene element tree and drives whichever renderer is plugged in. Does not know React, or any other rendering platform, exists.",
5
5
  "keywords": [
6
6
  "cratis",
@@ -48,9 +48,9 @@
48
48
  "up": "yarn g:up"
49
49
  },
50
50
  "dependencies": {
51
- "@cratis/scene.model": "4.26.0"
51
+ "@cratis/scene.model": "4.28.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@cratis/scene.model": "4.26.0"
54
+ "@cratis/scene.model": "4.28.0"
55
55
  }
56
56
  }
@@ -104,6 +104,17 @@ export class ApplicationStructureIndex {
104
104
  return [...(this.featuresByName.get(name.toLowerCase()) ?? [])];
105
105
  }
106
106
 
107
+ /**
108
+ * Finds the state change slices that accept a command, so a command named by hand can be turned into the slice that owns it.
109
+ * @param command A command name - the slice's `command`, or its `id` when it names none - or a slice's qualified id.
110
+ * @returns The qualified ids of the matching state change slices, in declaration order.
111
+ */
112
+ findSlicesByCommand(command: string): string[] {
113
+ return [...this.slices.entries()]
114
+ .filter(([qualified, slice]) => slice.kind === SliceKind.StateChange && (qualified === command || (slice.command ?? slice.id) === command))
115
+ .map(([qualified]) => qualified);
116
+ }
117
+
107
118
  private indexContainer(path: string, features: FeatureNode[], slices: SliceNode[]): void {
108
119
  const seen = new Map<string, string>();
109
120
  const featureNames = new Map<string, string>();
@@ -0,0 +1,34 @@
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 { ToolbarEmphasis } from '@cratis/scene.model';
5
+ import { SliceDialog } from './SliceDialog';
6
+
7
+ /**
8
+ * A toolbar button resolved against the application structure: what a renderer needs to draw it and to open its dialog.
9
+ */
10
+ export interface ResolvedToolbarButton {
11
+ /** The button identity. */
12
+ id: string;
13
+
14
+ /** The text shown on the button. */
15
+ title: string;
16
+
17
+ /** How prominent the button is. */
18
+ emphasis: ToolbarEmphasis;
19
+
20
+ /** Qualified icon reference, when the button has one. */
21
+ icon?: string;
22
+
23
+ /** The qualified id of the slice the button invokes, when it names or resolves to one. */
24
+ slice?: string;
25
+
26
+ /** Whether the slice was found from a hand-authored command rather than named directly. */
27
+ converted: boolean;
28
+
29
+ /** The command the slice accepts; absent unless the slice is a known state change. */
30
+ command?: string;
31
+
32
+ /** The task dialog the button opens; absent unless the slice is a known state change. */
33
+ dialog?: SliceDialog;
34
+ }
@@ -0,0 +1,21 @@
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 { SliceDialogSource } from './SliceDialogSource';
5
+
6
+ /**
7
+ * The task dialog a state change slice opens: its command and the dialog template that shows the command's fields as a form.
8
+ */
9
+ export interface SliceDialog {
10
+ /** The qualified id of the slice. */
11
+ slice: string;
12
+
13
+ /** The command the dialog submits. */
14
+ command: string;
15
+
16
+ /** The dialog template. */
17
+ template: string;
18
+
19
+ /** Whether the slice named the template or the default was used. */
20
+ source: SliceDialogSource;
21
+ }
@@ -0,0 +1,13 @@
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
+ /**
5
+ * Where a slice's dialog template comes from.
6
+ */
7
+ export enum SliceDialogSource {
8
+ /** The slice names its own dialog template. */
9
+ Slice = 'slice',
10
+
11
+ /** The slice names none, so the default form dialog is used. */
12
+ Default = 'default',
13
+ }
@@ -0,0 +1,22 @@
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 { ToolbarDiagnosticCode } from './ToolbarDiagnosticCode';
5
+ import { ToolbarDiagnosticSeverity } from './ToolbarDiagnosticSeverity';
6
+
7
+ /**
8
+ * One problem with a toolbar button.
9
+ */
10
+ export interface ToolbarDiagnostic {
11
+ /** What kind of problem this is. */
12
+ code: ToolbarDiagnosticCode;
13
+
14
+ /** Whether the button cannot work or is only incomplete. */
15
+ severity: ToolbarDiagnosticSeverity;
16
+
17
+ /** A sentence naming the button and the slice. */
18
+ message: string;
19
+
20
+ /** The id of the button the problem was found on. */
21
+ button: string;
22
+ }
@@ -0,0 +1,17 @@
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
+ /**
5
+ * What is wrong with a toolbar button. The values are shared with the C# engine so both report the same code for
6
+ * the same toolbar.
7
+ */
8
+ export enum ToolbarDiagnosticCode {
9
+ /** A button invokes a slice the application structure does not have. */
10
+ ToolbarButtonSliceNotFound = 'toolbarButtonSliceNotFound',
11
+
12
+ /** A button invokes a slice that is not a state change; state views and automations never take user input. */
13
+ ToolbarButtonSliceNotStateChange = 'toolbarButtonSliceNotStateChange',
14
+
15
+ /** A button has no slice yet. It is allowed while designing, and flagged. */
16
+ ToolbarButtonSliceEmpty = 'toolbarButtonSliceEmpty',
17
+ }
@@ -0,0 +1,13 @@
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
+ /**
5
+ * How serious a toolbar diagnostic is.
6
+ */
7
+ export enum ToolbarDiagnosticSeverity {
8
+ /** The button cannot work. */
9
+ Error = 'error',
10
+
11
+ /** The button is allowed but incomplete. */
12
+ Warning = 'warning',
13
+ }
@@ -0,0 +1,12 @@
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 './ToolbarDiagnosticCode';
5
+ export * from './ToolbarDiagnosticSeverity';
6
+ export * from './ToolbarDiagnostic';
7
+ export * from './SliceDialogSource';
8
+ export * from './SliceDialog';
9
+ export * from './ResolvedToolbarButton';
10
+ export * from './sliceDialogFor';
11
+ export * from './toolbarButtons';
12
+ export * from './validateToolbarButtons';
@@ -0,0 +1,31 @@
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 { SliceKind } from '@cratis/scene.model';
5
+ import { ApplicationStructureIndex } from '../structure';
6
+ import { SliceDialog } from './SliceDialog';
7
+ import { SliceDialogSource } from './SliceDialogSource';
8
+
9
+ /** The dialog template used when a slice names none. */
10
+ export const defaultSliceDialogTemplate = 'FormDialog';
11
+
12
+ /**
13
+ * Works out the task dialog a state change slice opens, once, so no host decides it again (TB4): the slice's own
14
+ * dialog template when it names one, else the default form dialog.
15
+ *
16
+ * The C# engine (`SliceDialogs.For`) resolves the same dialog; both are asserted against `toolbar-slice-fixtures.json`.
17
+ *
18
+ * @param index The modules, features and slices of the application.
19
+ * @param sliceId The slice's qualified id.
20
+ * @param defaultTemplate The template used when the slice names none; {@link defaultSliceDialogTemplate} when absent.
21
+ * @returns The dialog, or undefined when there is no such slice or it is not a state change.
22
+ */
23
+ export function sliceDialogFor(index: ApplicationStructureIndex, sliceId: string, defaultTemplate?: string): SliceDialog | undefined {
24
+ const slice = index.findSlice(sliceId);
25
+ if (slice?.kind !== SliceKind.StateChange) return undefined;
26
+
27
+ const command = slice.command ?? slice.id;
28
+ return slice.dialogTemplate
29
+ ? { slice: sliceId, command, template: slice.dialogTemplate, source: SliceDialogSource.Slice }
30
+ : { slice: sliceId, command, template: defaultTemplate ?? defaultSliceDialogTemplate, source: SliceDialogSource.Default };
31
+ }
@@ -0,0 +1,83 @@
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 { ToolbarButton, ToolbarEmphasis } from '@cratis/scene.model';
5
+ import { ApplicationStructureIndex } from '../structure';
6
+ import { ResolvedToolbarButton } from './ResolvedToolbarButton';
7
+ import { sliceDialogFor } from './sliceDialogFor';
8
+
9
+ function text(properties: Record<string, unknown>, name: string): string | undefined {
10
+ const value = properties[name];
11
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
12
+ }
13
+
14
+ /**
15
+ * Reads a button from the properties of a `core:toolbarItem` element: `label` (or `name`), `kind`, `slice`,
16
+ * `emphasis`, `icon` and `target`.
17
+ *
18
+ * The C# engine (`ToolbarButtons.FromItem`) reads the same; both are asserted against `toolbar-slice-fixtures.json`.
19
+ *
20
+ * @param id The element id.
21
+ * @param properties The element properties.
22
+ * @returns The button, or undefined when the item does not invoke a command or slice - a navigate or dialog item.
23
+ */
24
+ export function toolbarButtonFromItem(id: string, properties: Record<string, unknown>): ToolbarButton | undefined {
25
+ const slice = text(properties, 'slice');
26
+ const action = text(properties, 'kind')?.toLowerCase() === 'action';
27
+ if (slice === undefined && !action) return undefined;
28
+
29
+ const button: ToolbarButton = { id, title: text(properties, 'label') ?? text(properties, 'name') ?? id };
30
+ if (slice !== undefined) button.slice = slice;
31
+
32
+ const emphasis = text(properties, 'emphasis')?.toLowerCase();
33
+ if (emphasis === ToolbarEmphasis.Primary || emphasis === ToolbarEmphasis.Secondary || emphasis === ToolbarEmphasis.Quiet) button.emphasis = emphasis;
34
+
35
+ const icon = text(properties, 'icon');
36
+ if (icon !== undefined) button.icon = icon;
37
+
38
+ const target = text(properties, 'target');
39
+ if (target !== undefined) button.target = target;
40
+ return button;
41
+ }
42
+
43
+ /**
44
+ * Converts a hand-authored button to a slice-backed one: a button with no slice whose command is accepted by exactly
45
+ * one state change slice names that slice instead.
46
+ * @param index The modules, features and slices of the application.
47
+ * @param button The button.
48
+ * @returns The button to use, and whether it was converted. A button that cannot be converted is returned as it is.
49
+ */
50
+ export function normalizeToolbarButton(index: ApplicationStructureIndex, button: ToolbarButton): { button: ToolbarButton; converted: boolean } {
51
+ if (button.slice || !button.target) return { button, converted: false };
52
+
53
+ const matches = index.findSlicesByCommand(button.target);
54
+ if (matches.length !== 1) return { button, converted: false };
55
+
56
+ const { target, ...rest } = button;
57
+ void target;
58
+ return { button: { ...rest, slice: matches[0] }, converted: true };
59
+ }
60
+
61
+ /**
62
+ * Resolves a button: its slice, command and task dialog, without any rendering. This is what a renderer, Stage and the
63
+ * Studio preview call.
64
+ *
65
+ * The C# engine (`ToolbarButtons.Resolve`) resolves the same; both are asserted against `toolbar-slice-fixtures.json`.
66
+ *
67
+ * @param index The modules, features and slices of the application.
68
+ * @param button The button.
69
+ * @param defaultDialogTemplate The dialog template used when a slice names none.
70
+ */
71
+ export function resolveToolbarButton(index: ApplicationStructureIndex, button: ToolbarButton, defaultDialogTemplate?: string): ResolvedToolbarButton {
72
+ const { button: normalized, converted } = normalizeToolbarButton(index, button);
73
+ const dialog = normalized.slice ? sliceDialogFor(index, normalized.slice, defaultDialogTemplate) : undefined;
74
+ const resolved: ResolvedToolbarButton = { id: normalized.id, title: normalized.title, emphasis: normalized.emphasis ?? ToolbarEmphasis.Secondary, converted };
75
+ if (normalized.icon !== undefined) resolved.icon = normalized.icon;
76
+ if (normalized.slice) resolved.slice = normalized.slice;
77
+ if (dialog) {
78
+ resolved.command = dialog.command;
79
+ resolved.dialog = dialog;
80
+ }
81
+
82
+ return resolved;
83
+ }
@@ -0,0 +1,61 @@
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 { ApplicationStructure, SliceKind, Toolbar } from '@cratis/scene.model';
5
+ import { ApplicationStructureIndex } from '../structure';
6
+ import { ToolbarDiagnostic } from './ToolbarDiagnostic';
7
+ import { ToolbarDiagnosticCode } from './ToolbarDiagnosticCode';
8
+ import { ToolbarDiagnosticSeverity } from './ToolbarDiagnosticSeverity';
9
+ import { normalizeToolbarButton } from './toolbarButtons';
10
+
11
+ /**
12
+ * Validates the buttons of a toolbar against the application structure: a button must invoke a state change slice
13
+ * (TB3), and a button without a slice is allowed but flagged (TB5). A hand-authored button that names a command is
14
+ * converted first, and a button whose command no single slice accepts is a legacy button that still validates.
15
+ *
16
+ * The C# engine (`ToolbarButtonValidation.Validate`) reports the same codes, severities and messages in the same
17
+ * order; both are asserted against `toolbar-slice-fixtures.json`.
18
+ *
19
+ * @param structure The modules, features and slices of the application.
20
+ * @param toolbar The toolbar.
21
+ * @returns The problems found, in button order.
22
+ */
23
+ export function validateToolbarButtons(structure: ApplicationStructure | ApplicationStructureIndex, toolbar: Toolbar): ToolbarDiagnostic[] {
24
+ const index = structure instanceof ApplicationStructureIndex ? structure : new ApplicationStructureIndex(structure);
25
+ const diagnostics: ToolbarDiagnostic[] = [];
26
+
27
+ for (const authored of toolbar.buttons) {
28
+ const { button } = normalizeToolbarButton(index, authored);
29
+ if (!button.slice) {
30
+ if (!button.target) {
31
+ diagnostics.push({
32
+ code: ToolbarDiagnosticCode.ToolbarButtonSliceEmpty,
33
+ severity: ToolbarDiagnosticSeverity.Warning,
34
+ message: `Toolbar button '${button.id}' has no slice to invoke.`,
35
+ button: button.id,
36
+ });
37
+ }
38
+
39
+ continue;
40
+ }
41
+
42
+ const slice = index.findSlice(button.slice);
43
+ if (!slice) {
44
+ diagnostics.push({
45
+ code: ToolbarDiagnosticCode.ToolbarButtonSliceNotFound,
46
+ severity: ToolbarDiagnosticSeverity.Error,
47
+ message: `Toolbar button '${button.id}' invokes slice '${button.slice}', which does not exist.`,
48
+ button: button.id,
49
+ });
50
+ } else if (slice.kind !== SliceKind.StateChange) {
51
+ diagnostics.push({
52
+ code: ToolbarDiagnosticCode.ToolbarButtonSliceNotStateChange,
53
+ severity: ToolbarDiagnosticSeverity.Error,
54
+ message: `Toolbar button '${button.id}' invokes slice '${button.slice}', which is ${slice.kind === SliceKind.StateView ? 'a state view' : 'an automation'}; only a state change can be invoked from a toolbar.`,
55
+ button: button.id,
56
+ });
57
+ }
58
+ }
59
+
60
+ return diagnostics;
61
+ }