UNPKG

@kelvininc/react-ui-components

Version:

Kelvin UI Components for React applications

1,028 lines (1,004 loc) 458 kB
import { EToasterType, EComponentSize, EValidationState, EInputFieldType, EActionButtonType, EIconName, stringHelper, ETooltipPosition } from '@kelvininc/ui-components'; export * from '@kelvininc/ui-components'; import { __rest } from 'tslib'; import React, { createElement, useState, useCallback, Component as Component$2, useEffect, useReducer, useMemo, createRef, forwardRef, useRef } from 'react'; import ReactDOM from 'react-dom'; import { defineCustomElements } from '@kelvininc/ui-components/loader'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import isPlainObject from 'lodash-es/isPlainObject.js'; import isEqualWith from 'lodash-es/isEqualWith.js'; import get from 'lodash-es/get.js'; import isEmpty from 'lodash-es/isEmpty.js'; import jsonpointer from 'jsonpointer'; import omit from 'lodash-es/omit.js'; import has from 'lodash-es/has.js'; import isNumber from 'lodash-es/isNumber.js'; import isObject$1 from 'lodash-es/isObject.js'; import isString from 'lodash-es/isString.js'; import reduce from 'lodash-es/reduce.js'; import times from 'lodash-es/times.js'; import set from 'lodash-es/set.js'; import transform from 'lodash-es/transform.js'; import merge from 'lodash-es/merge.js'; import flattenDeep from 'lodash-es/flattenDeep.js'; import uniq from 'lodash-es/uniq.js'; import mergeAllOf from 'json-schema-merge-allof'; import union from 'lodash-es/union.js'; import isNil from 'lodash-es/isNil.js'; import cloneDeep from 'lodash-es/cloneDeep.js'; import setWith from 'lodash-es/setWith.js'; import ReactIs from 'react-is'; import toPath from 'lodash-es/toPath.js'; import keys from 'lodash-es/keys.js'; import pickBy from 'lodash-es/pickBy.js'; import difference from 'lodash-es/difference.js'; import forEach from 'lodash-es/forEach.js'; import _pick from 'lodash-es/pick.js'; import { nanoid } from 'nanoid'; import Markdown from 'markdown-to-jsx'; import unset from 'lodash-es/unset.js'; import classNames from 'classnames'; import { throttle, get as get$1, isNil as isNil$1, isString as isString$1, isEmpty as isEmpty$1, isArray, merge as merge$1, isBoolean, cloneDeep as cloneDeep$1, isEqualWith as isEqualWith$1 } from 'lodash-es'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import Editor, { useMonaco, DiffEditor } from '@monaco-editor/react'; const dashToPascalCase = (str) => str .toLowerCase() .split('-') .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) .join(''); const camelToDashCase = (str) => str.replace(/([A-Z])/g, (m) => `-${m[0].toLowerCase()}`); const attachProps = (node, newProps, oldProps = {}) => { if (node instanceof Element) { const className = getClassName(node.classList, newProps, oldProps); if (className !== '') { node.className = className; } Object.keys(newProps).forEach((name) => { if (name === 'children' || name === 'style' || name === 'ref' || name === 'class' || name === 'className' || name === 'forwardedRef') { return; } if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { const eventName = name.substring(2); const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); if (!isCoveredByReact(eventNameLc)) { syncEvent(node, eventNameLc, newProps[name]); } } else { node[name] = newProps[name]; const propType = typeof newProps[name]; if (propType === 'string') { node.setAttribute(camelToDashCase(name), newProps[name]); } } }); } }; const getClassName = (classList, newProps, oldProps) => { const newClassProp = newProps.className || newProps.class; const oldClassProp = oldProps.className || oldProps.class; const currentClasses = arrayToMap(classList); const incomingPropClasses = arrayToMap(newClassProp ? newClassProp.split(' ') : []); const oldPropClasses = arrayToMap(oldClassProp ? oldClassProp.split(' ') : []); const finalClassNames = []; currentClasses.forEach((currentClass) => { if (incomingPropClasses.has(currentClass)) { finalClassNames.push(currentClass); incomingPropClasses.delete(currentClass); } else if (!oldPropClasses.has(currentClass)) { finalClassNames.push(currentClass); } }); incomingPropClasses.forEach((s) => finalClassNames.push(s)); return finalClassNames.join(' '); }; const transformReactEventName = (eventNameSuffix) => { switch (eventNameSuffix) { case 'doubleclick': return 'dblclick'; } return eventNameSuffix; }; const isCoveredByReact = (eventNameSuffix) => { if (typeof document === 'undefined') { return true; } else { const eventName = 'on' + transformReactEventName(eventNameSuffix); let isSupported = eventName in document; if (!isSupported) { const element = document.createElement('div'); element.setAttribute(eventName, 'return;'); isSupported = typeof element[eventName] === 'function'; } return isSupported; } }; const syncEvent = (node, eventName, newEventHandler) => { const eventStore = node.__events || (node.__events = {}); const oldEventHandler = eventStore[eventName]; if (oldEventHandler) { node.removeEventListener(eventName, oldEventHandler); } node.addEventListener(eventName, (eventStore[eventName] = function handler(e) { if (newEventHandler) { newEventHandler.call(this, e); } })); }; const arrayToMap = (arr) => { const map = new Map(); arr.forEach((s) => map.set(s, s)); return map; }; const setRef = (ref, value) => { if (typeof ref === 'function') { ref(value); } else if (ref != null) { ref.current = value; } }; const mergeRefs = (...refs) => { return (value) => { refs.forEach((ref) => { setRef(ref, value); }); }; }; const createForwardRef = (ReactComponent, displayName) => { const forwardRef = (props, ref) => { return React.createElement(ReactComponent, Object.assign({}, props, { forwardedRef: ref })); }; forwardRef.displayName = displayName; return React.forwardRef(forwardRef); }; const createReactComponent = (tagName, ReactComponentContext, manipulatePropsFunction, defineCustomElement) => { const displayName = dashToPascalCase(tagName); const ReactComponent = class extends React.Component { constructor(props) { super(props); this.setComponentElRef = (element) => { this.componentEl = element; }; } componentDidMount() { this.componentDidUpdate(this.props); } componentDidUpdate(prevProps) { attachProps(this.componentEl, this.props, prevProps); } render() { const _a = this.props, { children, forwardedRef, style, className, ref } = _a, cProps = __rest(_a, ["children", "forwardedRef", "style", "className", "ref"]); let propsToPass = Object.keys(cProps).reduce((acc, name) => { const value = cProps[name]; if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { const eventName = name.substring(2).toLowerCase(); if (typeof document !== 'undefined' && isCoveredByReact(eventName)) { acc[name] = value; } } else { const type = typeof value; if (type === 'string' || type === 'boolean' || type === 'number') { acc[camelToDashCase(name)] = value; } } return acc; }, {}); const newProps = Object.assign(Object.assign({}, propsToPass), { ref: mergeRefs(forwardedRef, this.setComponentElRef), style }); return createElement(tagName, newProps, children); } static get displayName() { return displayName; } }; return createForwardRef(ReactComponent, displayName); }; defineCustomElements(); const KvAbsoluteTimePicker = createReactComponent('kv-absolute-time-picker'); const KvAbsoluteTimePickerDropdown = createReactComponent('kv-absolute-time-picker-dropdown'); const KvAbsoluteTimePickerDropdownInput = createReactComponent('kv-absolute-time-picker-dropdown-input'); const KvActionButton = createReactComponent('kv-action-button'); const KvActionButtonIcon = createReactComponent('kv-action-button-icon'); const KvActionButtonSplit = createReactComponent('kv-action-button-split'); const KvActionButtonText = createReactComponent('kv-action-button-text'); const KvAgree = createReactComponent('kv-agree'); const KvAlert = createReactComponent('kv-alert'); const KvBadge = createReactComponent('kv-badge'); const KvBoxBuild = createReactComponent('kv-box-build'); const KvBreadcrumb = createReactComponent('kv-breadcrumb'); const KvBreadcrumbItem = createReactComponent('kv-breadcrumb-item'); const KvBreadcrumbList = createReactComponent('kv-breadcrumb-list'); const KvCalendar = createReactComponent('kv-calendar'); const KvCalendarDay = createReactComponent('kv-calendar-day'); const KvCheckbox = createReactComponent('kv-checkbox'); const KvColorCircle = createReactComponent('kv-color-circle'); const KvCopyToClipboard = createReactComponent('kv-copy-to-clipboard'); const KvDateTimeInput = createReactComponent('kv-date-time-input'); const KvDescriptionList = createReactComponent('kv-description-list'); const KvDirtyDot = createReactComponent('kv-dirty-dot'); const KvDisagree = createReactComponent('kv-disagree'); const KvDropdown = createReactComponent('kv-dropdown'); const KvDropdownBase = createReactComponent('kv-dropdown-base'); const KvErrorState = createReactComponent('kv-error-state'); const KvErrorState404 = createReactComponent('kv-error-state-404'); const KvEsAssetPlaceholder = createReactComponent('kv-es-asset-placeholder'); const KvEsCheckbox = createReactComponent('kv-es-checkbox'); const KvEsComponentPlaceholder = createReactComponent('kv-es-component-placeholder'); const KvEsError404 = createReactComponent('kv-es-error-404'); const KvEsError503 = createReactComponent('kv-es-error-503'); const KvEsKelvinLogotype = createReactComponent('kv-es-kelvin-logotype'); const KvEsLock = createReactComponent('kv-es-lock'); const KvEsMetadata = createReactComponent('kv-es-metadata'); const KvEsPartPlaceholder = createReactComponent('kv-es-part-placeholder'); const KvEsSectionCell = createReactComponent('kv-es-section-cell'); const KvEsSectionSomethingwentwrong = createReactComponent('kv-es-section-somethingwentwrong'); const KvEsSectionThresholds = createReactComponent('kv-es-section-thresholds'); const KvEsSensorPlaceholder = createReactComponent('kv-es-sensor-placeholder'); const KvEsSlowy = createReactComponent('kv-es-slowy'); const KvEsSomethingwentwrong = createReactComponent('kv-es-somethingwentwrong'); const KvEsTableBuild = createReactComponent('kv-es-table-build'); const KvEsTableEmpty = createReactComponent('kv-es-table-empty'); const KvEsTableSearch = createReactComponent('kv-es-table-search'); const KvFeedbackForm = createReactComponent('kv-feedback-form'); const KvFormHelpText = createReactComponent('kv-form-help-text'); const KvFormLabel = createReactComponent('kv-form-label'); const KvIcon = createReactComponent('kv-icon'); const KvIllustration = createReactComponent('kv-illustration'); const KvIllustrationMessage = createReactComponent('kv-illustration-message'); const KvImpact = createReactComponent('kv-impact'); const KvImport = createReactComponent('kv-import'); const KvInfoLabel = createReactComponent('kv-info-label'); const KvInlineEditableField = createReactComponent('kv-inline-editable-field'); const KvInputWrapper = createReactComponent('kv-input-wrapper'); const KvLink = createReactComponent('kv-link'); const KvLoader = createReactComponent('kv-loader'); const KvModal = createReactComponent('kv-modal'); const KvMultiSelectDropdown = createReactComponent('kv-multi-select-dropdown'); const KvNoContentHere = createReactComponent('kv-no-content-here'); const KvNoDataAvailable = createReactComponent('kv-no-data-available'); const KvNoMatchingResults = createReactComponent('kv-no-matching-results'); const KvNoResultsFoundDark = createReactComponent('kv-no-results-found-dark'); const KvNoResultsFoundLight = createReactComponent('kv-no-results-found-light'); const KvPartyDance = createReactComponent('kv-party-dance'); const KvPortal = createReactComponent('kv-portal'); const KvRadio = createReactComponent('kv-radio'); const KvRadioList = createReactComponent('kv-radio-list'); const KvRadioListItem = createReactComponent('kv-radio-list-item'); const KvRange = createReactComponent('kv-range'); const KvRelativeTimePicker = createReactComponent('kv-relative-time-picker'); const KvSearch = createReactComponent('kv-search'); const KvSelect = createReactComponent('kv-select'); const KvSelectCreateOption = createReactComponent('kv-select-create-option'); const KvSelectMultiOptions = createReactComponent('kv-select-multi-options'); const KvSelectOption = createReactComponent('kv-select-option'); const KvSelectShortcutsLabel = createReactComponent('kv-select-shortcuts-label'); const KvSingleSelectDropdown = createReactComponent('kv-single-select-dropdown'); const KvSoftAgree = createReactComponent('kv-soft-agree'); const KvStateIndicator = createReactComponent('kv-state-indicator'); const KvStepBar = createReactComponent('kv-step-bar'); const KvStepIndicator = createReactComponent('kv-step-indicator'); const KvStepProgressBar = createReactComponent('kv-step-progress-bar'); const KvSummaryCard = createReactComponent('kv-summary-card'); const KvSwitchButton = createReactComponent('kv-switch-button'); const KvTabItem = createReactComponent('kv-tab-item'); const KvTabNavigation = createReactComponent('kv-tab-navigation'); const KvTableBuild = createReactComponent('kv-table-build'); const KvTag = createReactComponent('kv-tag'); const KvTagAlarm = createReactComponent('kv-tag-alarm'); const KvTagLetter = createReactComponent('kv-tag-letter'); const KvTagStatus = createReactComponent('kv-tag-status'); const KvTakeActions = createReactComponent('kv-take-actions'); const KvTextArea = createReactComponent('kv-text-area'); const KvTextField = createReactComponent('kv-text-field'); const KvTimePicker = createReactComponent('kv-time-picker'); const KvTimePickerSelectOption = createReactComponent('kv-time-picker-select-option'); const KvToaster = createReactComponent('kv-toaster'); const KvToggleButton = createReactComponent('kv-toggle-button'); const KvToggleButtonGroup = createReactComponent('kv-toggle-button-group'); const KvToggleSwitch = createReactComponent('kv-toggle-switch'); const KvToggleTip = createReactComponent('kv-toggle-tip'); const KvTooltip = createReactComponent('kv-tooltip'); const KvTooltipText = createReactComponent('kv-tooltip-text'); const KvTree = createReactComponent('kv-tree'); const KvTreeDropdown = createReactComponent('kv-tree-dropdown'); const KvTreeItem = createReactComponent('kv-tree-item'); const KvVirtualizedList = createReactComponent('kv-virtualized-list'); const KvWizard = createReactComponent('kv-wizard'); const KvWizardFooter = createReactComponent('kv-wizard-footer'); const KvWizardHeader = createReactComponent('kv-wizard-header'); const DEFAULT_ROOT_ID = 'toasters-root'; const TOASTER_TTL_MS = 5000; const TOASTER_CONFIG = { header: '', type: EToasterType.Info, ttl: TOASTER_TTL_MS }; function useToaster(initialConfig = TOASTER_CONFIG, initialState = false) { const [config, setConfig] = useState(initialConfig); const [isOpen, setOpen] = useState(initialState); const openToaster = useCallback((newConfig) => { setOpen(true); setConfig(Object.assign(Object.assign({}, initialConfig), newConfig)); }, [initialConfig]); const closeToaster = useCallback(() => { setOpen(false); setConfig(initialConfig); }, [initialConfig]); return { isOpen, openToaster, closeToaster, config }; } function ToasterContainer(_a) { var { rootId = DEFAULT_ROOT_ID, isOpen, children } = _a, otherProps = __rest(_a, ["rootId", "isOpen", "children"]); if (!isOpen) { return null; } return ReactDOM.createPortal(React.createElement(KvToaster, Object.assign({}, otherProps), children), document.getElementById(rootId)); } /** Determines whether a `thing` is an object for the purposes of RJSF. In this case, `thing` is an object if it has * the type `object` but is NOT null, an array or a File. * * @param thing - The thing to check to see whether it is an object * @returns - True if it is a non-null, non-array, non-File object */ function isObject(thing) { if (typeof thing !== 'object' || thing === null) { return false; } // lastModified is guaranteed to be a number on a File instance // as per https://w3c.github.io/FileAPI/#dfn-lastModified if (typeof thing.lastModified === 'number' && typeof File !== 'undefined' && thing instanceof File) { return false; } // getMonth is guaranteed to be a method on a Date instance // as per https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date.prototype.getmonth if (typeof thing.getMonth === 'function' && typeof Date !== 'undefined' && thing instanceof Date) { return false; } return !Array.isArray(thing); } /** Checks the schema to see if it is allowing additional items, by verifying that `schema.additionalItems` is an * object. The user is warned in the console if `schema.additionalItems` has the value `true`. * * @param schema - The schema object to check * @returns - True if additional items is allowed, otherwise false */ function allowAdditionalItems(schema) { if (schema.additionalItems === true) { console.warn('additionalItems=true is currently not supported'); } return isObject(schema.additionalItems); } /** Attempts to convert the string into a number. If an empty string is provided, then `undefined` is returned. If a * `null` is provided, it is returned. If the string ends in a `.` then the string is returned because the user may be * in the middle of typing a float number. If a number ends in a pattern like `.0`, `.20`, `.030`, string is returned * because the user may be typing number that will end in a non-zero digit. Otherwise, the string is wrapped by * `Number()` and if that result is not `NaN`, that number will be returned, otherwise the string `value` will be. * * @param value - The string or null value to convert to a number * @returns - The `value` converted to a number when appropriate, otherwise the `value` */ function asNumber(value) { if (value === '') { return undefined; } if (value === null) { return null; } if (/\.$/.test(value)) { // '3.' can't really be considered a number even if it parses in js. The // user is most likely entering a float. return value; } if (/\.0$/.test(value)) { // we need to return this as a string here, to allow for input like 3.07 return value; } if (/\.\d*0$/.test(value)) { // It's a number, that's cool - but we need it as a string so it doesn't screw // with the user when entering dollar amounts or other values (such as those with // specific precision or number of significant digits) return value; } const n = Number(value); const valid = typeof n === 'number' && !Number.isNaN(n); return valid ? n : value; } /** Below are the list of all the keys into various elements of a RJSFSchema or UiSchema that are used by the various * utility functions. In addition to those keys, there are the special `ADDITIONAL_PROPERTY_FLAG` and * `RJSF_ADDITIONAL_PROPERTIES_FLAG` flags that is added to a schema under certain conditions by the `retrieveSchema()` * utility. */ const ADDITIONAL_PROPERTY_FLAG = '__additional_property'; const ADDITIONAL_PROPERTIES_KEY = 'additionalProperties'; const ALL_OF_KEY = 'allOf'; const ANY_OF_KEY = 'anyOf'; const CONST_KEY = 'const'; const DEFAULT_KEY = 'default'; const DEPENDENCIES_KEY = 'dependencies'; const ENUM_KEY = 'enum'; const ERRORS_KEY = '__errors'; const ID_KEY = '$id'; const IF_KEY = 'if'; const ITEMS_KEY = 'items'; const JUNK_OPTION_ID = '_$junk_option_schema_id$_'; const NAME_KEY = '$name'; const ONE_OF_KEY = 'oneOf'; const PROPERTIES_KEY = 'properties'; const REQUIRED_KEY = 'required'; const SUBMIT_BTN_OPTIONS_KEY = 'submitButtonOptions'; const REF_KEY = '$ref'; const RJSF_ADDITIONAL_PROPERTIES_FLAG = '__rjsf_additionalProperties'; const ROOT_SCHEMA_PREFIX = '__rjsf_rootSchema'; const UI_FIELD_KEY = 'ui:field'; const UI_WIDGET_KEY = 'ui:widget'; const UI_OPTIONS_KEY = 'ui:options'; const UI_GLOBAL_OPTIONS_KEY = 'ui:globalOptions'; /** Get all passed options from ui:options, and ui:<optionName>, returning them in an object with the `ui:` * stripped off. Any `globalOptions` will always be returned, unless they are overridden by options in the `uiSchema`. * * @param [uiSchema={}] - The UI Schema from which to get any `ui:xxx` options * @param [globalOptions={}] - The optional Global UI Schema from which to get any fallback `xxx` options * @returns - An object containing all the `ui:xxx` options with the `ui:` stripped off along with all `globalOptions` */ function getUiOptions(uiSchema = {}, globalOptions = {}) { return Object.keys(uiSchema) .filter((key) => key.indexOf('ui:') === 0) .reduce((options, key) => { const value = uiSchema[key]; if (key === UI_WIDGET_KEY && isObject(value)) { console.error('Setting options via ui:widget object is no longer supported, use ui:options instead'); return options; } if (key === UI_OPTIONS_KEY && isObject(value)) { return { ...options, ...value }; } return { ...options, [key.substring(3)]: value }; }, { ...globalOptions }); } /** Checks whether the field described by `schema`, having the `uiSchema` and `formData` supports expanding. The UI for * the field can expand if it has additional properties, is not forced as non-expandable by the `uiSchema` and the * `formData` object doesn't already have `schema.maxProperties` elements. * * @param schema - The schema for the field that is being checked * @param [uiSchema={}] - The uiSchema for the field * @param [formData] - The formData for the field * @returns - True if the schema element has additionalProperties, is expandable, and not at the maxProperties limit */ function canExpand(schema, uiSchema = {}, formData) { if (!schema.additionalProperties) { return false; } const { expandable = true } = getUiOptions(uiSchema); if (expandable === false) { return expandable; } // if ui:options.expandable was not explicitly set to false, we can add // another property if we have not exceeded maxProperties yet if (schema.maxProperties !== undefined && formData) { return Object.keys(formData).length < schema.maxProperties; } return true; } /** Given a `formData` object, recursively creates a `FormValidation` error handling structure around it * * @param formData - The form data around which the error handler is created * @returns - A `FormValidation` object based on the `formData` structure */ function createErrorHandler(formData) { const handler = { // We store the list of errors for this node in a property named __errors // to avoid name collision with a possible sub schema field named // 'errors' (see `utils.toErrorSchema`). [ERRORS_KEY]: [], addError(message) { this[ERRORS_KEY].push(message); }, }; if (Array.isArray(formData)) { return formData.reduce((acc, value, key) => { return { ...acc, [key]: createErrorHandler(value) }; }, handler); } if (isPlainObject(formData)) { const formObject = formData; return Object.keys(formObject).reduce((acc, key) => { return { ...acc, [key]: createErrorHandler(formObject[key]) }; }, handler); } return handler; } /** Implements a deep equals using the `lodash.isEqualWith` function, that provides a customized comparator that * assumes all functions are equivalent. * * @param a - The first element to compare * @param b - The second element to compare * @returns - True if the `a` and `b` are deeply equal, false otherwise */ function deepEquals(a, b) { return isEqualWith(a, b, (obj, other) => { if (typeof obj === 'function' && typeof other === 'function') { // Assume all functions are equivalent // see https://github.com/rjsf-team/react-jsonschema-form/issues/255 return true; } return undefined; // fallback to default isEquals behavior }); } /** Splits out the value at the `key` in `object` from the `object`, returning an array that contains in the first * location, the `object` minus the `key: value` and in the second location the `value`. * * @param key - The key from the object to extract * @param object - The object from which to extract the element * @returns - An array with the first value being the object minus the `key` element and the second element being the * value from `object[key]` */ function splitKeyElementFromObject(key, object) { const value = object[key]; const remaining = omit(object, [key]); return [remaining, value]; } /** Given the name of a `$ref` from within a schema, using the `rootSchema`, recursively look up and return the * sub-schema using the path provided by that reference. If `#` is not the first character of the reference, the path * does not exist in the schema, or the reference resolves circularly back to itself, then throw an Error. * Otherwise return the sub-schema. Also deals with nested `$ref`s in the sub-schema. * * @param $ref - The ref string for which the schema definition is desired * @param [rootSchema={}] - The root schema in which to search for the definition * @param recurseList - List of $refs already resolved to prevent recursion * @returns - The sub-schema within the `rootSchema` which matches the `$ref` if it exists * @throws - Error indicating that no schema for that reference could be resolved */ function findSchemaDefinitionRecursive($ref, rootSchema = {}, recurseList = []) { const ref = $ref || ''; let decodedRef; if (ref.startsWith('#')) { // Decode URI fragment representation. decodedRef = decodeURIComponent(ref.substring(1)); } else { throw new Error(`Could not find a definition for ${$ref}.`); } const current = jsonpointer.get(rootSchema, decodedRef); if (current === undefined) { throw new Error(`Could not find a definition for ${$ref}.`); } const nextRef = current[REF_KEY]; if (nextRef) { // Check for circular references. if (recurseList.includes(nextRef)) { if (recurseList.length === 1) { throw new Error(`Definition for ${$ref} is a circular reference`); } const [firstRef, ...restRefs] = recurseList; const circularPath = [...restRefs, ref, firstRef].join(' -> '); throw new Error(`Definition for ${firstRef} contains a circular reference through ${circularPath}`); } const [remaining, theRef] = splitKeyElementFromObject(REF_KEY, current); const subSchema = findSchemaDefinitionRecursive(theRef, rootSchema, [...recurseList, ref]); if (Object.keys(remaining).length > 0) { return { ...remaining, ...subSchema }; } return subSchema; } return current; } /** Given the name of a `$ref` from within a schema, using the `rootSchema`, look up and return the sub-schema using the * path provided by that reference. If `#` is not the first character of the reference, the path does not exist in * the schema, or the reference resolves circularly back to itself, then throw an Error. Otherwise return the * sub-schema. Also deals with nested `$ref`s in the sub-schema. * * @param $ref - The ref string for which the schema definition is desired * @param [rootSchema={}] - The root schema in which to search for the definition * @returns - The sub-schema within the `rootSchema` which matches the `$ref` if it exists * @throws - Error indicating that no schema for that reference could be resolved */ function findSchemaDefinition($ref, rootSchema = {}) { const recurseList = []; return findSchemaDefinitionRecursive($ref, rootSchema, recurseList); } /** Compares the value of `discriminatorField` within `formData` against the value of `discriminatorField` within schema for each `option`. * Returns index of first `option` whose discriminator matches formData. Returns `undefined` if there is no match. * This function does not work with discriminators of `"type": "object"` and `"type": "array"` * * @param formData - The current formData, if any, used to figure out a match * @param options - The list of options to find a matching options from * @param [discriminatorField] - The optional name of the field within the options object whose value is used to * determine which option is selected * @returns - The index of the matched option or undefined if there is no match */ function getOptionMatchingSimpleDiscriminator(formData, options, discriminatorField) { var _a; if (formData && discriminatorField) { const value = get(formData, discriminatorField); if (value === undefined) { return; } for (let i = 0; i < options.length; i++) { const option = options[i]; const discriminator = get(option, [PROPERTIES_KEY, discriminatorField], {}); if (discriminator.type === 'object' || discriminator.type === 'array') { continue; } if (discriminator.const === value) { return i; } if ((_a = discriminator.enum) === null || _a === void 0 ? void 0 : _a.includes(value)) { return i; } } } return; } /** Given the `formData` and list of `options`, attempts to find the index of the option that best matches the data. * Deprecated, use `getFirstMatchingOption()` instead. * * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary * @param formData - The current formData, if any, used to figure out a match * @param options - The list of options to find a matching options from * @param rootSchema - The root schema, used to primarily to look up `$ref`s * @param [discriminatorField] - The optional name of the field within the options object whose value is used to * determine which option is selected * @returns - The index of the matched option or 0 if none is available * @deprecated */ function getMatchingOption$1(validator, formData, options, rootSchema, discriminatorField) { // For performance, skip validating subschemas if formData is undefined. We just // want to get the first option in that case. if (formData === undefined) { return 0; } const simpleDiscriminatorMatch = getOptionMatchingSimpleDiscriminator(formData, options, discriminatorField); if (isNumber(simpleDiscriminatorMatch)) { return simpleDiscriminatorMatch; } for (let i = 0; i < options.length; i++) { const option = options[i]; // If we have a discriminator field, then we will use this to make the determination if (discriminatorField && has(option, [PROPERTIES_KEY, discriminatorField])) { const value = get(formData, discriminatorField); const discriminator = get(option, [PROPERTIES_KEY, discriminatorField], {}); if (validator.isValid(discriminator, value, rootSchema)) { return i; } } else if (option[PROPERTIES_KEY]) { // If the schema describes an object then we need to add slightly more // strict matching to the schema, because unless the schema uses the // "requires" keyword, an object will match the schema as long as it // doesn't have matching keys with a conflicting type. To do this we use an // "anyOf" with an array of requires. This augmentation expresses that the // schema should match if any of the keys in the schema are present on the // object and pass validation. // // Create an "anyOf" schema that requires at least one of the keys in the // "properties" object const requiresAnyOf = { anyOf: Object.keys(option[PROPERTIES_KEY]).map((key) => ({ required: [key], })), }; let augmentedSchema; // If the "anyOf" keyword already exists, wrap the augmentation in an "allOf" if (option.anyOf) { // Create a shallow clone of the option const { ...shallowClone } = option; if (!shallowClone.allOf) { shallowClone.allOf = []; } else { // If "allOf" already exists, shallow clone the array shallowClone.allOf = shallowClone.allOf.slice(); } shallowClone.allOf.push(requiresAnyOf); augmentedSchema = shallowClone; } else { augmentedSchema = Object.assign({}, option, requiresAnyOf); } // Remove the "required" field as it's likely that not all fields have // been filled in yet, which will mean that the schema is not valid delete augmentedSchema.required; if (validator.isValid(augmentedSchema, formData, rootSchema)) { return i; } } else if (validator.isValid(option, formData, rootSchema)) { return i; } } return 0; } /** Given the `formData` and list of `options`, attempts to find the index of the first option that matches the data. * Always returns the first option if there is nothing that matches. * * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary * @param formData - The current formData, if any, used to figure out a match * @param options - The list of options to find a matching options from * @param rootSchema - The root schema, used to primarily to look up `$ref`s * @param [discriminatorField] - The optional name of the field within the options object whose value is used to * determine which option is selected * @returns - The index of the first matched option or 0 if none is available */ function getFirstMatchingOption(validator, formData, options, rootSchema, discriminatorField) { return getMatchingOption$1(validator, formData, options, rootSchema, discriminatorField); } /** Returns the `discriminator.propertyName` when defined in the `schema` if it is a string. A warning is generated when * it is not a string. Returns `undefined` when a valid discriminator is not present. * * @param schema - The schema from which the discriminator is potentially obtained * @returns - The `discriminator.propertyName` if it exists in the schema, otherwise `undefined` */ function getDiscriminatorFieldFromSchema(schema) { let discriminator; const maybeString = get(schema, 'discriminator.propertyName', undefined); if (isString(maybeString)) { discriminator = maybeString; } else if (maybeString !== undefined) { console.warn(`Expecting discriminator to be a string, got "${typeof maybeString}" instead`); } return discriminator; } /** Given a specific `value` attempts to guess the type of a schema element. In the case where we have to implicitly * create a schema, it is useful to know what type to use based on the data we are defining. * * @param value - The value from which to guess the type * @returns - The best guess for the object type */ function guessType(value) { if (Array.isArray(value)) { return 'array'; } if (typeof value === 'string') { return 'string'; } if (value == null) { return 'null'; } if (typeof value === 'boolean') { return 'boolean'; } if (!isNaN(value)) { return 'number'; } if (typeof value === 'object') { return 'object'; } // Default to string if we can't figure it out return 'string'; } /** Gets the type of a given `schema`. If the type is not explicitly defined, then an attempt is made to infer it from * other elements of the schema as follows: * - schema.const: Returns the `guessType()` of that value * - schema.enum: Returns `string` * - schema.properties: Returns `object` * - schema.additionalProperties: Returns `object` * - type is an array with a length of 2 and one type is 'null': Returns the other type * * @param schema - The schema for which to get the type * @returns - The type of the schema */ function getSchemaType(schema) { let { type } = schema; if (!type && schema.const) { return guessType(schema.const); } if (!type && schema.enum) { return 'string'; } if (!type && (schema.properties || schema.additionalProperties)) { return 'object'; } if (Array.isArray(type)) { if (type.length === 2 && type.includes('null')) { type = type.find((type) => type !== 'null'); } else { type = type[0]; } } return type; } /** Recursively merge deeply nested schemas. The difference between `mergeSchemas` and `mergeObjects` is that * `mergeSchemas` only concats arrays for values under the 'required' keyword, and when it does, it doesn't include * duplicate values. * * @param obj1 - The first schema object to merge * @param obj2 - The second schema object to merge * @returns - The merged schema object */ function mergeSchemas(obj1, obj2) { const acc = Object.assign({}, obj1); // Prevent mutation of source object. return Object.keys(obj2).reduce((acc, key) => { const left = obj1 ? obj1[key] : {}, right = obj2[key]; if (obj1 && key in obj1 && isObject(right)) { acc[key] = mergeSchemas(left, right); } else if (obj1 && obj2 && (getSchemaType(obj1) === 'object' || getSchemaType(obj2) === 'object') && key === REQUIRED_KEY && Array.isArray(left) && Array.isArray(right)) { // Don't include duplicate values when merging 'required' fields. acc[key] = union(left, right); } else { acc[key] = right; } return acc; }, acc); } /** Retrieves an expanded schema that has had all of its conditions, additional properties, references and dependencies * resolved and merged into the `schema` given a `validator`, `rootSchema` and `rawFormData` that is used to do the * potentially recursive resolution. * * @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs * @param schema - The schema for which retrieving a schema is desired * @param [rootSchema={}] - The root schema that will be forwarded to all the APIs * @param [rawFormData] - The current formData, if any, to assist retrieving a schema * @param [experimental_customMergeAllOf] - Optional function that allows for custom merging of `allOf` schemas * @returns - The schema having its conditions, additional properties, references and dependencies resolved */ function retrieveSchema(validator, schema, rootSchema = {}, rawFormData, experimental_customMergeAllOf) { return retrieveSchemaInternal(validator, schema, rootSchema, rawFormData, undefined, undefined, experimental_customMergeAllOf)[0]; } /** Resolves a conditional block (if/else/then) by removing the condition and merging the appropriate conditional branch * with the rest of the schema. If `expandAllBranches` is true, then the `retrieveSchemaInteral()` results for both * conditions will be returned. * * @param validator - An implementation of the `ValidatorType` interface that is used to detect valid schema conditions * @param schema - The schema for which resolving a condition is desired * @param rootSchema - The root schema that will be forwarded to all the APIs * @param expandAllBranches - Flag, if true, will return all possible branches of conditions, any/oneOf and * dependencies as a list of schemas * @param recurseList - The list of recursive references already processed * @param [formData] - The current formData to assist retrieving a schema * @param [experimental_customMergeAllOf] - Optional function that allows for custom merging of `allOf` schemas * @returns - A list of schemas with the appropriate conditions resolved, possibly with all branches expanded */ function resolveCondition(validator, schema, rootSchema, expandAllBranches, recurseList, formData, experimental_customMergeAllOf) { const { if: expression, then, else: otherwise, ...resolvedSchemaLessConditional } = schema; const conditionValue = validator.isValid(expression, formData || {}, rootSchema); let resolvedSchemas = [resolvedSchemaLessConditional]; let schemas = []; if (expandAllBranches) { if (then && typeof then !== 'boolean') { schemas = schemas.concat(retrieveSchemaInternal(validator, then, rootSchema, formData, expandAllBranches, recurseList, experimental_customMergeAllOf)); } if (otherwise && typeof otherwise !== 'boolean') { schemas = schemas.concat(retrieveSchemaInternal(validator, otherwise, rootSchema, formData, expandAllBranches, recurseList, experimental_customMergeAllOf)); } } else { const conditionalSchema = conditionValue ? then : otherwise; if (conditionalSchema && typeof conditionalSchema !== 'boolean') { schemas = schemas.concat(retrieveSchemaInternal(validator, conditionalSchema, rootSchema, formData, expandAllBranches, recurseList, experimental_customMergeAllOf)); } } if (schemas.length) { resolvedSchemas = schemas.map((s) => mergeSchemas(resolvedSchemaLessConditional, s)); } return resolvedSchemas.flatMap((s) => retrieveSchemaInternal(validator, s, rootSchema, formData, expandAllBranches, recurseList, experimental_customMergeAllOf)); } /** Given a list of lists of allOf, anyOf or oneOf values, create a list of lists of all permutations of the values. The * `listOfLists` is expected to be all resolved values of the 1st...nth schemas within an `allOf`, `anyOf` or `oneOf`. * From those lists, build a matrix for each `xxxOf` where there is more than one schema for a row in the list of lists. * * For example: * - If there are three xxxOf rows (A, B, C) and they have been resolved such that there is only one A, two B and three * C schemas then: * - The permutation for the first row is `[[A]]` * - The permutations for the second row are `[[A,B1], [A,B2]]` * - The permutations for the third row are `[[A,B1,C1], [A,B1,C2], [A,B1,C3], [A,B2,C1], [A,B2,C2], [A,B2,C3]]` * * @param listOfLists - The list of lists of elements that represent the allOf, anyOf or oneOf resolved values in order * @returns - The list of all permutations of schemas for a set of `xxxOf`s */ function getAllPermutationsOfXxxOf(listOfLists) { const allPermutations = listOfLists.reduce((permutations, list) => { // When there are more than one set of schemas for a row, duplicate the set of permutations and add in the values if (list.length > 1) { return list.flatMap((element) => times(permutations.length, (i) => [...permutations[i]].concat(element))); } // Otherwise just push in the single value into the current set of permutations permutations.forEach((permutation) => permutation.push(list[0])); return permutations; }, [[]] // Start with an empty list ); return allPermutations; } /** Resolves references and dependencies within a schema and its 'allOf' children. Passes the `expandAllBranches` flag * down to the `retrieveSchemaInternal()`, `resolveReference()` and `resolveDependencies()` helper calls. If * `expandAllBranches` is true, then all possible dependencies and/or allOf branches are returned. * * @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs * @param schema - The schema for which resolving a schema is desired * @param rootSchema - The root schema that will be forwarded to all the APIs * @param expandAllBranches - Flag, if true, will return all possible branches of conditions, any/oneOf and dependencies * as a list of schemas * @param recurseList - The list of recursive references already processed * @param [formData] - The current formData, if any, to assist retrieving a schema * @param [experimental_customMergeAllOf] - Optional function that allows for custom merging of `allOf` schemas * @returns - The list of schemas having its references, dependencies and allOf schemas resolved */ function resolveSchema(validator, schema, rootSchema, expandAllBranches, recurseList, formData, experimental_customMergeAllOf) { const updatedSchemas = resolveReference(validator, schema, rootSchema, expandAllBranches, recurseList, formData); if (updatedSchemas.length > 1 || updatedSchemas[0] !== schema) { // return the updatedSchemas array if it has either multiple schemas within it // OR the first schema is not the same as the original schema return updatedSchemas; } if (DEPENDENCIES_KEY in schema) { const resolvedSchemas = resolveDependencies(validator, schema, rootSchema, expandAllBranches, recurseList, formData); return resolvedSchemas.flatMap((s) => { return retrieveSchemaInternal(validator, s, rootSchema, formData, expandAllBranches, recurseList, experimental_customMergeAllOf); }); } if (ALL_OF_KEY in schema && Array.isArray(schema.allOf)) { const allOfSchemaElements = schema.allOf.map((allOfSubschema) => retrieveSchemaInternal(validator, allOfSubschema, rootSchema, formData, expandAllBranches, recurseList, experimental_customMergeAllOf)); const allPermutations = getAllPermutationsOfXxxOf(allOfSchemaElements); return allPermutations.map((permutation) => ({ ...schema, allOf: permutation, })); } // No $ref or dependencies or allOf attribute was found, returning the original schema. return [schema]; } /** Resolves all references within a schema and then returns the `retrieveSchemaInternal()` if the resolved schema is * actually different than the original. Passes the `expandAllBranches` flag down to the `retrieveSchemaInternal()` * helper call. * * @param validator - An implementation of the `ValidatorType` interface that will be forwarded to all the APIs * @param schema - The schema for which resolving a reference is desired * @param rootSchema - The root schema that will be forwarded to all the APIs * @param expandAllBranches - Flag, if true, will return all possible branches of conditions, any/oneOf and dependencies * as a list of schemas * @param recurseList - The list of recursive references already processed * @param [formData] - The current formData, if any, to assist retrieving a schema * @param [experimental_customMergeAllOf] - Optional function that allows for custom merging of `allOf` schemas * @returns - The list schemas retrieved after having all references resolved */ function resolveReference(validator, schema, rootSchema, expandAllBranches, recurseList, formData, experimental_customMergeAllOf) { const updatedSchema = resolveAllReferences(schema, rootSchema, recurseList); if (updatedSchema !== schema) { // Only call this if the schema was actually changed by the `resolveAllReferences()` function return retrieveSchemaInternal(validator, updatedSchema, rootSchema, formData, expandAllBranches, recurseList, experimental_customMergeAllOf); } return [schema]; } /** Resolves all references within the schema itself as well as any of its properties and array items. * * @param schema - The schema for which resolving all references is desired * @param rootSchema - The root schema that will be forwarded to all the APIs * @param recurseList - List of $refs already resolved to prevent recursion * @returns - given schema will all references resolved or the original schema if no internal `$refs` were resolved */ function resolveAllReferences(schema, rootSchema, recurseList) { if (!isObject(schema)) { return schema; } let resolvedSchema = schema; // resolve top level ref if (REF_KEY in resolvedSchema) { const { $ref, ...localSchema } = resolvedSchema; // Check for a recursive reference and stop the loop if (recurseList.includes($ref)) { return resolvedSchema; } recurseList.push($ref); // Retrieve the referenced