@kelvininc/react-ui-components
Version:
Kelvin UI Components for React applications
1,028 lines (1,004 loc) • 458 kB
JavaScript
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