From ddd43157c5d987175693a3e2bf187bb1e421f52f Mon Sep 17 00:00:00 2001 From: Stephan Wienczny Date: Tue, 21 Jun 2022 18:03:56 +0200 Subject: [PATCH] Adapt Scopable and add Scoped, Labelable and Labeled interfaces Adjusts the 'scope' attribute of 'Scopable' to be optional as indicated by the interface name. Adds new 'Scoped', 'Labelable' and 'Labeled' interfaces. Also adds type guards for all mentioned interfaces. Adapts 'composeWithUi' to being able to handle optional scopes via the adapted 'Scopable' interface. The new interfaces and type guards can be used to for example handle unknown UI Schemas in a more convenient fashion. --- MIGRATION.md | 6 +++ packages/core/src/models/uischema.ts | 75 ++++++++++++++++++---------- packages/core/src/util/path.ts | 14 ++++-- packages/core/src/util/runtime.ts | 2 +- packages/core/src/util/util.ts | 16 +++--- 5 files changed, 73 insertions(+), 40 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index f3644a177..bfad9d875 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -104,6 +104,12 @@ There should not be any behavior changes. All React Material class components were refactored to functional components. Please check whether you extended any of our base renderers in your adaptation. +### Scopable interface change + +The `scope` attribute in `Scopable` is now optional. +Use `Scoped` instead for non optional scopes. +The utility function `fromScopable` was renamed to `fromScoped` accordingly. + ### Localization of Date Picker in Angular Material Date Picker in Angular Material will use the global configuration of your Angular Material application. diff --git a/packages/core/src/models/uischema.ts b/packages/core/src/models/uischema.ts index b93897f30..ec0f2e9f0 100644 --- a/packages/core/src/models/uischema.ts +++ b/packages/core/src/models/uischema.ts @@ -27,9 +27,20 @@ import { JsonSchema } from './jsonSchema'; /** * Interface for describing an UI schema element that is referencing - * a subschema. The value of the scope must be a JSON Pointer. + * a subschema. The value of the scope may be a JSON Pointer. */ export interface Scopable { + /** + * The scope that determines to which part this element should be bound to. + */ + scope?: string; +} + +/** + * Interface for describing an UI schema element that is referencing + * a subschema. The value of the scope must be a JSON Pointer. + */ +export interface Scoped extends Scopable { /** * The scope that determines to which part this element should be bound to. */ @@ -37,6 +48,23 @@ export interface Scopable { } /** + * Interface for describing an UI schema element that may be labeled. + */ +export interface Lableable { + /** + * Label for UI schema element. + */ + label?: string|T; +} + +/** + * Interface for describing an UI schema element that is labeled. + */ +export interface Labeled extends Lableable { + label: string | T; +} + +/* * Interface for describing an UI schema element that can provide an internationalization base key. * If defined, this key is suffixed to derive applicable message keys for the UI schema element. * For example, such suffixes are `.label` or `.description` to derive the corresponding message keys for a control element. @@ -96,7 +124,7 @@ export interface Condition { /** * A leaf condition. */ -export interface LeafCondition extends Condition, Scopable { +export interface LeafCondition extends Condition, Scoped { type: 'LEAF'; /** @@ -105,7 +133,7 @@ export interface LeafCondition extends Condition, Scopable { expectedValue: any; } -export interface SchemaBasedCondition extends Condition, Scopable { +export interface SchemaBasedCondition extends Condition, Scoped { schema: JsonSchema; } @@ -179,12 +207,8 @@ export interface HorizontalLayout extends Layout { * A group resembles a vertical layout, but additionally might have a label. * This layout is useful when grouping different elements by a certain criteria. */ -export interface GroupLayout extends Layout { +export interface GroupLayout extends Layout, Lableable { type: 'Group'; - /** - * The label of this group layout. - */ - label?: string; } /** @@ -216,23 +240,15 @@ export interface LabelElement extends UISchemaElement { * A control element. The scope property of the control determines * to which part of the schema the control should be bound. */ -export interface ControlElement extends UISchemaElement, Scopable, Internationalizable { +export interface ControlElement extends UISchemaElement, Scoped, Lableable, Internationalizable { type: 'Control'; - /** - * An optional label that will be associated with the control - */ - label?: string | boolean | LabelDescription; } /** * The category layout. */ -export interface Category extends Layout { +export interface Category extends Layout, Labeled { type: 'Category'; - /** - * The label associated with this category layout. - */ - label: string; } /** @@ -240,12 +256,8 @@ export interface Category extends Layout { * A child element may either be itself a Categorization or a Category, hence * the categorization element can be used to represent recursive structures like trees. */ -export interface Categorization extends UISchemaElement { +export interface Categorization extends UISchemaElement, Labeled { type: 'Categorization'; - /** - * The label of this categorization. - */ - label: string; /** * The child elements of this categorization which are either of type * {@link Category} or {@link Categorization}. @@ -253,12 +265,23 @@ export interface Categorization extends UISchemaElement { elements: (Category | Categorization)[]; } -export const isInternationalized = (element: unknown): element is Required => { - return typeof element === 'object' && element !== null && typeof (element as Internationalizable).i18n === 'string'; -} +export const isInternationalized = (element: unknown): element is Required => + typeof element === 'object' && element !== null && typeof (element as Internationalizable).i18n === 'string'; export const isGroup = (layout: Layout): layout is GroupLayout => layout.type === 'Group'; export const isLayout = (uischema: UISchemaElement): uischema is Layout => (uischema as Layout).elements !== undefined; + +export const isScopable = (obj: unknown): obj is Scopable => + obj && typeof obj === 'object'; + +export const isScoped = (obj: unknown): obj is Scoped => + isScopable(obj) && typeof obj.scope === 'string'; + +export const isLabelable = (obj: unknown): obj is Lableable => + obj && typeof obj === 'object'; + +export const isLabeled = (obj: unknown): obj is Labeled => + isLabelable(obj) && ['string', 'object'].includes(typeof obj.label); diff --git a/packages/core/src/util/path.ts b/packages/core/src/util/path.ts index b62ed6afc..e5920cd54 100644 --- a/packages/core/src/util/path.ts +++ b/packages/core/src/util/path.ts @@ -25,7 +25,7 @@ import isEmpty from 'lodash/isEmpty'; import range from 'lodash/range'; -import { Scopable } from '../models'; +import { isScoped, Scopable } from '../models'; export const compose = (path1: string, path2: string) => { let p1 = path1; @@ -81,13 +81,17 @@ export const toDataPath = (schemaPath: string): string => { }; export const composeWithUi = (scopableUi: Scopable, path: string): string => { + if (!isScoped(scopableUi)) { + return path ?? ''; + } + const segments = toDataPathSegments(scopableUi.scope); - if (isEmpty(segments) && path === undefined) { - return ''; + if (isEmpty(segments)) { + return path ?? ''; } - return isEmpty(segments) ? path : compose(path, segments.join('.')); + return compose(path, segments.join('.')); }; /** @@ -99,4 +103,4 @@ export const encode = (segment: string) => segment?.replace(/~/g, '~0').replace( /** * Decodes a given JSON Pointer segment to its "normal" representation */ -export const decode = (pointerSegment: string) => pointerSegment?.replace(/~1/g, '/').replace(/~0/, '~'); \ No newline at end of file +export const decode = (pointerSegment: string) => pointerSegment?.replace(/~1/g, '/').replace(/~0/, '~'); diff --git a/packages/core/src/util/runtime.ts b/packages/core/src/util/runtime.ts index d636f4c70..d6cee0505 100644 --- a/packages/core/src/util/runtime.ts +++ b/packages/core/src/util/runtime.ts @@ -27,6 +27,7 @@ import has from 'lodash/has'; import { AndCondition, Condition, + JsonSchema, LeafCondition, OrCondition, RuleEffect, @@ -39,7 +40,6 @@ import { composeWithUi } from './path'; import Ajv from 'ajv'; import { getAjv } from '../reducers'; import { JsonFormsState } from '../store'; -import { JsonSchema } from '../models/jsonSchema'; const isOrCondition = (condition: Condition): condition is OrCondition => condition.type === 'OR'; diff --git a/packages/core/src/util/util.ts b/packages/core/src/util/util.ts index 1909d1f98..3b445380b 100644 --- a/packages/core/src/util/util.ts +++ b/packages/core/src/util/util.ts @@ -27,7 +27,7 @@ import isEmpty from 'lodash/isEmpty'; import isArray from 'lodash/isArray'; import includes from 'lodash/includes'; import find from 'lodash/find'; -import { JsonSchema, Scopable, UISchemaElement } from '..'; +import { JsonSchema, Scoped, UISchemaElement } from '..'; import { resolveData, resolveSchema } from './resolvers'; import { composePaths, toDataPathSegments } from './path'; import { isEnabled, isVisible } from './runtime'; @@ -56,8 +56,8 @@ export const hasType = (jsonSchema: JsonSchema, expected: string): boolean => { }; /** -* Derives the type of the jsonSchema element -*/ + * Derives the type of the jsonSchema element + */ export const deriveTypes = (jsonSchema: JsonSchema): string[] => { if (isEmpty(jsonSchema)) { return []; @@ -93,8 +93,8 @@ export const deriveTypes = (jsonSchema: JsonSchema): string[] => { }; /** -* Convenience wrapper around resolveData and resolveSchema. -*/ + * Convenience wrapper around resolveData and resolveSchema. + */ export const Resolve: { schema( schema: JsonSchema, @@ -108,18 +108,18 @@ export const Resolve: { }; // Paths -- -const fromScopable = (scopable: Scopable) => +const fromScoped = (scopable: Scoped): string => toDataPathSegments(scopable.scope).join('.'); export const Paths = { compose: composePaths, - fromScopable + fromScoped }; // Runtime -- export const Runtime = { isEnabled(uischema: UISchemaElement, data: any, ajv: Ajv): boolean { - return isEnabled(uischema, data,undefined, ajv); + return isEnabled(uischema, data, undefined, ajv); }, isVisible(uischema: UISchemaElement, data: any, ajv: Ajv): boolean { return isVisible(uischema, data, undefined, ajv);