@velis/dynamicforms
Version:
Data entry boilerplate components and a RESTful API consumer
1 lines • 343 kB
Source Map (JSON)
{"version":3,"file":"dynamicforms.umd.cjs","sources":["../src/components/util/translations-mixin.ts","../src/components/actions/action.ts","../src/components/actions/filtered-actions.ts","../src/components/actions/action-handler-composable.ts","../src/components/classes/display-mode.ts","../src/components/form/namespace.ts","../src/components/modal/definitions/dialog-size.ts","../src/components/form/definitions/field-render-params.ts","../src/components/form/definitions/field.ts","../src/components/form/definitions/layout.ts","../src/components/form/definitions/form-payload.ts","../src/components/modal/dialog-definition.ts","../src/components/modal/modal-renderer.ts","../src/components/modal/modal-view-list.ts","../src/components/modal/modal-view.ts","../src/components/modal/modal-view-api.ts","../src/components/table/definitions/row-types.ts","../src/components/form/inputs/base.ts","../src/components/form/inputs/date-time.vue","../src/components/form/inputs/df-checkbox.vue","../src/components/form/inputs/df-ckeditor.vue","../src/components/form/inputs/df-color.vue","../src/components/util/request-tracker.ts","../src/components/util/api-client.ts","../src/components/form/inputs/df-file.vue","../src/components/form/inputs/df-input.vue","../src/components/api_consumer/component-display.ts","../src/components/util/get-object-from-path.ts","../src/components/table/definitions/column-ordering-direction.ts","../src/components/table/definitions/column-ordering.ts","../src/components/table/definitions/column.ts","../src/components/table/definitions/columns.ts","../src/components/classes/indexed-array.ts","../src/components/table/definitions/row.ts","../src/components/table/definitions/filterrow.ts","../src/components/table/definitions/rows.ts","../src/components/api_consumer/consumer-logic-base.ts","../src/components/api_consumer/form-consumer/base.ts","../src/components/api_consumer/api-consumer.vue","../src/components/util/internal-record.ts","../src/components/adapters/api/view-set-api.ts","../src/components/adapters/index.ts","../src/components/api_consumer/form-consumer/array.ts","../src/components/api_consumer/form-consumer/one-shot/base.ts","../src/components/api_consumer/form-consumer/one-shot/array.ts","../src/components/api_consumer/consumer-logic-array.ts","../src/components/form/inputs/df-list.vue","../src/components/form/inputs/df-placeholder.vue","../src/components/form/inputs/df-select.vue","../src/components/form/inputs/text-area.vue","../src/components/form/form-field.vue","../src/components/table/cell-renderers/table-cell-date-time.vue","../src/components/table/cell-renderers/table-cell-float.vue","../src/components/table/column-group.vue","../src/components/table/ordering-indicator.vue","../src/components/table/render-measure.ts","../src/components/table/tcolumn-generic.vue","../../../node_modules/vuetify/lib/util/helpers.js","../../../node_modules/vuetify/lib/util/getCurrentInstance.js","../../../node_modules/vuetify/lib/composables/display.js","../src/components/actions/action-handler-mixin.ts","../src/components/actions/actions-mixin.ts","../src/components/actions/actions-vuetify.vue","../src/components/form/field-group.vue","../src/components/form/definitions/field-operator.ts","../src/components/form/inputs/conditional-visibility.ts","../src/components/form/row.vue","../src/components/form/layout-vuetify.vue","../src/components/form/form-component.vue","../src/components/modal/modal.ts","../src/components/modal/modal-api-vuetify.vue","../src/components/table/definitions/responsive-layout.ts","../src/components/table/table.ts","../src/components/table/table-style.vue","../src/components/table/rows-visibility-observer.ts","../src/components/table/trow-generic.vue","../src/components/table/tbody-generic.vue","../src/components/table/thead-generic.vue","../src/components/table/table-vuetify.vue","../src/components/notifications/index.ts","../src/components/adapters/api/detail-view-api.ts","../src/components/api_consumer/form-consumer/api.ts","../src/components/api_consumer/form-consumer/one-shot/api.ts","../src/components/api_consumer/consumer-logic-api.ts","../src/components/api_consumer/api-consumer-loader.vue","../src/index.ts"],"sourcesContent":["/**\n * translations mixin provides utility functions that gettext will recognise\n */\n\ntype InterpolateData = any[] | Record<string, any>;\n\ndeclare global {\n interface Window {\n gettext?: (str: string) => string;\n interpolate?: (str: string, data: InterpolateData, named: boolean) => string;\n }\n}\n\nconst gettext = (str: string) => (\n window.gettext ? window.gettext(str) : str\n);\n\nconst interpolate = (str: string, data: Record<string, any>) => (\n window.interpolate ?\n window.interpolate(str, data, true) :\n str.replace(/%\\(\\w+\\)s/g, (match) => String(data[match.slice(2, -2)]))\n);\n\nexport function useTranslations() {\n return { gettext, interpolate };\n}\n\nexport { gettext, interpolate };\n\nexport default {\n methods: {\n gettext(str: string) { return gettext(str); },\n interpolate(str: string, data: { [key: string]: any }) { return interpolate(str, data); },\n },\n};\n","import { camelCase, isBoolean, isObjectLike, isString, toLower, upperFirst } from 'lodash-es';\n\nimport FormPayload from '../form/definitions/form-payload';\nimport { gettext } from '../util/translations-mixin';\n\nimport type { ActionsNS } from './namespace';\n\ntype ActionHandlerExtraData = ActionsNS.ActionHandlerExtraData;\ntype ActionJSON = ActionsNS.ActionJSON;\ntype BreakpointJSON = ActionsNS.BreakpointJSON;\ntype BreakpointsJSON = ActionsNS.BreakpointsJSON;\n\nlet uniqueIdCounter = 0;\nconst ACTION_POSITIONS = [\n 'HEADER', 'ROW_START', 'ROW_END', 'FIELD_START', 'FIELD_END', 'FORM_HEADER', 'FORM_FOOTER',\n 'ROW_CLICK', 'ROW_RIGHT_CLICK', 'VALUE_CHANGED',\n];\n\nexport function defaultActionHandler(\n action: Action,\n payload: FormPayload | undefined,\n extraData: ActionHandlerExtraData,\n) {\n const dialog = extraData?.dialog;\n // console.log('Action execute:', action, payload, extraData);\n if (dialog && dialog.resolvePromise && dialog.close) {\n dialog.resolvePromise({ action, payload, extraData, dialog });\n dialog.close();\n }\n return true;\n}\n\nexport function getActionName(actionName: string | undefined): `action${string}` {\n return `action${upperFirst(camelCase(toLower(actionName)))}`;\n}\n\nclass Action implements ActionJSON {\n public name!: string;\n\n public uniqueId!: number;\n\n public position!: string;\n\n public fieldName!: string | null;\n\n public label?: string;\n\n public labelAvailable!: boolean;\n\n public icon?: string;\n\n public iconAvailable!: boolean;\n\n public displayStyle!: BreakpointsJSON;\n\n public payload!: FormPayload | undefined;\n\n public title?: string;\n\n public extra_data!: Record<string, any>;\n\n [key: `action${string}`]: ActionsNS.ActionHandler;\n\n constructor(data: Action | ActionJSON, payload?: FormPayload) {\n const uniqueId = ++uniqueIdCounter;\n\n const name = data.name;\n const position = data.position;\n\n if (!isString(name) || !/^[a-zA-Z-_]+$/.test(name)) {\n console.warn(\n `Action name must be a valid string, matching [a-zA-Z-_]+. Got ${name}.` +\n ' Impossible to construct handler function name',\n data,\n );\n }\n\n // any non-string or empty string must resolve as null for label\n const label = !isString(data.label) || data.label.length === 0 ? null : data.label;\n // any non-string or empty string must resolve as null for icon\n const icon = !isString(data.icon) || data.icon.length === 0 ? null : data.icon;\n\n // displayStyle is an object of { breakpoint.{ showLabel, showIcon, asButton } } specs detailing how an action\n // should be rendered at various breakpoints. A special case without a breakpoint is allowed specifying the common\n // arrangement\n const displayStyleProps = ['asButton', 'showLabel', 'showIcon'];\n\n function displayStyleBreakpointClean(bp: BreakpointsJSON | undefined) {\n if (bp == null || !isObjectLike(bp)) return null;\n const cbp = displayStyleProps.reduce((res: BreakpointsJSON, p: string) => {\n if (isBoolean(bp[p])) res[p] = bp[p];\n return res;\n }, {});\n return Object.keys(cbp).length === 0 ? null : cbp;\n }\n\n function conditionalAddBreakpoint(ds: BreakpointsJSON, breakpoint: string) {\n if (data.displayStyle == null) return;\n const bpData = displayStyleBreakpointClean(data.displayStyle[breakpoint] as BreakpointJSON);\n if (bpData) ds[breakpoint] = bpData;\n }\n\n const displayStyle = displayStyleBreakpointClean(data.displayStyle) || {};\n // see also actions-mixin.ts about these breakpoints\n conditionalAddBreakpoint(displayStyle, 'xl');\n conditionalAddBreakpoint(displayStyle, 'lg');\n conditionalAddBreakpoint(displayStyle, 'md');\n conditionalAddBreakpoint(displayStyle, 'sm');\n conditionalAddBreakpoint(displayStyle, 'xs');\n\n if (ACTION_POSITIONS.indexOf(position as string) === -1) {\n console.warn(`Action position ${position} not known`, data);\n }\n\n const fieldNameTemp = data instanceof Action ? data.fieldName : data.field_name;\n // any non-string or empty string must resolve as null for fieldName\n const fieldName = !isString(fieldNameTemp) || fieldNameTemp.length === 0 ? null : fieldNameTemp;\n\n const title = !isString(data.title) || data.title.length === 0 ? null : data.title;\n\n const actionName = getActionName(data.name);\n this[actionName] = data[actionName];\n\n Object.defineProperties(this, {\n name: { get() { return name; }, enumerable: true },\n uniqueId: { get() { return uniqueId; }, enumerable: false },\n position: { get() { return position; }, enumerable: true },\n fieldName: { get() { return fieldName; }, enumerable: true },\n\n label: { get() { return label; }, enumerable: true },\n labelAvailable: { get() { return label != null; }, enumerable: true },\n icon: { get() { return icon; }, enumerable: true },\n iconAvailable: { get() { return icon != null; }, enumerable: true },\n displayStyle: { get() { return displayStyle; }, enumerable: true },\n title: { get() { return title; }, enumerable: true },\n extra_data: { get() { return data.extra_data ?? {}; }, enumerable: true },\n\n payload: { get(): FormPayload | undefined { return payload; }, enumerable: true },\n // elementType & bindAttrs, entire action.action concept:\n // action.py && actionsHandler.decorateActions, added by brontes, modified by velis\n // not sure this is necessary: I have only found one instance of action declaration in python and\n // it's just so a dialog can be shown. Doubt it even worked at any time. Also that same thing is now\n // solved via parent components declaring action handler methods\n });\n }\n\n static closeAction(data: ActionJSON = {}) {\n return new Action({\n name: 'close',\n label: gettext('Close'),\n icon: 'close-outline',\n displayStyle: { asButton: true, showLabel: true, showIcon: true },\n position: 'FORM_FOOTER',\n ...data, // any properties in data should overwrite properties in the constant\n });\n }\n\n static yesAction(data: ActionJSON = {}) {\n return new Action({\n name: 'yes',\n label: gettext('Yes'),\n icon: 'thumbs-up-outline',\n displayStyle: { asButton: true, showLabel: true, showIcon: true },\n position: 'FORM_FOOTER',\n ...data, // any properties in data should overwrite properties in the constant\n });\n }\n\n static noAction(data: ActionJSON = {}) {\n return new Action({\n name: 'no',\n label: gettext('No'),\n icon: 'thumbs-down-outline',\n displayStyle: { asButton: true, showLabel: true, showIcon: true },\n position: 'FORM_FOOTER',\n ...data, // any properties in data should overwrite properties in the constant\n });\n }\n}\n\nexport default Action;\n","import type FormPayload from '../form/definitions/form-payload';\n\nimport Action from './action';\nimport type { ActionsNS } from './namespace';\n\ntype ActionCollection = { [key: string]: Action };\n\ninterface FilterCache {\n [key: string]: FilteredActions;\n}\n\nclass FilteredActions {\n public actions: ActionCollection;\n\n private filterCache: FilterCache;\n\n public payload!: FormPayload;\n\n constructor(actions: ActionsNS.ActionsJSON | ActionCollection, payload?: FormPayload) {\n this.actions = Object.values(actions).reduce((res: ActionCollection, action) => {\n if (action == null) return res; // redundant, but typescript complains otherwise\n const name = action.name;\n if (name == null) {\n console.error('Action has no name and will not be added to the list', action);\n return res;\n }\n if (action instanceof Action && (payload === undefined || action.payload === payload)) {\n res[name] = action;\n } else {\n res[name] = new Action(action, payload as FormPayload);\n }\n return res;\n }, {});\n this.filterCache = {}; // contains cached .filter results\n\n // usually payload will be provided in a component higher up, but sometimes actions are standalone and need\n // their own parameters, or maybe they are detached in component hierarchy (like with the dialog). This is when\n // the payload member is populated and then used by action execution plugin\n Object.defineProperties(this, { payload: { get() { return payload; }, enumerable: true } });\n }\n\n get actionsList() {\n return Object.values(this.actions);\n }\n\n get length() {\n return this.actionsList.length;\n }\n\n * [Symbol.iterator]() {\n for (const action of this.actionsList) {\n yield action;\n }\n }\n\n hasAction(action: Action) {\n return this.actionsList.some((a) => a.uniqueId === action.uniqueId);\n }\n\n /**\n * filters actions to include only those rendering at given position\n * @param position\n * @param fieldName\n * @returns {FilteredActions}\n */\n filter(position: string, fieldName: string | null = null) {\n const cacheKey = position + (fieldName ? `|${fieldName}` : '');\n if (this.filterCache[cacheKey] === undefined) {\n // noinspection JSUnresolvedVariable\n this.filterCache[cacheKey] = new FilteredActions(\n Object.values(this.actions)\n .filter((action) => action.position === position && (\n action.fieldName == null || action.fieldName === fieldName))\n .reduce((obj: ActionCollection, item) => {\n obj[item.name] = this.actions[item.name];\n return obj;\n }, {}),\n );\n }\n return this.filterCache[cacheKey];\n }\n\n get header() {\n return this.filter('HEADER');\n }\n\n get rowStart() {\n return this.filter('ROW_START');\n }\n\n get rowEnd() {\n return this.filter('ROW_END');\n }\n\n get rowClick() {\n return this.filter('ROW_CLICK');\n }\n\n get rowRightClick() {\n return this.filter('ROW_RIGHT_CLICK');\n }\n\n fieldAll(fieldName: string) {\n const res = this.filter('FIELD_START', fieldName);\n const add = this.filter('FIELD_END', fieldName);\n const actions = [...res.actionsList, ...add.actionsList];\n // @ts-ignore: yes, we're not sending ActionsJSON here. We're sending Action[].\n // But object.keys.reduce will work on this too and declaring the proper type for it is too much of a hassle\n return new FilteredActions(actions);\n }\n\n fieldStart(fieldName: string) {\n return this.filter('FIELD_START', fieldName);\n }\n\n fieldEnd(fieldName: string) {\n return this.filter('FIELD_END', fieldName);\n }\n\n get formHeader() {\n return this.filter('FORM_HEADER');\n }\n\n get formFooter() {\n return this.filter('FORM_FOOTER');\n }\n\n get valueChanged() {\n return this.filter('VALUE_CHANGED');\n }\n}\n\nexport default FilteredActions;\n","// eslint-disable-next-line max-classes-per-file\nimport { inject, provide } from 'vue';\n\nimport Action, { getActionName } from './action';\nimport FilteredActions from './filtered-actions';\nimport type { ActionsNS } from './namespace';\n\ntype IHandlers = ActionsNS.IHandlers;\ntype Handler = ActionsNS.Handler;\ntype ActionHandlerComposable = ActionsNS.ActionHandlerComposable;\ntype IActionHandler = ActionsNS.IActionHandler;\n\nclass Handlers implements IHandlers {\n [key: string]: Handler;\n}\n\n// eslint-disable-next-line import/prefer-default-export\nexport function useActionHandler(payload?: any, firstToLast: boolean = true): ActionHandlerComposable {\n const parentHandler = inject<IActionHandler | undefined>('actionHandler', undefined);\n const internalPayload = payload ?? inject<any>('payload', {});\n\n class ActionHandlers implements IActionHandler {\n private handlers: Handlers = new Handlers();\n\n register = (actionName: string, handler: Handler) => {\n this.handlers[actionName] = handler;\n return this;\n };\n\n call = async (actions: Action | FilteredActions, context?: any): Promise<boolean> => {\n const recursiveExecuted = await this.recursiveCall(actions, internalPayload.value, context, firstToLast);\n const actionResolved = await this.resolveAction(actions, context);\n return recursiveExecuted || actionResolved;\n };\n\n recursiveCall = async (\n actions: Action | FilteredActions,\n actionPayload?: any,\n context?: any,\n f2L: boolean = true,\n ): Promise<boolean> => {\n if (f2L) {\n return (\n await this.executeHandler(actions, actionPayload, context) ||\n (await parentHandler?.recursiveCall(actions, actionPayload, context, f2L) ?? false)\n );\n }\n return (\n (await parentHandler?.recursiveCall(actions, actionPayload, context, f2L) ?? false) ||\n await this.executeHandler(actions, actionPayload, context)\n );\n };\n\n private executeHandler = async (\n actions: Action | FilteredActions,\n actionPayload: any,\n context?: any,\n ): Promise<boolean> => {\n if (actions instanceof FilteredActions) {\n for (const action of actions) {\n const ed = { ...action.payload?.['$extra-data'], ...context };\n // eslint-disable-next-line no-await-in-loop\n if (await this.handlers[action.name]?.(action, actionPayload, ed)) return true;\n }\n return false;\n }\n const ed = { ...actions.payload?.['$extra-data'], ...context };\n return this.handlers[actions.name]?.(actions, actionPayload, ed) ?? false;\n };\n\n // eslint-disable-next-line class-methods-use-this\n private resolveAction = async (actions: Action | FilteredActions, context?: any): Promise<boolean> => {\n if (actions instanceof FilteredActions) {\n for (const action of actions) {\n const ed = { ...action.payload?.['$extra-data'], ...context };\n // eslint-disable-next-line no-await-in-loop\n if (await action?.[getActionName(action.name)]?.(action, action.payload, ed)) return true;\n }\n return false;\n }\n const ed = { ...actions.payload?.['$extra-data'], ...context };\n return actions?.[getActionName(actions.name)]?.(actions, actions.payload, ed) ?? false;\n };\n }\n\n const handler = new ActionHandlers();\n\n const callHandler = handler.call;\n const registerHandler = handler.register;\n\n provide('actionHandler', handler);\n\n return { registerHandler, callHandler, handler };\n}\n","/**\n * DisplayMode enum provides an enumeration for supported ways of rendering a particular object in the DOM\n */\nenum DisplayMode {\n // This enum is actually declared in dynamicforms.mixins.field_render.py\n SUPPRESS = 1, // Field will be entirely suppressed. it will not render (not even to JSON) and will not parse for PUT\n HIDDEN = 5, // Field will render as <input type=\"hidden\"> or <tr data-field_name>\n INVISIBLE = 8, // Field will render completely, but with display: none. Equal to setting its style = {display: none}\n FULL = 10, // Field will render completely\n}\n\nexport const defaultDisplayMode = DisplayMode.FULL;\n\nnamespace DisplayMode {\n export function fromString(mode: string): DisplayMode {\n if (mode.toUpperCase() === 'SUPPRESS') return DisplayMode.SUPPRESS;\n if (mode.toUpperCase() === 'HIDDEN') return DisplayMode.HIDDEN;\n if (mode.toUpperCase() === 'INVISIBLE') return DisplayMode.INVISIBLE;\n return defaultDisplayMode;\n }\n\n export function fromAny(mode: any): DisplayMode {\n const input = (typeof mode === 'number') ? mode : DisplayMode.fromString(mode as string);\n if (Object.values(DisplayMode).includes(input)) return input;\n return defaultDisplayMode;\n }\n\n export function isDefined(mode: number | string): boolean {\n const check = (typeof mode === 'number') ? mode : DisplayMode.fromString(mode as string);\n return Object.values(DisplayMode).includes(check);\n }\n}\n\nObject.freeze(DisplayMode);\n\nexport default DisplayMode;\n","import { APIConsumer } from '../api_consumer/namespace';\n\nexport namespace DfForm {\n export interface ChoicesJSON {\n id: any;\n text: any;\n icon: any;\n }\n\n export interface AJAXJSON {\n url_reverse: string;\n placeholder: string;\n additional_parameters: string;\n query_field: string;\n value_field: string;\n text_field: string;\n icon_field: string;\n }\n\n export type StatementJSON = [string | StatementJSON, number, any | StatementJSON] | null;\n\n export interface FormFieldJSON {\n uuid: string;\n name: string | null;\n label: string;\n placeholder: string;\n alignment: 'left' | 'right' | 'center' | 'decimal';\n visibility: { form: number, table: number };\n render_params: DfForm.RenderParamsJSON;\n read_only: true | any; // boolean\n choices: ChoicesJSON[];\n ajax?: AJAXJSON;\n colspan: number;\n help_text: string;\n allow_null: boolean;\n conditional_visibility?: DfForm.StatementJSON;\n }\n\n export interface FormFieldsJSON {\n [key: string]: FormFieldJSON;\n }\n\n export interface FormComponentDefinition extends APIConsumer.TableUXDefinition {\n detail_url: string;\n }\n\n export interface RenderParamsJSON {\n input_type: string;\n form_component_name: string;\n form_component_def?: DfForm.FormComponentDefinition;\n field_class?: string;\n pattern?: string;\n min?: number;\n max?: number;\n min_length?: number;\n max_length?: number;\n step?: number;\n size?: number;\n form_date_format?: string;\n form_time_format?: string;\n multiple?: boolean;\n allow_tags?: boolean;\n table?: string;\n table_show_zeroes?: boolean;\n }\n\n export type FormLayoutFieldsCollection = { [key: string]: DfForm.FormFieldJSON };\n}\n\nexport namespace FormLayoutNS {\n /*\n interfaces for dynamicforms.layout definitions\n */\n export interface LayoutInterface {\n rows: RowInterface[];\n fields: DfForm.FormLayoutFieldsCollection;\n size?: string;\n header_classes?: string;\n component_name: string;\n }\n\n export interface RowInterface {\n component_name: string;\n columns: (ColumnInterface | GroupInterface)[];\n }\n\n export interface ColumnInterface {\n type: 'column' | 'group';\n field: string;\n component_name: string;\n colspan?: number;\n }\n\n export interface GroupInterface extends ColumnInterface {\n footer?: string;\n title?: string;\n uuid: string;\n layout: LayoutInterface;\n }\n\n export type ColumnOrGroupInterface = GroupInterface | ColumnInterface;\n}\n\nexport namespace FormLayoutTypeGuards {\n export function isGroupTemplate(col: FormLayoutNS.ColumnOrGroupInterface): col is FormLayoutNS.GroupInterface {\n return col.type === 'group';\n }\n\n export function isLayoutTemplate(layout: any): layout is FormLayoutNS.LayoutInterface {\n return 'rows' in layout;\n }\n}\n","enum DialogSize {\n SMALL = 1,\n MEDIUM = 2,\n LARGE = 3,\n X_LARGE = 4,\n DEFAULT = 0,\n}\n\nexport const defaultDialogSize: DialogSize = DialogSize.DEFAULT;\n\nnamespace DialogSize {\n const largeIdentifiers: string[] = ['large', 'lg', 'modal-lg'];\n const mediumIdentifiers: string[] = ['medium', 'md', 'modal-md'];\n const smallIdentifiers: string[] = ['small', 'sm', 'modal-sm'];\n const xLargeIdentifiers: string[] = ['x-large', 'xl', 'modal-xl'];\n\n export function fromString(size?: string): DialogSize {\n if (size === undefined) return defaultDialogSize;\n if (largeIdentifiers.includes(size)) return DialogSize.LARGE;\n if (mediumIdentifiers.includes(size)) return DialogSize.MEDIUM;\n if (smallIdentifiers.includes(size)) return DialogSize.SMALL;\n if (xLargeIdentifiers.includes(size)) return DialogSize.X_LARGE;\n return defaultDialogSize;\n }\n\n export function isDefined(size: number | string) {\n const check = (typeof size === 'number') ? size : DialogSize.fromString(size as string);\n return Object.values(DialogSize).includes(check);\n }\n}\n\nObject.freeze(DialogSize);\nexport default DialogSize;\n","import { DfForm } from '../namespace';\n\nexport default class RenderParams {\n inputType: string;\n\n fieldCSSClass?: string;\n\n pattern?: string;\n\n min?: number;\n\n max?: number;\n\n minLength: number;\n\n maxLength: number;\n\n step?: number;\n\n size?: number;\n\n formDateFormat?: string;\n\n formTimeFormat?: string;\n\n multiple?: boolean;\n\n allowTags?: boolean;\n\n formComponentDef?: DfForm.FormComponentDefinition;\n\n constructor(params: DfForm.RenderParamsJSON) {\n this.inputType = params.input_type;\n this.fieldCSSClass = params.field_class;\n\n // Text input\n this.pattern = params.pattern;\n this.min = params.min;\n this.max = params.max;\n this.minLength = params.min_length ?? 0;\n this.maxLength = params.max_length ?? 1E20;\n\n // text input, translated into HTML attributes\n this.step = params.step;\n this.size = params.size;\n\n // DateTime\n this.formDateFormat = params.form_date_format;\n this.formTimeFormat = params.form_time_format;\n\n // select\n this.multiple = params.multiple;\n this.allowTags = params.allow_tags;\n\n // form definition\n this.formComponentDef = params.form_component_def;\n }\n}\n","/**\n * FormField is a JavaScript class representation of JSON form field declaration\n *\n * Note: we're just assembling every possible configuration into a flat structure here\n * This introduces properties that might have nothing to do with a particular field type\n * Example: only a select field uses the \"choices\" property, but it's there for all fields.\n * Some inheritance might have fixed this, but would also introduce additional complexity\n * as we tried to determine the correct field type from various field properties\n *\n * So, right now we aren't doing anything about that!\n */\nimport DisplayMode from '../../classes/display-mode';\nimport { Statement } from '../inputs/conditional-visibility';\nimport { DfForm } from '../namespace';\n\nimport RenderParams from './field-render-params';\n\nexport default class FormField {\n private declare fieldDef: DfForm.FormFieldJSON;\n\n public uuid!: string;\n\n public name!: string;\n\n public label!: string;\n\n public placeholder!: string;\n\n public align!: string;\n\n public visibility!: DisplayMode;\n\n public renderParams!: RenderParams;\n\n public readOnly!: boolean;\n\n public componentName!: string;\n\n public helpText!: string;\n\n public choices!: DfForm.ChoicesJSON[];\n\n public ajax!: DfForm.AJAXJSON;\n\n public widthClasses!: string;\n\n public allowNull!: boolean;\n\n public renderKey: number;\n\n public conditionalVisibility!: Statement;\n\n constructor(fieldDef: DfForm.FormFieldJSON) {\n this.renderKey = 0; // used in row.vue\n // Below we circumvent having to declare an internal variable which property getters would be reading from\n Object.defineProperties(this, {\n fieldDef: { get() { return fieldDef; }, enumerable: false },\n\n uuid: { get() { return fieldDef.uuid; }, enumerable: true },\n name: { get() { return fieldDef.name; }, enumerable: true },\n label: { get() { return fieldDef.label; }, enumerable: true },\n placeholder: { get() { return fieldDef.placeholder; }, enumerable: true },\n align: {\n get() {\n if (fieldDef.alignment === 'decimal') return 'right';\n return fieldDef.alignment;\n },\n enumerable: true,\n },\n visibility: { get() { return DisplayMode.fromAny(fieldDef.visibility.form); }, enumerable: true },\n renderParams: { get() { return new RenderParams(fieldDef.render_params); }, enumerable: true },\n readOnly: { get() { return fieldDef.read_only === true; }, enumerable: true },\n componentName: {\n get() { return fieldDef.render_params.form_component_name; },\n enumerable: true,\n configurable: true,\n },\n choices: { get() { return fieldDef.choices; }, enumerable: true },\n ajax: { get() { return fieldDef.ajax; }, enumerable: true },\n colspan: { get() { return fieldDef.colspan; }, enumerable: true, configurable: true },\n helpText: { get() { return fieldDef.help_text; }, enumerable: true },\n allowNull: { get() { return fieldDef.allow_null; }, enumerable: true },\n conditionalVisibility: { get() { return fieldDef.conditional_visibility; }, enumerable: true },\n });\n }\n\n get isVisible() { return (this.visibility !== DisplayMode.SUPPRESS && this.visibility !== DisplayMode.HIDDEN); }\n\n setVisibility(visibility: number) {\n let displayMode;\n if (DisplayMode.isDefined(visibility)) {\n displayMode = DisplayMode.fromAny(visibility);\n } else if (visibility) {\n displayMode = DisplayMode.FULL;\n } else {\n displayMode = DisplayMode.HIDDEN;\n }\n if (displayMode !== this.fieldDef.visibility.form) {\n this.fieldDef.visibility.form = displayMode;\n this.renderKey++; // notify the components to redraw\n }\n }\n}\n","// eslint-disable-next-line max-classes-per-file\nimport DisplayMode from '../../classes/display-mode';\nimport DialogSize from '../../modal/definitions/dialog-size';\nimport { FormLayoutNS, DfForm } from '../namespace';\n\nimport FormField from './field';\n\nlet FormLayoutClass: typeof FormLayout; // hoisting required: so we can use the class before it's even declared\n\nexport class Column extends FormField {\n layoutFieldComponentName!: string;\n\n colspan!: number;\n\n constructor(layoutRow: LayoutRow, def: FormLayoutNS.ColumnInterface, fieldDef: DfForm.FormFieldJSON) {\n super(fieldDef);\n let renderKey = 0;\n Object.defineProperty(this, 'layoutFieldComponentName', {\n get() { return def.component_name || 'FormField'; },\n enumerable: true,\n configurable: true,\n });\n Object.defineProperty(this, 'colspan', {\n get() { return def.colspan || 1; },\n enumerable: true,\n configurable: true,\n });\n Object.defineProperty(this, 'renderKey', {\n get() { return renderKey; },\n set(value) {\n renderKey = value;\n layoutRow.renderKey++;\n },\n enumerable: true,\n });\n }\n}\n\nexport class Group extends Column {\n public title!: string;\n\n public layout!: FormLayout;\n\n constructor(layoutRow: LayoutRow, def: FormLayoutNS.GroupInterface, fieldDef: DfForm.FormFieldJSON) {\n if (fieldDef == null) {\n const fd: DfForm.FormFieldJSON = {\n uuid: crypto.randomUUID(),\n name: null,\n label: '',\n placeholder: '',\n alignment: 'left',\n visibility: { form: DisplayMode.FULL, table: DisplayMode.SUPPRESS },\n render_params: {\n input_type: '',\n form_component_name: 'df-form-layout',\n },\n read_only: true,\n choices: [],\n colspan: 1,\n help_text: '',\n allow_null: false,\n };\n super(layoutRow, def, fd);\n } else {\n super(layoutRow, def, fieldDef);\n }\n Object.defineProperty(this, 'layoutFieldComponentName', {\n get() { return def.component_name || 'FormFieldGroup'; },\n enumerable: true,\n configurable: true,\n });\n Object.defineProperty(this, 'title', { get() { return def.title; }, enumerable: true });\n Object.defineProperty(\n this,\n 'layout',\n { get() { return new FormLayoutClass(def.layout as FormLayoutNS.LayoutInterface); }, enumerable: true },\n );\n Object.defineProperty(\n this,\n 'componentName',\n { get() { return 'FormFieldGroup'; }, enumerable: true, configurable: true },\n );\n // footer=self.footer, title=self.title or sub_serializer.label,\n // uuid=sub_serializer.uuid,\n // layout=layout.as_component_def(sub_serializer, fields)\n }\n}\n\nfunction FormColumn(\n layoutRow: LayoutRow,\n columnDef: FormLayoutNS.ColumnOrGroupInterface,\n fieldDef: DfForm.FormFieldJSON,\n) {\n switch (columnDef.type) {\n case 'column':\n return new Column(layoutRow, columnDef, fieldDef);\n case 'group':\n return new Group(layoutRow, columnDef as FormLayoutNS.GroupInterface, fieldDef);\n default:\n throw Error(`Unknown layout column type \"${columnDef.type}\"`);\n }\n}\n\ntype FormLayoutFields = { [key: string]: FormField };\n\nclass LayoutRow {\n public renderKey!: number;\n\n public columns: Column[];\n\n public componentName!: string;\n\n constructor(\n rowDef: FormLayoutNS.RowInterface,\n fields: FormLayoutFields,\n fieldDefs: DfForm.FormLayoutFieldsCollection,\n ) {\n this.columns = rowDef.columns.map((column) => {\n const res = FormColumn(this, column, fieldDefs[column.field]);\n fields[column.field] = res;\n return res;\n });\n this.renderKey = 0;\n Object.defineProperty(this, 'componentName', { get() { return rowDef.component_name; }, enumerable: true });\n }\n\n get anyVisible() {\n return this.columns.reduce((result, column) => (result || column.isVisible), false);\n }\n}\n\nclass FormLayout {\n public fields: FormLayoutFields;\n\n public rows: LayoutRow[];\n\n public componentName!: string;\n\n public fieldName!: string;\n\n public size!: DialogSize;\n\n constructor(layout?: FormLayoutNS.LayoutInterface) {\n if (layout === undefined) {\n this.fields = {};\n this.rows = [];\n Object.defineProperty(this, 'size', { get() { return DialogSize.DEFAULT; }, enumerable: true });\n } else {\n this.fields = {};\n this.rows = layout.rows.map((row) => new LayoutRow(row, this.fields, layout.fields));\n Object.keys(layout.fields).forEach((fieldName: string) => {\n // here we generically add all the fields that were not listed in the layout definition\n if (!(fieldName in this.fields)) this.fields[fieldName] = new FormField(layout.fields[fieldName]);\n });\n Object.defineProperty(this, 'componentName', { get() { return layout.component_name; }, enumerable: true });\n Object.defineProperty(this, 'size', { get() { return DialogSize.fromString(layout.size); }, enumerable: true });\n }\n }\n}\n\nFormLayoutClass = FormLayout;\n\nexport default FormLayout;\n","import { cloneDeep } from 'lodash-es';\nimport { reactive } from 'vue';\n\nimport type { APIConsumer } from '../../api_consumer/namespace';\nimport DisplayMode from '../../classes/display-mode';\nimport type { FormLayoutNS } from '../namespace';\nimport { FormLayoutTypeGuards } from '../namespace';\n\nimport type FormField from './field';\nimport type FormLayout from './layout';\nimport { Group } from './layout';\n\ntype FormLayoutInterface = FormLayoutNS.LayoutInterface;\ntype FormLayoutOrInterface = FormLayout | FormLayoutInterface;\n\nfunction colIsGroup(col: any): col is Group {\n return col instanceof Group;\n}\n\nfunction collectAllFields(layout: FormLayoutOrInterface): FormField[] {\n const fields: FormField[] = [];\n if (layout.fields) {\n fields.push(...Object.values(layout.fields));\n }\n\n if (FormLayoutTypeGuards.isLayoutTemplate(layout)) {\n // Recursively check all nested groups that don't have their own nested objects\n layout.rows.forEach((row) => {\n row.columns?.forEach((col) => {\n if (FormLayoutTypeGuards.isGroupTemplate(col) && col.layout && col.field == null) {\n fields.push(...collectAllFields(col.layout));\n } else if (colIsGroup(col) && col.layout && col.field == null) {\n // TODO: this should not be. code too unpredictable. How the hell does col become group here?\n fields.push(...collectAllFields(col.layout));\n }\n });\n });\n }\n return fields;\n}\n\nexport default class FormPayload {\n [key: string]: any;\n\n ['$extra-data']: any;\n\n private constructor(data?: APIConsumer.FormPayloadJSON, layout?: FormLayoutOrInterface) {\n this.setData(data, layout);\n }\n\n static create(): FormPayload;\n static create(data: FormPayload): FormPayload;\n static create(data: APIConsumer.FormPayloadJSON, layout?: FormLayoutOrInterface): FormPayload;\n static create(data?: APIConsumer.FormPayloadJSON, layout?: FormLayoutOrInterface): FormPayload {\n // Type assertion necessary to keep PyCharm from complaining about private functions\n return <FormPayload> reactive(new FormPayload(data, layout));\n }\n\n addExtraData(data: { [key: string]: any }) {\n const extraData = { ...this['$extra-data'], ...data };\n Object.defineProperty(this, '$extra-data', { get() { return extraData; }, enumerable: false, configurable: true });\n }\n\n clear() {\n Object.getOwnPropertyNames(this).forEach((key) => {\n delete this[key];\n });\n }\n\n setData(data?: APIConsumer.FormPayloadJSON, layout?: FormLayoutOrInterface): FormPayload {\n let properties = {} as { [key: string]: any };\n let extraData = {};\n\n if (data == null) {\n // This is an empty constructor which creates just the class instance, without any payload nor any fields.\n // properties and extraData are already what they need to be\n } else if (data instanceof FormPayload) {\n [properties, extraData] = this.copyWithProperties(data);\n } else if (layout?.fields) {\n collectAllFields(layout).forEach((field: FormField) => {\n if (field.visibility === DisplayMode.SUPPRESS) return;\n if (field.readOnly && field.name != null && !field.name.endsWith('-display')) {\n properties[field.name] = { get() { return data[field.name]; }, enumerable: false, configurable: true };\n } else if (field.name != null) {\n const self = this;\n self[field.name] = data[field.name];\n Object.defineProperty(self, `set${field.name}Value`, {\n get() { return function setValue(newValue: any) { self[field.name] = newValue; }; },\n enumerable: false,\n configurable: true,\n });\n }\n });\n }\n Object.defineProperty(this, '$extra-data', { get() { return extraData; }, enumerable: false, configurable: true });\n Object.defineProperties(this, properties);\n Object.defineProperty(this, '_properties', { get() { return properties; }, enumerable: false, configurable: true });\n\n return this;\n }\n\n replaceData(data?: APIConsumer.FormPayloadJSON, layout?: FormLayout): FormPayload {\n this.clear();\n return this.setData(data, layout);\n }\n\n private copyWithProperties(base: FormPayload): [_properties: any, extraData: any] {\n Object.entries(base).forEach(([itemName, itemValue]) => {\n this[itemName] = itemValue;\n });\n return [\n cloneDeep(base._properties), // eslint-disable-line no-underscore-dangle\n cloneDeep(base['$extra-data']),\n ];\n }\n}\n","import { Slot, VNode } from 'vue';\n\nimport FilteredActions from '../actions/filtered-actions';\nimport type { ActionsNS } from '../actions/namespace';\n\nimport { Dialogs } from './namespace';\n\ntype IHandlers = ActionsNS.IHandlers;\n\nlet idGenerator = 0;\n\nexport default class DialogDefinition implements Dialogs.RunningDialog {\n /* Base dialog definition */\n public title: string | VNode | Slot;\n\n public body: Dialogs.DialogMessage;\n\n public options: Dialogs.DialogOptions;\n\n public actions: FilteredActions | VNode | Slot;\n\n public actionHandlers?: IHandlers;\n\n public dialogId: number; // Unique dialog ID\n\n private _topOfTheStack?: boolean; // true when this dialog is the first one\n\n /* API functions */\n public close: Function; // API function to close this dialog from calling code\n\n /* Support declarations */\n public promise: Promise<any>; // The promise which will be resolved when the dialog closes\n\n public resolvePromise: Function; // function to resolve the promise\n\n public rejectPromise: Function; // Function to reject the promise\n\n constructor(\n title: string | VNode | Slot,\n body: Dialogs.DialogMessage,\n options: Dialogs.DialogOptions,\n actions: FilteredActions | Slot,\n actionHandlers?: IHandlers,\n ) {\n this.title = title;\n this.body = body;\n this.options = options;\n this.actions = actions;\n this.actionHandlers = actionHandlers;\n this.dialogId = ++idGenerator;\n this.close = () => null;\n this.promise = new Promise<any>(() => {});\n this.resolvePromise = () => null;\n this.rejectPromise = () => null;\n }\n\n get topOfTheStack() { return this._topOfTheStack || false; }\n\n set topOfTheStack(value: boolean) { this._topOfTheStack = value; }\n}\n","import { isString } from 'lodash-es';\nimport { defineComponent, h, resolveComponent, DefineComponent } from 'vue';\n\nimport FilteredActions from '../actions/filtered-actions';\nimport type { ActionsNS } from '../actions/namespace';\n\nimport { Dialogs } from './namespace';\n\ntype IHandlers = ActionsNS.IHandlers;\n\nfunction processSlot(\n slot: string,\n content: Dialogs.DialogSectionContent | DefineComponent | FilteredActions | Dialogs.CustomComponentMessage,\n) {\n if (content == null) return null;\n if (typeof content === 'string') {\n // The slot is a plain string so let's just create a span element\n return () => h('span', null, content);\n }\n if (content instanceof FilteredActions) {\n // the slot is FilteredActions. Need to construct a DfActions component\n return () => h(resolveComponent('DfActions'), { slot, actions: content });\n }\n if (content && 'componentName' in content && 'props' in content) {\n const component = isString(content.componentName) ?\n resolveComponent(content.componentName) :\n content.componentName; // it is a component\n return () => h(component, { slot, ...content.props });\n }\n // we have slots as render functions (template usage of DfDialog)\n return content;\n}\n\nexport default /* #__PURE__ */ defineComponent({\n methods: {\n renderFunction(\n curDlgKey: number,\n titleSlot: Dialogs.DialogSectionContent,\n bodySlot: Dialogs.DialogMessage,\n actionsSlot: Dialogs.DialogSectionContent | FilteredActions,\n options: Dialogs.DialogOptions,\n actionHandlers?: IHandlers,\n ) {\n return h(\n // Jure 16.3.2023 types don't match here, but the code works. Too green to be able to fix\n // @ts-ignore\n resolveComponent('DfModalDialog'),\n { show: true, options: options || {}, key: curDlgKey, actionHandlers },\n {\n title: processSlot('title', titleSlot),\n body: processSlot('body', bodySlot),\n actions: processSlot('actions', actionsSlot),\n },\n );\n },\n },\n});\n","import { find, findIndex } from 'lodash-es';\nimport { Ref, ref } from 'vue';\n\nimport FilteredActions from '../actions/filtered-actions';\nimport FormPayload from '../form/definitions/form-payload';\n\nimport DialogDefinition from './dialog-definition';\n\nclass DialogList {\n public list: DialogDefinition[];\n\n private currentRef: Ref<DialogDefinition | null>;\n\n constructor() {\n this.list = [];\n this.currentRef = ref(null);\n }\n\n setCurrent() {\n const newValue = this.list.length ? this.list[this.list.length - 1] : null;\n if (this.currentRef.value !== newValue) this.currentRef.value = newValue;\n }\n\n get current(): DialogDefinition | null {\n return this.currentRef.value;\n }\n\n push(dialogDef: DialogDefinition, existingDialogId?: number): number {\n const pos: number = findIndex(this.list, { dialogId: existingDialogId });\n\n if (pos !== -1) {\n // replace existing definition\n dialogDef.dialogId = <number>existingDialogId;\n this.list[pos] = dialogDef;\n if (pos === this.list.length - 1) dialogDef.topOfTheStack = true;\n } else {\n // add a new dialog to the top of the stack\n if (this.list.length) this.list[this.list.length - 1].topOfTheStack = false;\n if (dialogDef.actions instanceof FilteredActions) {\n const newPayload = FormPayload.create(dialogDef.actions.payload || FormPayload.create());\n newPayload.addExtraData({ dialog: dialogDef });\n dialogDef.actions = new FilteredActions(dialogDef.actions.actions, newPayload);\n }\n this.list.push(dialogDef);\n dialogDef.topOfTheStack = true;\n }\n const self = this;\n dialogDef.close = () => self.pop(dialogDef.dialogId);\n dialogDef.promise = new Promise((resolve, reject) => {\n dialogDef.resolvePromise = resolve;\n dialogDef.rejectPromise = reject;\n });\n this.setCurrent();\n return dialogDef.dialogId;\n }\n\n getDialogFromId(id: number) {\n const pos = findIndex(this.list, { dialogId: id });\n return pos !== -1 ? this.list[pos] : null;\n }\n\n pop(existingDialogId: number) {\n const pos = findIndex(this.list, { dialogId: existingDialogId });\n if (pos !== -1) {\n this.list.splice(pos, 1);\n if (this.list.length && pos === this.list.length) {\n this.list[this.list.length - 1].topOfTheStack = true;\n }\n }\n this.setCurrent();\n }\n\n isCurrentDialogPromise(promise: Promise<any>) {\n return this.current?.promise === promise;\n }\n\n getDialogDefFromPromise(promise: Promise<any>) {\n return find(this.list, { promise });\n }\n}\n\nconst dialogList = new DialogList();\n\nexport { DialogDefinition };\nexport default dialogList;\n","import { defineComponent, h, onMounted, onUnmounted } from 'vue';\n\nimport ModalRenderer from './modal-renderer';\nimport dialogList from './modal-view-list';\n\nlet uniqueIdCounter = 0;\nexport const instances: number[] = [];\n\nexport default defineComponent({\n name: 'ModalView',\n mixins: [ModalRenderer],\n setup() {\n const uniqueId = ++uniqueIdCounter;\n onMounted(() => {\n instances.push(uniqueId);\n if (instances.length > 1 && instances.indexOf(uniqueId) > 0) {\n console.warn('Multiple instances of ModalView placed in VDom. there should be only one!', instances);\n // return;\n }\n });\n onUnmounted(() => {\n const index = instances.indexOf(uniqueId);\n if (index > -1) instances.splice(index, 1);\n });\n // watch(dialogList.current, (newValue, oldValue) => {\n // console.log('watch triggered', [newValue, oldValue]);\n // });\n\n // return the render function\n return { uniqueId };\n },\n render() {\n const curDlg = dialogList.current;\n const curDlgKey = curDlg?.dialogId;\n // only render if we're the first instance of ModalView\n if (!curDlg || instances.indexOf(this.uniqueId) !== 0) return h('div', { key: curDlgKey });\n return this.renderFunction(\n <number>curDlgKey,\n curDlg.title,\n curDlg.body,\n curDlg.actions,\n curDlg.options,\n curDlg.actionHandlers,\n );\n },\n});\n","import Action, { defaultActionHandler, getActionName } from '../actions/action';\nimport FilteredActions from '../actions/filtered-actions';\nimport type { ActionsNS } from '../actions/namespace';\nimport { APIConsumer } from '../api_consumer/namespace';\n\nimport DialogSize from './definitions/dialog-size';\nimport DialogDefinition from './dialog-definition';\nimport { instances } from './modal-view';\nimport dialogList from './modal-view-list';\nimport { Dialogs } from './namespace';\n\ntype IHandlers = ActionsNS.IHandlers;\n\nconst defaultOptions = { size: DialogSize.DEFAULT };\n\nconst dfModal = {\n get isInstalled() { return instances.length > 0; },\n getDialogDefinition(existingDialog: number | Promise<any>) {\n if (existingDialog instanceof Promise) return dialogList.getDialogDefFromPromise(existingDialog);\n return dialogList.getDialogFromId(existingDialog);\n },\n fromRenderFunctions(existingDialog: number, dfDialog: DialogDefinition) {\n return dialogList.push(dfDialog, existingDialog);\n },\n fromFormDefinition(formDefinition: APIConsumer.FormDefinition) {\n const layout = formDefinition.layout;\n const payload = formDefinition.payload;\n const actions = formDefinition.actions;\n const actionHandlers = formDefinition.actionHandlers;\n const errors = formDefinition.errors;\n return this.message(\n formDefinition.title,\n {\n componentName: formDefinition.layout.componentName,\n props: { layout, payload, actions, errors },\n },\n actions.formFooter,\n { size: layout.size || DialogSize.DEFAULT },\n actionHandlers,\n );\n },\n message(\n title: Dialogs.DialogTitle,\n message: Dialogs.DialogMessage,\n actions?: FilteredActions,\n options?: Dialogs.DialogOptions,\n actionHandlers?: IHandlers,\n ) {\n if (actions) {\n //