@cratis/scene.engine 4.15.0 → 4.17.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.
@@ -162,6 +162,17 @@ export function resolveEffectiveConfiguration(
162
162
  }
163
163
  }
164
164
 
165
+ const maximumItems = grant.descriptor.constraints?.maximumItems;
166
+ if (isCollection && maximumItems !== undefined && items.length > maximumItems) {
167
+ // Saved items that no longer fit - the owner lowered the limit after they were added - are left out of
168
+ // the result in order and kept in the saved data, like any other saved value that no longer fits.
169
+ const dropped = items.splice(maximumItems);
170
+ diagnostics.push(errorDiagnostic(
171
+ DiagnosticCode.ContributionTypeMismatch,
172
+ `'${grant.path}' on '${grant.component}' allows at most ${maximumItems} items; ${dropped.map(item => `'${item.id}'`).join(', ')} ${dropped.length === 1 ? 'is' : 'are'} ignored and kept.`,
173
+ { instance: current.contributedBy ?? owner.instance, component: grant.component, path: grant.path }));
174
+ }
175
+
165
176
  if (isCollection) {
166
177
  current.items = items;
167
178
  current.inheritedValue = baseItems.map(toFlatItem);
@@ -1,7 +1,7 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- import { CollectionItemDescriptor, PropertyDescriptor, PropertyValueType } from '@cratis/scene.model';
4
+ import { BindingSourceKind, CollectionItemDescriptor, PropertyDescriptor, PropertyValueType } from '@cratis/scene.model';
5
5
 
6
6
  function isRecord(value: unknown): value is Record<string, unknown> {
7
7
  return value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -24,6 +24,13 @@ export function validateValue(descriptor: PropertyDescriptor, value: unknown): s
24
24
  return constraints.required ? 'a value is required' : undefined;
25
25
  }
26
26
 
27
+ const binding = bindingKindOf(value);
28
+ if (binding !== undefined && descriptor.acceptedBindingKinds !== undefined) {
29
+ return descriptor.acceptedBindingKinds.includes(binding.kind)
30
+ ? undefined
31
+ : `a ${binding.kind} binding is not accepted here; use ${descriptor.acceptedBindingKinds.join(' or ')}`;
32
+ }
33
+
27
34
  switch (descriptor.valueType) {
28
35
  case PropertyValueType.String: {
29
36
  if (typeof value !== 'string') return 'expected a string';
@@ -77,6 +84,19 @@ export function validateValue(descriptor: PropertyDescriptor, value: unknown): s
77
84
  * The deepest nesting a JSON value may have. Authored configuration (chart datasets, option lists) is shallow;
78
85
  * the limit turns a runaway or hostile structure into a validation message instead of a stack overflow.
79
86
  */
87
+ const bindingKinds = new Set<string>(Object.values(BindingSourceKind));
88
+
89
+ /**
90
+ * The source kind of a value that is a typed binding expression - an object with a known `kind` and a
91
+ * string `path` - rather than a value of the property's own type. Only consulted for properties that declare
92
+ * `acceptedBindingKinds`, so a collection property such as a command form's `inputs` can be bound to a
93
+ * table's `selectedItem` instead of listing its items.
94
+ */
95
+ function bindingKindOf(value: unknown): { kind: BindingSourceKind } | undefined {
96
+ if (!isRecord(value) || typeof value.kind !== 'string' || !bindingKinds.has(value.kind) || typeof value.path !== 'string') return undefined;
97
+ return { kind: value.kind as BindingSourceKind };
98
+ }
99
+
80
100
  const maximumJsonDepth = 64;
81
101
 
82
102
  /**
package/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from './BindingScope';
7
7
  export * from './BindingDiagnostic';
8
8
  export * from './resolveBindingExpression';
9
9
  export * from './nestBindingScope';
10
+ export * from './resolveArgumentSource';
10
11
  export * from './removeComponentOutputs';
11
12
  export * from './QueryBindingState';
12
13
  export * from './QueryBindingTicket';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/scene.engine",
3
- "version": "4.15.0",
3
+ "version": "4.17.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.15.0"
51
+ "@cratis/scene.model": "4.17.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@cratis/scene.model": "4.15.0"
54
+ "@cratis/scene.model": "4.17.0"
55
55
  }
56
56
  }
@@ -0,0 +1,33 @@
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 { BindingSourceKind } from '@cratis/scene.model';
5
+ import { BindingScope } from './BindingScope';
6
+ import { resolveBindingExpression } from './resolveBindingExpression';
7
+
8
+ /** The prefix of an argument source that reads another element's output. */
9
+ export const componentArgumentSourcePrefix = 'component.';
10
+
11
+ /**
12
+ * Resolves where a command argument comes from, as an action's argument mapping writes it.
13
+ *
14
+ * `component.<id>.<output>` reads an output another element publishes - an input's `value`, a table's
15
+ * `selectedItem.id` - from the same output scope a `componentProperty` binding reads. Element ids may contain
16
+ * dots, so the longest id with outputs in scope wins: `component.order.lines.value` reads `value` of
17
+ * `order.lines` when that element exists. Any other source is a path in the data context. An empty source,
18
+ * or an element with no outputs in scope, resolves as absent.
19
+ *
20
+ * The C# engine (`ArgumentSources.Resolve`) behaves identically; both run `argument-source-fixtures.json`.
21
+ */
22
+ export function resolveArgumentSource(source: string, scope: BindingScope): unknown {
23
+ if (!source) return undefined;
24
+ if (!source.startsWith(componentArgumentSourcePrefix)) return resolveBindingExpression({ path: source }, scope);
25
+
26
+ const reference = source.substring(componentArgumentSourcePrefix.length);
27
+ const componentId = Object.keys(scope.componentOutputs ?? {})
28
+ .filter(id => reference === id || reference.startsWith(`${id}.`))
29
+ .sort((left, right) => right.length - left.length)[0];
30
+ if (componentId === undefined) return undefined;
31
+
32
+ return resolveBindingExpression({ kind: BindingSourceKind.ComponentProperty, componentId, path: reference.substring(componentId.length + 1) }, scope);
33
+ }