@cratis/scene.engine 4.2.1 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/editing/DescriptorCatalog.js +42 -0
- package/dist/cjs/editing/DescriptorCatalog.js.map +1 -0
- package/dist/cjs/editing/EditSession.js +44 -0
- package/dist/cjs/editing/EditSession.js.map +1 -0
- package/dist/cjs/editing/applyEdit.js +83 -0
- package/dist/cjs/editing/applyEdit.js.map +1 -0
- package/dist/cjs/editing/applyEffectiveConfiguration.js +62 -0
- package/dist/cjs/editing/applyEffectiveConfiguration.js.map +1 -0
- package/dist/cjs/editing/collectComponents.js +34 -0
- package/dist/cjs/editing/collectComponents.js.map +1 -0
- package/dist/cjs/editing/describeNode.js +63 -0
- package/dist/cjs/editing/describeNode.js.map +1 -0
- package/dist/cjs/editing/diagnostics.js +36 -0
- package/dist/cjs/editing/diagnostics.js.map +1 -0
- package/dist/cjs/editing/documentPaths.js +45 -0
- package/dist/cjs/editing/documentPaths.js.map +1 -0
- package/dist/cjs/editing/exposureEdits.js +106 -0
- package/dist/cjs/editing/exposureEdits.js.map +1 -0
- package/dist/cjs/editing/exposureGrants.js +115 -0
- package/dist/cjs/editing/exposureGrants.js.map +1 -0
- package/dist/cjs/editing/indexDocument.js +400 -0
- package/dist/cjs/editing/indexDocument.js.map +1 -0
- package/dist/cjs/editing/inspect.js +127 -0
- package/dist/cjs/editing/inspect.js.map +1 -0
- package/dist/cjs/editing/instanceEdits.js +255 -0
- package/dist/cjs/editing/instanceEdits.js.map +1 -0
- package/dist/cjs/editing/layoutTypeDescriptors.js +425 -0
- package/dist/cjs/editing/layoutTypeDescriptors.js.map +1 -0
- package/dist/cjs/editing/layoutTypeEdits.js +141 -0
- package/dist/cjs/editing/layoutTypeEdits.js.map +1 -0
- package/dist/cjs/editing/nodeIds.js +115 -0
- package/dist/cjs/editing/nodeIds.js.map +1 -0
- package/dist/cjs/editing/pathAccess.js +53 -0
- package/dist/cjs/editing/pathAccess.js.map +1 -0
- package/dist/cjs/editing/propertyEdits.js +118 -0
- package/dist/cjs/editing/propertyEdits.js.map +1 -0
- package/dist/cjs/editing/resolveEffectiveConfiguration.js +200 -0
- package/dist/cjs/editing/resolveEffectiveConfiguration.js.map +1 -0
- package/dist/cjs/editing/resolveTemplateChain.js +100 -0
- package/dist/cjs/editing/resolveTemplateChain.js.map +1 -0
- package/dist/cjs/editing/structureEdits.js +231 -0
- package/dist/cjs/editing/structureEdits.js.map +1 -0
- package/dist/cjs/editing/validateBinding.js +111 -0
- package/dist/cjs/editing/validateBinding.js.map +1 -0
- package/dist/cjs/editing/validateValue.js +114 -0
- package/dist/cjs/editing/validateValue.js.map +1 -0
- package/dist/cjs/index.js +51 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/editing/DescriptorCatalog.d.ts +9 -0
- package/dist/esm/editing/DescriptorCatalog.d.ts.map +1 -0
- package/dist/esm/editing/DescriptorCatalog.js +38 -0
- package/dist/esm/editing/DescriptorCatalog.js.map +1 -0
- package/dist/esm/editing/DocumentIndex.d.ts +31 -0
- package/dist/esm/editing/DocumentIndex.d.ts.map +1 -0
- package/dist/esm/editing/DocumentIndex.js +2 -0
- package/dist/esm/editing/DocumentIndex.js.map +1 -0
- package/dist/esm/editing/EditOutcome.d.ts +7 -0
- package/dist/esm/editing/EditOutcome.d.ts.map +1 -0
- package/dist/esm/editing/EditOutcome.js +2 -0
- package/dist/esm/editing/EditOutcome.js.map +1 -0
- package/dist/esm/editing/EditSession.d.ts +20 -0
- package/dist/esm/editing/EditSession.d.ts.map +1 -0
- package/dist/esm/editing/EditSession.js +42 -0
- package/dist/esm/editing/EditSession.js.map +1 -0
- package/dist/esm/editing/EditingContext.d.ts +8 -0
- package/dist/esm/editing/EditingContext.d.ts.map +1 -0
- package/dist/esm/editing/EditingContext.js +2 -0
- package/dist/esm/editing/EditingContext.js.map +1 -0
- package/dist/esm/editing/TemplateChain.d.ts +13 -0
- package/dist/esm/editing/TemplateChain.d.ts.map +1 -0
- package/dist/esm/editing/TemplateChain.js +2 -0
- package/dist/esm/editing/TemplateChain.js.map +1 -0
- package/dist/esm/editing/applyEdit.d.ts +5 -0
- package/dist/esm/editing/applyEdit.d.ts.map +1 -0
- package/dist/esm/editing/applyEdit.js +81 -0
- package/dist/esm/editing/applyEdit.js.map +1 -0
- package/dist/esm/editing/applyEffectiveConfiguration.d.ts +3 -0
- package/dist/esm/editing/applyEffectiveConfiguration.d.ts.map +1 -0
- package/dist/esm/editing/applyEffectiveConfiguration.js +60 -0
- package/dist/esm/editing/applyEffectiveConfiguration.js.map +1 -0
- package/dist/esm/editing/collectComponents.d.ts +3 -0
- package/dist/esm/editing/collectComponents.d.ts.map +1 -0
- package/dist/esm/editing/collectComponents.js +32 -0
- package/dist/esm/editing/collectComponents.js.map +1 -0
- package/dist/esm/editing/describeNode.d.ts +11 -0
- package/dist/esm/editing/describeNode.d.ts.map +1 -0
- package/dist/esm/editing/describeNode.js +60 -0
- package/dist/esm/editing/describeNode.js.map +1 -0
- package/dist/esm/editing/diagnostics.d.ts +6 -0
- package/dist/esm/editing/diagnostics.d.ts.map +1 -0
- package/dist/esm/editing/diagnostics.js +32 -0
- package/dist/esm/editing/diagnostics.js.map +1 -0
- package/dist/esm/editing/documentPaths.d.ts +8 -0
- package/dist/esm/editing/documentPaths.d.ts.map +1 -0
- package/dist/esm/editing/documentPaths.js +41 -0
- package/dist/esm/editing/documentPaths.js.map +1 -0
- package/dist/esm/editing/exposureEdits.d.ts +5 -0
- package/dist/esm/editing/exposureEdits.d.ts.map +1 -0
- package/dist/esm/editing/exposureEdits.js +103 -0
- package/dist/esm/editing/exposureEdits.js.map +1 -0
- package/dist/esm/editing/exposureGrants.d.ts +21 -0
- package/dist/esm/editing/exposureGrants.d.ts.map +1 -0
- package/dist/esm/editing/exposureGrants.js +112 -0
- package/dist/esm/editing/exposureGrants.js.map +1 -0
- package/dist/esm/editing/given/a_layout_document.d.ts +14 -0
- package/dist/esm/editing/given/a_layout_document.d.ts.map +1 -0
- package/dist/esm/editing/given/a_layout_document.js +67 -0
- package/dist/esm/editing/given/a_layout_document.js.map +1 -0
- package/dist/esm/editing/given/a_scene_document.d.ts +20 -0
- package/dist/esm/editing/given/a_scene_document.d.ts.map +1 -0
- package/dist/esm/editing/given/a_scene_document.js +110 -0
- package/dist/esm/editing/given/a_scene_document.js.map +1 -0
- package/dist/esm/editing/index.d.ts +19 -0
- package/dist/esm/editing/index.d.ts.map +1 -0
- package/dist/esm/editing/index.js +19 -0
- package/dist/esm/editing/index.js.map +1 -0
- package/dist/esm/editing/indexDocument.d.ts +6 -0
- package/dist/esm/editing/indexDocument.d.ts.map +1 -0
- package/dist/esm/editing/indexDocument.js +396 -0
- package/dist/esm/editing/indexDocument.js.map +1 -0
- package/dist/esm/editing/inspect.d.ts +8 -0
- package/dist/esm/editing/inspect.d.ts.map +1 -0
- package/dist/esm/editing/inspect.js +124 -0
- package/dist/esm/editing/inspect.js.map +1 -0
- package/dist/esm/editing/instanceEdits.d.ts +9 -0
- package/dist/esm/editing/instanceEdits.d.ts.map +1 -0
- package/dist/esm/editing/instanceEdits.js +248 -0
- package/dist/esm/editing/instanceEdits.js.map +1 -0
- package/dist/esm/editing/layoutTypeDescriptors.d.ts +5 -0
- package/dist/esm/editing/layoutTypeDescriptors.d.ts.map +1 -0
- package/dist/esm/editing/layoutTypeDescriptors.js +421 -0
- package/dist/esm/editing/layoutTypeDescriptors.js.map +1 -0
- package/dist/esm/editing/layoutTypeEdits.d.ts +4 -0
- package/dist/esm/editing/layoutTypeEdits.d.ts.map +1 -0
- package/dist/esm/editing/layoutTypeEdits.js +139 -0
- package/dist/esm/editing/layoutTypeEdits.js.map +1 -0
- package/dist/esm/editing/nodeIds.d.ts +14 -0
- package/dist/esm/editing/nodeIds.d.ts.map +1 -0
- package/dist/esm/editing/nodeIds.js +102 -0
- package/dist/esm/editing/nodeIds.js.map +1 -0
- package/dist/esm/editing/pathAccess.d.ts +7 -0
- package/dist/esm/editing/pathAccess.d.ts.map +1 -0
- package/dist/esm/editing/pathAccess.js +48 -0
- package/dist/esm/editing/pathAccess.js.map +1 -0
- package/dist/esm/editing/propertyEdits.d.ts +15 -0
- package/dist/esm/editing/propertyEdits.d.ts.map +1 -0
- package/dist/esm/editing/propertyEdits.js +113 -0
- package/dist/esm/editing/propertyEdits.js.map +1 -0
- package/dist/esm/editing/resolveEffectiveConfiguration.d.ts +5 -0
- package/dist/esm/editing/resolveEffectiveConfiguration.d.ts.map +1 -0
- package/dist/esm/editing/resolveEffectiveConfiguration.js +198 -0
- package/dist/esm/editing/resolveEffectiveConfiguration.js.map +1 -0
- package/dist/esm/editing/resolveTemplateChain.d.ts +4 -0
- package/dist/esm/editing/resolveTemplateChain.d.ts.map +1 -0
- package/dist/esm/editing/resolveTemplateChain.js +98 -0
- package/dist/esm/editing/resolveTemplateChain.js.map +1 -0
- package/dist/esm/editing/structureEdits.d.ts +6 -0
- package/dist/esm/editing/structureEdits.d.ts.map +1 -0
- package/dist/esm/editing/structureEdits.js +227 -0
- package/dist/esm/editing/structureEdits.js.map +1 -0
- package/dist/esm/editing/validateBinding.d.ts +7 -0
- package/dist/esm/editing/validateBinding.d.ts.map +1 -0
- package/dist/esm/editing/validateBinding.js +108 -0
- package/dist/esm/editing/validateBinding.js.map +1 -0
- package/dist/esm/editing/validateValue.d.ts +8 -0
- package/dist/esm/editing/validateValue.d.ts.map +1 -0
- package/dist/esm/editing/validateValue.js +111 -0
- package/dist/esm/editing/validateValue.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +14 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/editing/DescriptorCatalog.ts +51 -0
- package/editing/DocumentIndex.ts +73 -0
- package/editing/EditOutcome.ts +21 -0
- package/editing/EditSession.ts +50 -0
- package/editing/EditingContext.ts +23 -0
- package/editing/TemplateChain.ts +36 -0
- package/editing/applyEdit.ts +69 -0
- package/editing/applyEffectiveConfiguration.ts +53 -0
- package/editing/collectComponents.ts +36 -0
- package/editing/describeNode.ts +61 -0
- package/editing/diagnostics.ts +30 -0
- package/editing/documentPaths.ts +40 -0
- package/editing/exposureEdits.ts +119 -0
- package/editing/exposureGrants.ts +159 -0
- package/editing/given/a_layout_document.ts +88 -0
- package/editing/given/a_scene_document.ts +135 -0
- package/editing/index.ts +21 -0
- package/editing/indexDocument.ts +237 -0
- package/editing/inspect.ts +142 -0
- package/editing/instanceEdits.ts +243 -0
- package/editing/layoutTypeDescriptors.ts +191 -0
- package/editing/layoutTypeEdits.ts +135 -0
- package/editing/nodeIds.ts +111 -0
- package/editing/pathAccess.ts +81 -0
- package/editing/propertyEdits.ts +119 -0
- package/editing/resolveEffectiveConfiguration.ts +202 -0
- package/editing/resolveTemplateChain.ts +109 -0
- package/editing/structureEdits.ts +234 -0
- package/editing/validateBinding.ts +103 -0
- package/editing/validateValue.ts +130 -0
- package/index.ts +1 -0
- package/package.json +3 -3
|
@@ -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
|
+
}
|
package/editing/index.ts
ADDED
|
@@ -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
|
+
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';
|