@cratis/scene.engine 4.3.0 → 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.
- package/dist/cjs/editing/collectIconUsages.js +86 -0
- package/dist/cjs/editing/collectIconUsages.js.map +1 -0
- package/dist/cjs/editing/diagnoseIcons.js +67 -0
- package/dist/cjs/editing/diagnoseIcons.js.map +1 -0
- package/dist/cjs/editing/iconDiagnostics.js +103 -0
- package/dist/cjs/editing/iconDiagnostics.js.map +1 -0
- package/dist/cjs/editing/inspect.js +28 -1
- package/dist/cjs/editing/inspect.js.map +1 -1
- package/dist/cjs/editing/instanceEdits.js +15 -3
- package/dist/cjs/editing/instanceEdits.js.map +1 -1
- package/dist/cjs/editing/propertyEdits.js +7 -0
- package/dist/cjs/editing/propertyEdits.js.map +1 -1
- package/dist/cjs/icons/EffectiveIconCatalog.js +212 -0
- package/dist/cjs/icons/EffectiveIconCatalog.js.map +1 -0
- package/dist/cjs/icons/EffectiveIconEntry.js +22 -0
- package/dist/cjs/icons/EffectiveIconEntry.js.map +1 -0
- package/dist/cjs/icons/analyzeIconImpact.js +48 -0
- package/dist/cjs/icons/analyzeIconImpact.js.map +1 -0
- package/dist/cjs/icons/matchesIconQuery.js +22 -0
- package/dist/cjs/icons/matchesIconQuery.js.map +1 -0
- package/dist/cjs/icons/resolveIconLibraries.js +45 -0
- package/dist/cjs/icons/resolveIconLibraries.js.map +1 -0
- package/dist/cjs/index.js +16 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/editing/EditingContext.d.ts +2 -0
- package/dist/esm/editing/EditingContext.d.ts.map +1 -1
- package/dist/esm/editing/collectIconUsages.d.ts +5 -0
- package/dist/esm/editing/collectIconUsages.d.ts.map +1 -0
- package/dist/esm/editing/collectIconUsages.js +84 -0
- package/dist/esm/editing/collectIconUsages.js.map +1 -0
- package/dist/esm/editing/diagnoseIcons.d.ts +4 -0
- package/dist/esm/editing/diagnoseIcons.d.ts.map +1 -0
- package/dist/esm/editing/diagnoseIcons.js +65 -0
- package/dist/esm/editing/diagnoseIcons.js.map +1 -0
- package/dist/esm/editing/iconDiagnostics.d.ts +11 -0
- package/dist/esm/editing/iconDiagnostics.d.ts.map +1 -0
- package/dist/esm/editing/iconDiagnostics.js +99 -0
- package/dist/esm/editing/iconDiagnostics.js.map +1 -0
- package/dist/esm/editing/index.d.ts +3 -0
- package/dist/esm/editing/index.d.ts.map +1 -1
- package/dist/esm/editing/index.js +3 -0
- package/dist/esm/editing/index.js.map +1 -1
- package/dist/esm/editing/inspect.d.ts.map +1 -1
- package/dist/esm/editing/inspect.js +29 -2
- package/dist/esm/editing/inspect.js.map +1 -1
- package/dist/esm/editing/instanceEdits.d.ts.map +1 -1
- package/dist/esm/editing/instanceEdits.js +17 -5
- package/dist/esm/editing/instanceEdits.js.map +1 -1
- package/dist/esm/editing/propertyEdits.d.ts.map +1 -1
- package/dist/esm/editing/propertyEdits.js +9 -2
- package/dist/esm/editing/propertyEdits.js.map +1 -1
- package/dist/esm/icons/EffectiveIconCatalog.d.ts +25 -0
- package/dist/esm/icons/EffectiveIconCatalog.d.ts.map +1 -0
- package/dist/esm/icons/EffectiveIconCatalog.js +210 -0
- package/dist/esm/icons/EffectiveIconCatalog.js.map +1 -0
- package/dist/esm/icons/EffectiveIconEntry.d.ts +9 -0
- package/dist/esm/icons/EffectiveIconEntry.d.ts.map +1 -0
- package/dist/esm/icons/EffectiveIconEntry.js +20 -0
- package/dist/esm/icons/EffectiveIconEntry.js.map +1 -0
- package/dist/esm/icons/IconCatalogSource.d.ts +6 -0
- package/dist/esm/icons/IconCatalogSource.d.ts.map +1 -0
- package/dist/esm/icons/IconCatalogSource.js +2 -0
- package/dist/esm/icons/IconCatalogSource.js.map +1 -0
- package/dist/esm/icons/IconDiagnostic.d.ts +9 -0
- package/dist/esm/icons/IconDiagnostic.d.ts.map +1 -0
- package/dist/esm/icons/IconDiagnostic.js +2 -0
- package/dist/esm/icons/IconDiagnostic.js.map +1 -0
- package/dist/esm/icons/IconImpactReport.d.ts +18 -0
- package/dist/esm/icons/IconImpactReport.d.ts.map +1 -0
- package/dist/esm/icons/IconImpactReport.js +2 -0
- package/dist/esm/icons/IconImpactReport.js.map +1 -0
- package/dist/esm/icons/IconLibraryResolution.d.ts +7 -0
- package/dist/esm/icons/IconLibraryResolution.d.ts.map +1 -0
- package/dist/esm/icons/IconLibraryResolution.js +2 -0
- package/dist/esm/icons/IconLibraryResolution.js.map +1 -0
- package/dist/esm/icons/IconQuery.d.ts +7 -0
- package/dist/esm/icons/IconQuery.d.ts.map +1 -0
- package/dist/esm/icons/IconQuery.js +2 -0
- package/dist/esm/icons/IconQuery.js.map +1 -0
- package/dist/esm/icons/IconResolution.d.ts +14 -0
- package/dist/esm/icons/IconResolution.d.ts.map +1 -0
- package/dist/esm/icons/IconResolution.js +2 -0
- package/dist/esm/icons/IconResolution.js.map +1 -0
- package/dist/esm/icons/IconSearchResult.d.ts +7 -0
- package/dist/esm/icons/IconSearchResult.d.ts.map +1 -0
- package/dist/esm/icons/IconSearchResult.js +2 -0
- package/dist/esm/icons/IconSearchResult.js.map +1 -0
- package/dist/esm/icons/IconUsage.d.ts +6 -0
- package/dist/esm/icons/IconUsage.d.ts.map +1 -0
- package/dist/esm/icons/IconUsage.js +2 -0
- package/dist/esm/icons/IconUsage.js.map +1 -0
- package/dist/esm/icons/ResolvedIconLibrary.d.ts +8 -0
- package/dist/esm/icons/ResolvedIconLibrary.d.ts.map +1 -0
- package/dist/esm/icons/ResolvedIconLibrary.js +2 -0
- package/dist/esm/icons/ResolvedIconLibrary.js.map +1 -0
- package/dist/esm/icons/analyzeIconImpact.d.ts +5 -0
- package/dist/esm/icons/analyzeIconImpact.d.ts.map +1 -0
- package/dist/esm/icons/analyzeIconImpact.js +46 -0
- package/dist/esm/icons/analyzeIconImpact.js.map +1 -0
- package/dist/esm/icons/index.d.ts +14 -0
- package/dist/esm/icons/index.d.ts.map +1 -0
- package/dist/esm/icons/index.js +14 -0
- package/dist/esm/icons/index.js.map +1 -0
- package/dist/esm/icons/matchesIconQuery.d.ts +4 -0
- package/dist/esm/icons/matchesIconQuery.d.ts.map +1 -0
- package/dist/esm/icons/matchesIconQuery.js +20 -0
- package/dist/esm/icons/matchesIconQuery.js.map +1 -0
- package/dist/esm/icons/resolveIconLibraries.d.ts +4 -0
- package/dist/esm/icons/resolveIconLibraries.d.ts.map +1 -0
- package/dist/esm/icons/resolveIconLibraries.js +43 -0
- package/dist/esm/icons/resolveIconLibraries.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 +7 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/editing/EditingContext.ts +11 -0
- package/editing/collectIconUsages.ts +78 -0
- package/editing/diagnoseIcons.ts +60 -0
- package/editing/iconDiagnostics.ts +90 -0
- package/editing/index.ts +3 -0
- package/editing/inspect.ts +25 -2
- package/editing/instanceEdits.ts +13 -5
- package/editing/propertyEdits.ts +10 -2
- package/icons/EffectiveIconCatalog.ts +199 -0
- package/icons/EffectiveIconEntry.ts +36 -0
- package/icons/IconCatalogSource.ts +23 -0
- package/icons/IconDiagnostic.ts +45 -0
- package/icons/IconImpactReport.ts +63 -0
- package/icons/IconLibraryResolution.ts +22 -0
- package/icons/IconQuery.ts +27 -0
- package/icons/IconResolution.ts +25 -0
- package/icons/IconSearchResult.ts +21 -0
- package/icons/IconUsage.ts +21 -0
- package/icons/ResolvedIconLibrary.ts +32 -0
- package/icons/analyzeIconImpact.ts +41 -0
- package/icons/index.ts +16 -0
- package/icons/matchesIconQuery.ts +19 -0
- package/icons/resolveIconLibraries.ts +47 -0
- package/index.ts +1 -0
- package/package.json +3 -3
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import { EditingScope, QueryCandidate } from '@cratis/scene.model';
|
|
5
|
+
import { EffectiveIconCatalog } from '../icons/EffectiveIconCatalog';
|
|
5
6
|
import { DescriptorCatalog } from './DescriptorCatalog';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -20,4 +21,14 @@ export interface EditingContext {
|
|
|
20
21
|
* edited it is checked against these; without them it is checked for shape only.
|
|
21
22
|
*/
|
|
22
23
|
queryCandidates?: QueryCandidate[];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The icons the profile can use. When present, an icon value that is edited is checked against it - a
|
|
27
|
+
* library the profile lacks, an icon the library no longer has, a library at an incompatible version -
|
|
28
|
+
* and inspection reports the same problems for values already stored, which are kept either way.
|
|
29
|
+
* Without it an icon value is checked for shape only. An edit is applied synchronously, so
|
|
30
|
+
* `await iconCatalog.load()` first for definite answers about icons and variants; until a library's
|
|
31
|
+
* catalog is loaded its icons are reported as not verified.
|
|
32
|
+
*/
|
|
33
|
+
iconCatalog?: EffectiveIconCatalog;
|
|
23
34
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { InstanceContribution, PropertyDescriptor, PropertyValueType, SceneDocument } from '@cratis/scene.model';
|
|
5
|
+
import { IconUsage } from '../icons/IconUsage';
|
|
6
|
+
import { collectComponents } from './collectComponents';
|
|
7
|
+
import { DescriptorCatalog, findComponentDescriptor } from './DescriptorCatalog';
|
|
8
|
+
import { getValueAtPath } from './pathAccess';
|
|
9
|
+
|
|
10
|
+
function iconFields(property: PropertyDescriptor): string[] {
|
|
11
|
+
return (property.item?.properties ?? []).filter((field) => field.valueType === PropertyValueType.Icon).map((field) => field.path);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function usagesOf(property: PropertyDescriptor, value: unknown, items: { id: string; values: Record<string, unknown> }[] | undefined, location: string): IconUsage[] {
|
|
15
|
+
if (property.valueType === PropertyValueType.Icon) {
|
|
16
|
+
return value === undefined ? [] : [{ value: value as IconUsage['value'], location: `${location}:${property.path}` }];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return property.valueType === PropertyValueType.Collection
|
|
20
|
+
? (items ?? []).flatMap((item) =>
|
|
21
|
+
iconFields(property)
|
|
22
|
+
.filter((field) => item.values[field] !== undefined)
|
|
23
|
+
.map((field) => ({ value: item.values[field] as IconUsage['value'], location: `${location}:${property.path}:${item.id}:${field}` })))
|
|
24
|
+
: [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ownerItems(value: unknown): { id: string; values: Record<string, unknown> }[] {
|
|
28
|
+
return (Array.isArray(value) ? value : [])
|
|
29
|
+
.filter((item): item is Record<string, unknown> => item !== null && typeof item === 'object')
|
|
30
|
+
.map(({ id, ...values }) => ({ id: String(id), values }));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Finds every icon value stored anywhere in a document - the values components carry themselves, and the values
|
|
35
|
+
* and collection-item icon fields that template instances contributed - as the `IconUsage`s that
|
|
36
|
+
* `analyzeIconImpact` takes, so "what would removing this library break" can be asked of a whole document.
|
|
37
|
+
*
|
|
38
|
+
* Each usage's `location` reads `<owner or instance>:<component>:<path>[:<itemId>:<field>]`, where the first part is
|
|
39
|
+
* the contributing instance id (`screen:Orders`) for a contributed value and the name of the owning layout or
|
|
40
|
+
* template for a value the component carries. Values are read as stored; none are changed.
|
|
41
|
+
*
|
|
42
|
+
* @param document The document to read.
|
|
43
|
+
* @param catalog The descriptors saying which properties are icons.
|
|
44
|
+
*/
|
|
45
|
+
export function collectIconUsages(document: SceneDocument, catalog: DescriptorCatalog): IconUsage[] {
|
|
46
|
+
const usages: IconUsage[] = [];
|
|
47
|
+
const componentNames = new Map<string, string>();
|
|
48
|
+
|
|
49
|
+
const owners: { name: string; value: unknown }[] = [
|
|
50
|
+
...document.layouts.map((layout) => ({ name: layout.name, value: layout })),
|
|
51
|
+
...document.screenTemplates.map((template) => ({ name: template.name, value: template })),
|
|
52
|
+
...document.dialogTemplates.map((template) => ({ name: template.name, value: template })),
|
|
53
|
+
...document.screens.map((screen) => ({ name: screen.name, value: screen })),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
for (const owner of owners) {
|
|
57
|
+
for (const component of collectComponents(owner.value)) {
|
|
58
|
+
if (!componentNames.has(component.id)) componentNames.set(component.id, component.componentName);
|
|
59
|
+
|
|
60
|
+
for (const property of findComponentDescriptor(catalog, component.componentName)?.properties ?? []) {
|
|
61
|
+
const value = getValueAtPath(component.properties, property.path);
|
|
62
|
+
usages.push(...usagesOf(property, value, ownerItems(value), `${owner.name}:${component.id}`));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const contribution of document.instanceContributions) {
|
|
68
|
+
const componentName = componentNames.get(contribution.component);
|
|
69
|
+
const property = componentName === undefined ? undefined : findComponentDescriptor(catalog, componentName)?.properties.find((candidate) => candidate.path === contribution.path);
|
|
70
|
+
if (property) usages.push(...contributedUsages(property, contribution));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return usages;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function contributedUsages(property: PropertyDescriptor, contribution: InstanceContribution): IconUsage[] {
|
|
77
|
+
return usagesOf(property, contribution.value, contribution.items, `${contribution.instance}:${contribution.component}`);
|
|
78
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
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 { DiagnosticSeverity, PropertyDescriptor, PropertyValueType, SceneDiagnostic, SceneDocument } from '@cratis/scene.model';
|
|
5
|
+
import { findComponentDescriptor } from './DescriptorCatalog';
|
|
6
|
+
import { EditingContext } from './EditingContext';
|
|
7
|
+
import { iconProblemsOfItems, iconProblemsOfProperty } from './iconDiagnostics';
|
|
8
|
+
import { getValueAtPath } from './pathAccess';
|
|
9
|
+
import { resolveEffectiveConfiguration } from './resolveEffectiveConfiguration';
|
|
10
|
+
import { resolveTemplateChain } from './resolveTemplateChain';
|
|
11
|
+
|
|
12
|
+
function isIconBearing(property: PropertyDescriptor): boolean {
|
|
13
|
+
return property.valueType === PropertyValueType.Icon
|
|
14
|
+
|| (property.valueType === PropertyValueType.Collection && (property.item?.properties ?? []).some((field) => field.valueType === PropertyValueType.Icon));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Reports every icon in what the editing scope renders - the template chain's own values and what each instance
|
|
19
|
+
* contributed - that the profile's icon catalog cannot supply: a library the profile lacks, an icon or variant a
|
|
20
|
+
* library no longer has, a library at an incompatible version.
|
|
21
|
+
*
|
|
22
|
+
* It reports; it never changes anything. The offending values stay in the document, still applied (a renderer
|
|
23
|
+
* shows its fallback), and apply again as soon as the icon is available, so removing a library is reversible.
|
|
24
|
+
* Each diagnostic names the component, property, collection item and - for a contributed value - the instance.
|
|
25
|
+
*
|
|
26
|
+
* Returns nothing when the context carries no `iconCatalog`. Load the catalogs first (`iconCatalog.load()`) for
|
|
27
|
+
* definite answers; an icon from an unloaded library is reported as not verified.
|
|
28
|
+
*/
|
|
29
|
+
export function diagnoseIcons(document: SceneDocument, context: EditingContext): SceneDiagnostic[] {
|
|
30
|
+
const iconCatalog = context.iconCatalog;
|
|
31
|
+
if (!iconCatalog) return [];
|
|
32
|
+
|
|
33
|
+
const chain = resolveTemplateChain(document, context.scope);
|
|
34
|
+
const configuration = resolveEffectiveConfiguration(chain, document.instanceContributions, context.catalog);
|
|
35
|
+
const diagnostics: SceneDiagnostic[] = [];
|
|
36
|
+
|
|
37
|
+
for (const level of chain.levels) {
|
|
38
|
+
for (const element of level.elements) {
|
|
39
|
+
const descriptor = findComponentDescriptor(context.catalog, element.componentName);
|
|
40
|
+
const configured = configuration.components.find((candidate) => candidate.component === element.id);
|
|
41
|
+
|
|
42
|
+
for (const property of (descriptor?.properties ?? []).filter(isIconBearing)) {
|
|
43
|
+
const effective = configured?.values.find((candidate) => candidate.path === property.path);
|
|
44
|
+
const where = { component: element.id, ...(effective?.contributedBy === undefined ? {} : { instance: effective.contributedBy }) };
|
|
45
|
+
|
|
46
|
+
if (property.valueType === PropertyValueType.Icon) {
|
|
47
|
+
const value = effective ? effective.value : getValueAtPath(element.properties, property.path);
|
|
48
|
+
diagnostics.push(...iconProblemsOfProperty(iconCatalog, property, value, DiagnosticSeverity.Warning, where));
|
|
49
|
+
} else if (effective?.items) {
|
|
50
|
+
const items = effective.items.map((item) => ({ id: item.id, values: item.values, instance: item.origin === 'owner' ? undefined : item.origin }));
|
|
51
|
+
diagnostics.push(...iconProblemsOfItems(iconCatalog, property.item, items, DiagnosticSeverity.Warning, { component: element.id, path: property.path }));
|
|
52
|
+
} else {
|
|
53
|
+
diagnostics.push(...iconProblemsOfProperty(iconCatalog, property, getValueAtPath(element.properties, property.path), DiagnosticSeverity.Warning, where));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return diagnostics;
|
|
60
|
+
}
|
|
@@ -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
|
+
}
|
package/editing/index.ts
CHANGED
package/editing/inspect.ts
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
DiagnosticCode, EditTarget, InspectedCollectionItem, InspectedProperty, NodeInspection,
|
|
5
|
+
DiagnosticCode, DiagnosticSeverity, EditTarget, InspectedCollectionItem, InspectedProperty, NodeInspection,
|
|
6
6
|
NodeProvenance, ProvenanceKind, PropertyDescriptor, PropertyValueType, SceneDiagnostic, SceneDocument, SceneNodeKind, ValueSource,
|
|
7
7
|
} from '@cratis/scene.model';
|
|
8
8
|
import { describeNode, readNodeValue } from './describeNode';
|
|
9
|
+
import { EffectiveIconCatalog } from '../icons/EffectiveIconCatalog';
|
|
9
10
|
import { EditingContext } from './EditingContext';
|
|
10
11
|
import { computeExposureGrants, ExposureGrant, grantKey } from './exposureGrants';
|
|
12
|
+
import { iconProblemsOfItems, iconProblemsOfProperty } from './iconDiagnostics';
|
|
11
13
|
import { indexDocument } from './indexDocument';
|
|
12
14
|
import { NodeRecord } from './DocumentIndex';
|
|
13
15
|
import { resolveEffectiveConfiguration } from './resolveEffectiveConfiguration';
|
|
@@ -90,6 +92,22 @@ function inspectProperty(
|
|
|
90
92
|
return inspected;
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Reports icons the profile cannot supply - on the node itself and in configuration contributed to it. The values
|
|
97
|
+
* stay where they are; these are warnings.
|
|
98
|
+
*/
|
|
99
|
+
function iconDiagnosticsOf(properties: InspectedProperty[], iconCatalog: EffectiveIconCatalog, nodeId: string): SceneDiagnostic[] {
|
|
100
|
+
return properties.flatMap(property => {
|
|
101
|
+
const descriptor = property.descriptor;
|
|
102
|
+
if (descriptor.valueType !== PropertyValueType.Collection) {
|
|
103
|
+
return iconProblemsOfProperty(iconCatalog, descriptor, property.effectiveValue, DiagnosticSeverity.Warning, { nodeId });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const items = (property.items ?? []).map(item => ({ id: item.id, values: item.values, instance: item.origin === 'owner' ? undefined : item.origin }));
|
|
107
|
+
return iconProblemsOfItems(iconCatalog, descriptor.item, items, DiagnosticSeverity.Warning, { nodeId, path: descriptor.path });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
93
111
|
/**
|
|
94
112
|
* Describes one node for an editor: what it is, where it comes from, what can be edited on it - with current and
|
|
95
113
|
* effective values and effective editability - and what can be done with it.
|
|
@@ -127,6 +145,11 @@ export function inspect(document: SceneDocument, nodeId: string, context: Editin
|
|
|
127
145
|
diagnostics.push(...chain.diagnostics.filter(diagnostic => diagnostic.code === DiagnosticCode.UnknownScope));
|
|
128
146
|
}
|
|
129
147
|
|
|
148
|
+
const properties = description.properties.map(descriptor => inspectProperty(descriptor, record, provenance, scopeInstance, scopeGrants, configuredValues));
|
|
149
|
+
if (context.iconCatalog) {
|
|
150
|
+
diagnostics.push(...iconDiagnosticsOf(properties, context.iconCatalog, nodeId));
|
|
151
|
+
}
|
|
152
|
+
|
|
130
153
|
return {
|
|
131
154
|
nodeId,
|
|
132
155
|
kind: record.kind,
|
|
@@ -134,7 +157,7 @@ export function inspect(document: SceneDocument, nodeId: string, context: Editin
|
|
|
134
157
|
label: record.label,
|
|
135
158
|
parentId: record.parentId,
|
|
136
159
|
provenance,
|
|
137
|
-
properties
|
|
160
|
+
properties,
|
|
138
161
|
capabilities: description.capabilities,
|
|
139
162
|
removable: isRemovable(record, provenance),
|
|
140
163
|
diagnostics,
|
package/editing/instanceEdits.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
AddCollectionItemEdit, CollectionOperation, ContributedItem, DiagnosticCode, EditCollectionItemEdit, InstanceContribution,
|
|
5
|
+
AddCollectionItemEdit, CollectionOperation, ContributedItem, DiagnosticCode, DiagnosticSeverity, EditCollectionItemEdit, InstanceContribution,
|
|
6
6
|
PropertyValueType, RemoveCollectionItemEdit, ReorderCollectionItemEdit, ResetInstanceValueEdit, SceneDiagnostic,
|
|
7
7
|
SceneDocument, SetInstanceValueEdit,
|
|
8
8
|
} from '@cratis/scene.model';
|
|
9
|
-
import { errorDiagnostic } from './diagnostics';
|
|
9
|
+
import { errorDiagnostic, hasErrors } from './diagnostics';
|
|
10
|
+
import { iconProblemsOfItems } from './iconDiagnostics';
|
|
10
11
|
import { EditSession } from './EditSession';
|
|
11
12
|
import { ExposureGrant, grantKey } from './exposureGrants';
|
|
12
13
|
import { cloneData } from './pathAccess';
|
|
@@ -66,7 +67,7 @@ function permitted(grant: ExposureGrant, operation: CollectionOperation, instanc
|
|
|
66
67
|
return true;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
function checkFields(grant: ExposureGrant, values: Record<string, unknown>, itemId: string, instance: string, diagnostics: SceneDiagnostic[], requireAll: boolean): boolean {
|
|
70
|
+
function checkFields(session: EditSession, grant: ExposureGrant, values: Record<string, unknown>, itemId: string, instance: string, diagnostics: SceneDiagnostic[], requireAll: boolean): boolean {
|
|
70
71
|
const context = { instance, component: grant.component, path: grant.path, itemId };
|
|
71
72
|
let valid = true;
|
|
72
73
|
|
|
@@ -88,6 +89,13 @@ function checkFields(grant: ExposureGrant, values: Record<string, unknown>, item
|
|
|
88
89
|
valid = false;
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
const icons = session.context.iconCatalog;
|
|
93
|
+
if (icons && valid) {
|
|
94
|
+
const iconProblems = iconProblemsOfItems(icons, itemDescriptor, [{ id: itemId, values }], DiagnosticSeverity.Error, context);
|
|
95
|
+
diagnostics.push(...iconProblems);
|
|
96
|
+
valid = !hasErrors(iconProblems);
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
return valid;
|
|
92
100
|
}
|
|
93
101
|
|
|
@@ -163,7 +171,7 @@ export function applyAddCollectionItem(session: EditSession, edit: AddCollection
|
|
|
163
171
|
return;
|
|
164
172
|
}
|
|
165
173
|
|
|
166
|
-
if (!checkFields(grant, edit.item.values, edit.item.id, instance, diagnostics, true)) return;
|
|
174
|
+
if (!checkFields(session, grant, edit.item.values, edit.item.id, instance, diagnostics, true)) return;
|
|
167
175
|
|
|
168
176
|
const { contribution, items } = ownItems(clone, instance, grant);
|
|
169
177
|
if (edit.index !== undefined && (!Number.isInteger(edit.index) || edit.index < 0 || edit.index > items.length)) {
|
|
@@ -236,7 +244,7 @@ export function applyEditCollectionItem(session: EditSession, edit: EditCollecti
|
|
|
236
244
|
const position = findOwnItem(items, edit.itemId, resolved.instance, resolved.grant, session, diagnostics);
|
|
237
245
|
if (position < 0) return;
|
|
238
246
|
|
|
239
|
-
if (!checkFields(resolved.grant, { [edit.field]: edit.value }, edit.itemId, resolved.instance, diagnostics, false)) return;
|
|
247
|
+
if (!checkFields(session, resolved.grant, { [edit.field]: edit.value }, edit.itemId, resolved.instance, diagnostics, false)) return;
|
|
240
248
|
|
|
241
249
|
const next = items.map((item, index) => index === position ? { ...item, values: { ...item.values, [edit.field]: cloneData(edit.value) } } : item);
|
|
242
250
|
save(clone, resolved.instance, resolved.grant, contribution, next);
|
package/editing/propertyEdits.ts
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
DiagnosticCode, PropertyDescriptor, PropertyValueType, QueryBinding, ResetPropertyEdit, SceneDiagnostic, SceneDocument,
|
|
5
|
+
DiagnosticCode, DiagnosticSeverity, PropertyDescriptor, PropertyValueType, QueryBinding, ResetPropertyEdit, SceneDiagnostic, SceneDocument,
|
|
6
6
|
SceneNodeKind, SetPropertyEdit,
|
|
7
7
|
} from '@cratis/scene.model';
|
|
8
8
|
import { describeNode } from './describeNode';
|
|
9
|
-
import { errorDiagnostic } from './diagnostics';
|
|
9
|
+
import { errorDiagnostic, hasErrors } from './diagnostics';
|
|
10
10
|
import { NodeRecord } from './DocumentIndex';
|
|
11
11
|
import { getAtPath, mirroredPaths } from './documentPaths';
|
|
12
12
|
import { EditSession } from './EditSession';
|
|
13
|
+
import { iconProblemsOfProperty } from './iconDiagnostics';
|
|
13
14
|
import { cloneData, removeValueAtPath, setValueAtPath } from './pathAccess';
|
|
14
15
|
import { validateBinding } from './validateBinding';
|
|
15
16
|
import { validateValue } from './validateValue';
|
|
@@ -75,6 +76,13 @@ export function checkValue(session: EditSession, descriptor: PropertyDescriptor,
|
|
|
75
76
|
return problems.length === 0;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
const icons = session.context.iconCatalog;
|
|
80
|
+
if (icons) {
|
|
81
|
+
const problems = iconProblemsOfProperty(icons, descriptor, value, DiagnosticSeverity.Error, context);
|
|
82
|
+
diagnostics.push(...problems);
|
|
83
|
+
return !hasErrors(problems);
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
return true;
|
|
79
87
|
}
|
|
80
88
|
|
|
@@ -0,0 +1,199 @@
|
|
|
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 { IconEntry, IconReference, formatIconReference } from '@cratis/scene.model';
|
|
5
|
+
import { EffectiveIconEntry, referencesOfEntry } from './EffectiveIconEntry';
|
|
6
|
+
import { IconCatalogSource } from './IconCatalogSource';
|
|
7
|
+
import { IconDiagnostic } from './IconDiagnostic';
|
|
8
|
+
import { IconLibraryResolution } from './IconLibraryResolution';
|
|
9
|
+
import { IconQuery } from './IconQuery';
|
|
10
|
+
import { IconResolution } from './IconResolution';
|
|
11
|
+
import { IconSearchResult } from './IconSearchResult';
|
|
12
|
+
import { ResolvedIconLibrary } from './ResolvedIconLibrary';
|
|
13
|
+
import { matchesIconQuery } from './matchesIconQuery';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The icons a profile can actually use: the active libraries from {@link resolveIconLibraries}, with their
|
|
17
|
+
* catalogs loaded on demand.
|
|
18
|
+
*
|
|
19
|
+
* Opening it costs nothing - no catalog is read until a query or a lookup needs that library, and a
|
|
20
|
+
* query narrowed to one library reads only that one. A catalog is loaded once and kept; a load that
|
|
21
|
+
* fails is not kept, so the next call tries again. Everything that goes wrong is reported as an
|
|
22
|
+
* {@link IconDiagnostic}; nothing is ever answered with a different icon than the one asked for.
|
|
23
|
+
*/
|
|
24
|
+
export class EffectiveIconCatalog {
|
|
25
|
+
readonly #sources: Map<string, IconCatalogSource>;
|
|
26
|
+
readonly #loads = new Map<string, Promise<IconEntry[]>>();
|
|
27
|
+
readonly #loadedEntries = new Map<string, IconEntry[]>();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param resolution The active libraries and the problems found with them.
|
|
31
|
+
* @param sources The catalog source of each library; a library with none is active but reports
|
|
32
|
+
* 'catalog-unavailable' when its icons are needed.
|
|
33
|
+
*/
|
|
34
|
+
constructor(readonly resolution: IconLibraryResolution, sources: IconCatalogSource[]) {
|
|
35
|
+
this.#sources = new Map(sources.map((source) => [source.library, source]));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The active libraries, with provenance.
|
|
40
|
+
*/
|
|
41
|
+
get libraries(): ResolvedIconLibrary[] {
|
|
42
|
+
return this.resolution.libraries;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Problems with the libraries themselves, known without loading any catalog.
|
|
47
|
+
*/
|
|
48
|
+
get diagnostics(): IconDiagnostic[] {
|
|
49
|
+
return this.resolution.diagnostics;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether a library's catalog has been read yet.
|
|
54
|
+
*/
|
|
55
|
+
isLoaded(library: string): boolean {
|
|
56
|
+
return this.#loadedEntries.has(library);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The entries of one library, loading its catalog if this is the first time it is needed.
|
|
61
|
+
* Resolves to `undefined` when the library is not active.
|
|
62
|
+
*
|
|
63
|
+
* @throws Whatever the library's source throws, or an Error when an active library has no source.
|
|
64
|
+
*/
|
|
65
|
+
async entriesOf(library: string): Promise<IconEntry[] | undefined> {
|
|
66
|
+
if (!this.libraries.some((resolved) => resolved.library === library)) return undefined;
|
|
67
|
+
|
|
68
|
+
let load = this.#loads.get(library);
|
|
69
|
+
if (!load) {
|
|
70
|
+
const source = this.#sources.get(library);
|
|
71
|
+
if (!source) throw new Error(`The icon library '${library}' is active but has no catalog source`);
|
|
72
|
+
|
|
73
|
+
load = source.loadEntries();
|
|
74
|
+
this.#loads.set(library, load);
|
|
75
|
+
load.then(
|
|
76
|
+
(entries) => this.#loadedEntries.set(library, entries),
|
|
77
|
+
() => this.#loads.delete(library)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return load;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Searches the catalogs of the libraries the query covers - all of them, or the one it names.
|
|
86
|
+
*/
|
|
87
|
+
async search(query: IconQuery = {}): Promise<IconSearchResult> {
|
|
88
|
+
const covered = this.libraries.filter((resolved) => query.library === undefined || resolved.library === query.library);
|
|
89
|
+
const outcomes = await Promise.all(covered.map(async (resolved) => this.#entriesOrDiagnostic(resolved)));
|
|
90
|
+
|
|
91
|
+
const entries: EffectiveIconEntry[] = [];
|
|
92
|
+
const diagnostics: IconDiagnostic[] = [];
|
|
93
|
+
outcomes.forEach((outcome, position) => {
|
|
94
|
+
const library = covered[position];
|
|
95
|
+
if ('diagnostic' in outcome) {
|
|
96
|
+
diagnostics.push(outcome.diagnostic);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const entry of outcome.entries.filter((candidate) => matchesIconQuery(candidate, query))) {
|
|
101
|
+
entries.push({ library, entry, references: referencesOfEntry(library.library, entry) });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return { entries, diagnostics };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Every active library's icons whose name or key is exactly `name` (ignoring case) - the several
|
|
110
|
+
* candidates a bare legacy icon name could have meant. More than one is a real answer, not a tie to
|
|
111
|
+
* break: it is the author's decision which library the icon comes from.
|
|
112
|
+
*/
|
|
113
|
+
async findByName(name: string): Promise<EffectiveIconEntry[]> {
|
|
114
|
+
const wanted = name.trim().toLowerCase();
|
|
115
|
+
const { entries } = await this.search({});
|
|
116
|
+
return entries.filter(({ entry }) => entry.key.toLowerCase() === wanted || entry.name.toLowerCase() === wanted);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The categories in use, sorted, across all libraries or one.
|
|
121
|
+
*/
|
|
122
|
+
async categories(library?: string): Promise<string[]> {
|
|
123
|
+
const { entries } = await this.search({ library });
|
|
124
|
+
return [...new Set(entries.flatMap(({ entry }) => entry.categories))].sort();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Looks a reference up: the icon it names, or exactly why there is none.
|
|
129
|
+
*/
|
|
130
|
+
async resolve(reference: IconReference): Promise<IconResolution> {
|
|
131
|
+
const library = this.libraries.find((resolved) => resolved.library === reference.library);
|
|
132
|
+
if (library && !this.isLoaded(library.library) && !this.#incompatibility(library)) {
|
|
133
|
+
const outcome = await this.#entriesOrDiagnostic(library);
|
|
134
|
+
if ('diagnostic' in outcome) return { isResolved: false, reference, diagnostic: { ...outcome.diagnostic, reference } };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return this.lookup(reference) as IconResolution;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Looks a reference up without loading anything, for callers that cannot wait - an edit is applied
|
|
142
|
+
* synchronously. Resolves everything that is decidable from what is already known: a library that is not
|
|
143
|
+
* active, one at an incompatible version, and - once the library's catalog has been loaded - a missing
|
|
144
|
+
* icon or variant.
|
|
145
|
+
*
|
|
146
|
+
* @returns The resolution, or `undefined` when the library is active but its catalog has not been loaded,
|
|
147
|
+
* so nothing can be said about the icon yet. Call {@link load} first to make the answer definite.
|
|
148
|
+
*/
|
|
149
|
+
lookup(reference: IconReference): IconResolution | undefined {
|
|
150
|
+
const library = this.libraries.find((resolved) => resolved.library === reference.library);
|
|
151
|
+
if (!library) {
|
|
152
|
+
return this.#unresolved(reference, 'missing-library', `The icon library '${reference.library}' is not active in this profile`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const incompatible = this.#incompatibility(library);
|
|
156
|
+
if (incompatible) return { isResolved: false, reference, diagnostic: { ...incompatible, reference } };
|
|
157
|
+
|
|
158
|
+
const entries = this.#loadedEntries.get(library.library);
|
|
159
|
+
if (!entries) return undefined;
|
|
160
|
+
|
|
161
|
+
const entry = entries.find((candidate) => candidate.key === reference.key);
|
|
162
|
+
if (!entry) {
|
|
163
|
+
return this.#unresolved(reference, 'missing-icon', `The icon library '${library.library}' has no icon '${reference.key}'`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (reference.variant !== undefined && !(entry.variants ?? []).includes(reference.variant)) {
|
|
167
|
+
return this.#unresolved(reference, 'missing-variant', `The icon '${formatIconReference({ library: library.library, key: entry.key })}' has no '${reference.variant}' variant`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { isResolved: true, reference, library, entry };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Loads the catalogs of one library, or of every active library, so that {@link lookup} can answer
|
|
175
|
+
* definitely. Libraries whose catalog cannot be loaded are reported, not thrown.
|
|
176
|
+
*/
|
|
177
|
+
async load(library?: string): Promise<IconDiagnostic[]> {
|
|
178
|
+
const covered = this.libraries.filter((resolved) => library === undefined || resolved.library === library);
|
|
179
|
+
const outcomes = await Promise.all(covered.map(async (resolved) => this.#entriesOrDiagnostic(resolved)));
|
|
180
|
+
return outcomes.flatMap((outcome) => ('diagnostic' in outcome ? [outcome.diagnostic] : []));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#incompatibility(library: ResolvedIconLibrary): IconDiagnostic | undefined {
|
|
184
|
+
return this.diagnostics.find((diagnostic) => diagnostic.kind === 'incompatible-version' && diagnostic.library === library.library);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async #entriesOrDiagnostic(library: ResolvedIconLibrary): Promise<{ entries: IconEntry[] } | { diagnostic: IconDiagnostic }> {
|
|
188
|
+
try {
|
|
189
|
+
return { entries: (await this.entriesOf(library.library)) ?? [] };
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
192
|
+
return { diagnostic: { kind: 'catalog-unavailable', library: library.library, message: `The catalog of '${library.library}' could not be loaded: ${reason}` } };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#unresolved(reference: IconReference, kind: IconDiagnostic['kind'], message: string): IconResolution & { isResolved: false } {
|
|
197
|
+
return { isResolved: false, reference, diagnostic: { kind, library: reference.library, message, reference } };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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 { IconEntry, IconReference } from '@cratis/scene.model';
|
|
5
|
+
import { ResolvedIconLibrary } from './ResolvedIconLibrary';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An icon as a picker shows it: the catalog entry, the library that provides it (so provenance and
|
|
9
|
+
* attribution are one step away), and the references that name it.
|
|
10
|
+
*/
|
|
11
|
+
export interface EffectiveIconEntry {
|
|
12
|
+
/**
|
|
13
|
+
* The active library that provides the icon.
|
|
14
|
+
*/
|
|
15
|
+
library: ResolvedIconLibrary;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The catalog entry.
|
|
19
|
+
*/
|
|
20
|
+
entry: IconEntry;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One reference per variant the icon exists in, or the single variant-less reference when it has none.
|
|
24
|
+
*/
|
|
25
|
+
references: IconReference[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The references that name an entry of a library: one per variant, or one without a variant.
|
|
30
|
+
*/
|
|
31
|
+
export function referencesOfEntry(library: string, entry: IconEntry): IconReference[] {
|
|
32
|
+
const variants = entry.variants ?? [];
|
|
33
|
+
return variants.length === 0
|
|
34
|
+
? [{ library, key: entry.key }]
|
|
35
|
+
: variants.map((variant) => ({ library, key: entry.key, variant }));
|
|
36
|
+
}
|