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);