/** * @module SelfServe/Decorators */ import { ChoiceItem, Description, Info, NumberUiType, OnChangeCallback, RefreshParams } from "./SelfServeTypes"; import { addPropertyToMap, buildSmartUiDescriptor, DecoratorProperties } from "./SelfServeUtils"; type ValueOf = T[keyof T]; interface Decorator { name: keyof DecoratorProperties; value: ValueOf; } interface InputOptionsBase { /** * Key used to pickup the string corresponding to the label of the UI element, from the strings JSON file. */ labelTKey: string; } /** * Numeric input UI element is rendered. The current options are to render it as a slider or a spinner. */ export interface NumberInputOptions extends InputOptionsBase { /** * Min value of the numeric input UI element */ min: (() => Promise) | number; /** * Max value of the numeric input UI element */ max: (() => Promise) | number; /** * Value by which the numeric input is incremented or decremented in the UI. */ step: (() => Promise) | number; /** * The type of the numeric input UI element */ uiType: NumberUiType; } /** * Text box is rendered. */ export interface StringInputOptions extends InputOptionsBase { /** * Key used to pickup the string corresponding to the place holder text of the text box, from the strings JSON file. */ placeholderTKey?: (() => Promise) | string; } /** * Toggle is rendered. */ export interface BooleanInputOptions extends InputOptionsBase { /** * Key used to pickup the string corresponding to the true label of the toggle, from the strings JSON file. */ trueLabelTKey: (() => Promise) | string; /** * Key used to pickup the string corresponding to the false label of the toggle, from the strings JSON file. */ falseLabelTKey: (() => Promise) | string; } /** * Dropdown is rendered. */ export interface ChoiceInputOptions extends InputOptionsBase { /** * Choices to be shown in the dropdown */ choices: (() => Promise) | ChoiceItem[]; /** * Key used to pickup the string corresponding to the placeholder text of the dropdown, from the strings JSON file. */ placeholderTKey?: (() => Promise) | string; } /** * Text is rendered. */ export interface DescriptionDisplayOptions { /** * Optional heading for the text displayed by this description element. */ labelTKey?: string; /** * Static description to be shown as text. */ description?: (() => Promise) | Description; /** * If true, Indicates that the Description will be populated dynamically and that it may not be present in some scenarios. */ isDynamicDescription?: boolean; } /** * Interprets the type of the UI element and correspondingly renders * - slider or spinner * - text box * - toggle * - drop down * - plain text or message bar */ export type InputOptions = | NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions | DescriptionDisplayOptions; const isNumberInputOptions = (inputOptions: InputOptions): inputOptions is NumberInputOptions => { return "min" in inputOptions; }; const isBooleanInputOptions = (inputOptions: InputOptions): inputOptions is BooleanInputOptions => { return "trueLabelTKey" in inputOptions; }; const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is ChoiceInputOptions => { return "choices" in inputOptions; }; const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => { return "description" in inputOptions || "isDynamicDescription" in inputOptions; }; const addToMap = (...decorators: Decorator[]): PropertyDecorator => { return (target, property) => { let className = target.constructor.name; const propertyName = property.toString(); if (className === "Function") { //eslint-disable-next-line @typescript-eslint/ban-types className = (target as Function).name; throw new Error(`Property '${propertyName}' in class '${className}'should be not be static.`); } const propertyType = (Reflect.getMetadata("design:type", target, property)?.name as string)?.toLowerCase(); addPropertyToMap(target, propertyName, className, "type", propertyType); addPropertyToMap(target, propertyName, className, "dataFieldName", propertyName); decorators.map((decorator: Decorator) => addPropertyToMap(target, propertyName, className, decorator.name, decorator.value) ); }; }; /** * Indicates the callback to be fired when the UI element corresponding to the property is changed. */ export const OnChange = (onChange: OnChangeCallback): PropertyDecorator => { return addToMap({ name: "onChange", value: onChange }); }; /** * Indicates that the UI element corresponding to the property should have an Info bubble. The Info * bubble is the icon that looks like an "i" which users click on to get more information about the UI element. */ export const PropertyInfo = (info: (() => Promise) | Info): PropertyDecorator => { return addToMap({ name: "info", value: info }); }; /** * Indicates that this property should correspond to a UI element with the given parameters. */ export const Values = (inputOptions: InputOptions): PropertyDecorator => { if (isNumberInputOptions(inputOptions)) { return addToMap( { name: "labelTKey", value: inputOptions.labelTKey }, { name: "min", value: inputOptions.min }, { name: "max", value: inputOptions.max }, { name: "step", value: inputOptions.step }, { name: "uiType", value: inputOptions.uiType } ); } else if (isBooleanInputOptions(inputOptions)) { return addToMap( { name: "labelTKey", value: inputOptions.labelTKey }, { name: "trueLabelTKey", value: inputOptions.trueLabelTKey }, { name: "falseLabelTKey", value: inputOptions.falseLabelTKey } ); } else if (isChoiceInputOptions(inputOptions)) { return addToMap( { name: "labelTKey", value: inputOptions.labelTKey }, { name: "placeholderTKey", value: inputOptions.placeholderTKey }, { name: "choices", value: inputOptions.choices } ); } else if (isDescriptionDisplayOptions(inputOptions)) { return addToMap( { name: "labelTKey", value: inputOptions.labelTKey }, { name: "description", value: inputOptions.description }, { name: "isDynamicDescription", value: inputOptions.isDynamicDescription } ); } else { return addToMap( { name: "labelTKey", value: inputOptions.labelTKey }, { name: "placeholderTKey", value: inputOptions.placeholderTKey } ); } }; /** * Indicates to the compiler that UI should be generated from this class. */ export const IsDisplayable = (): ClassDecorator => { return (target) => { buildSmartUiDescriptor(target.name, target.prototype); }; }; /** * If there is a long running operation in your page after the {@linkcode onSave} action, the page can * optionally auto refresh itself using the {@linkcode onRefresh} action. The 'RefreshOptions' indicate * how often the auto refresh of the page occurs. */ export const RefreshOptions = (refreshParams: RefreshParams): ClassDecorator => { return (target) => { addPropertyToMap(target.prototype, "root", target.name, "refreshParams", refreshParams); }; };