UNPKG

@airplane/views

Version:

A React library for building Airplane views. Views components are optimized in style and functionality to produce internal apps that are easy to build and maintain.

445 lines (444 loc) 17.3 kB
import { jsx } from "react/jsx-runtime"; import { Input } from "@mantine/core"; import dayjs from "dayjs"; import json5 from "json5"; import { isArray } from "lodash-es"; import { useRef, useState, useEffect } from "react"; import { useDebouncedCallback } from "use-debounce"; import { isTaskOption, isConstraintOptions, isTemplate } from "../../client/types.js"; import { Checkbox } from "../checkbox/Checkbox.js"; import { CodeInput } from "../codeinput/CodeInput.js"; import { DatePicker } from "../datepicker/DatePicker.js"; import { DateTimePicker } from "../datepicker/DateTimePicker.js"; import { FileInput } from "../fileinput/FileInput.js"; import { MultiInput as MultiInput$1 } from "../multiInput/MultiInput.js"; import { MultiSelect } from "../multiselect/MultiSelect.js"; import { NumberInput } from "../number/NumberInput.js"; import { Select, outputToData } from "../select/Select.js"; import { Textarea } from "../textarea/Textarea.js"; import { TextInput } from "../textinput/TextInput.js"; import { useRegisterFormInput } from "../../state/components/form/useRegisterFormInput.js"; import { useInput } from "../../state/components/input/useInput.js"; import { useMultiInputState } from "../../state/components/multiInput/useMultiInputState.js"; import { useComponentId } from "../../state/components/useId.js"; import { useSyncComponentState } from "../../state/context/context.js"; import { useEvaluateTemplate, useEvaluateTemplates } from "./jst.js"; import { useTaskQuery } from "../../state/tasks/useTaskQuery.js"; const PARAM_CONFIG_MAP = { boolean: { getInput: (props) => /* @__PURE__ */ jsx(Checkbox, { ...props, checked: props.value }), validate: (val, slug) => { if (typeof val !== "boolean") { return `Value of param ${slug} must be a boolean`; } } }, upload: { getInput: (props) => /* @__PURE__ */ jsx(FileInput, { ...props }), validate: () => { return "Cannot set value constraints for file params"; } }, date: { getInput: (props) => /* @__PURE__ */ jsx(DatePicker, { ...props }), validate: (val, slug, constraints) => { if (typeof val !== "string" && !(val instanceof Date) || typeof val === "string" && !dayjs(val).isValid() || constraints && constraints.find((v) => typeof v === "string" && new Date(v).getTime() === new Date(val).getTime()) === void 0) { return `${val} is not a valid value for ${slug}`; } } }, datetime: { getInput: (props) => /* @__PURE__ */ jsx(DateTimePicker, { ...props }), validate: (val, slug, constraints) => { if (typeof val !== "string" && !(val instanceof Date) || typeof val === "string" && !dayjs(val).isValid() || constraints && constraints.find((v) => typeof v === "string" && new Date(v).getTime() === new Date(val).getTime()) === void 0) { return `${val} is not a valid value for ${slug}`; } } }, float: { getInput: (props) => /* @__PURE__ */ jsx(NumberInput, { precision: 9, removeTrailingZeros: true, ...props }), validate: (val, slug, constraints) => { if (typeof val !== "number") { return `Value of param ${slug} must be a number`; } if (constraints && constraints.find((v) => Number(v) == val) === void 0) { return `${val} is not a valid value for ${slug}`; } } }, integer: { getInput: (props) => /* @__PURE__ */ jsx(NumberInput, { ...props }), validate: (val, slug, constraints) => { if (typeof val !== "number") { return `Value of param ${slug} must be a number`; } if (constraints && constraints.find((v) => Number(v) == val) === void 0) { return `${val} is not a valid value for ${slug}`; } } }, string: { getInput: (props, component) => { if (component === "textarea") { return /* @__PURE__ */ jsx(Textarea, { ...props }); } else if (component === "editor-sql") { return /* @__PURE__ */ jsx(CodeInput, { ...props, language: "sql" }); } return /* @__PURE__ */ jsx(TextInput, { ...props, onChange: (v) => { var _a; return (_a = props.onChange) == null ? void 0 : _a.call(props, v.target.value); } }); }, validate: (val, slug, constraints) => { if (typeof val !== "string") { return `Value of param ${slug} must be a string`; } if (constraints && !constraints.includes(val)) { return `${val} is not a valid value for ${slug}`; } } }, json: { getInput: (props) => { return /* @__PURE__ */ jsx(CodeInput, { ...props, language: "json" }); }, validate: (val, slug) => { if (typeof val !== "string") { return `Value of param ${slug} must be a string`; } try { json5.parse(val); } catch { return `Value of param ${slug} must be valid JSON`; } } } }; const ParameterInput = ({ param, idPrefix, opt, paramValues, onChange, value }) => { var _a, _b; const defaultValue = (opt == null ? void 0 : opt.defaultValue) ?? param.default; const hiddenEval = useEvaluateTemplate(param.hidden, { params: paramValues }, { forceEvaluate: true }); const isHidden = param.hidden && (hiddenEval.result || hiddenEval.initialLoading); const validateEval = useEvaluateTemplate(param.constraints.validate, { params: paramValues }, { forceEvaluate: true }); const taskBackedConstraintOptionsLoaded = useRef(false); const { constraintOptions: taskBackedConstraintOptions, error: taskBackedConstraintError, isLoading: taskBackedConstraintLoading } = useTaskBackedConstraintOptions({ param, paramValues: paramValues ?? {}, enabled: !isHidden }); if (value && isTaskOption(param.constraints.options) && taskBackedConstraintOptionsLoaded.current && !(taskBackedConstraintOptions == null ? void 0 : taskBackedConstraintOptions.some((o) => { if (isArray(value)) { return value.includes(o.value); } return o.value === value; }))) { onChange(void 0); } if (taskBackedConstraintOptions && !taskBackedConstraintOptionsLoaded.current) { if ((opt == null ? void 0 : opt.value) ?? defaultValue) { onChange(canonicalizeValue((opt == null ? void 0 : opt.value) ?? defaultValue, param.type)); } taskBackedConstraintOptionsLoaded.current = true; } if (isHidden) { return null; } if ((opt == null ? void 0 : opt.value) !== void 0) { return /* @__PURE__ */ jsx(Input.Label, { children: `${param.slug}: ${JSON.stringify(opt.value)}` }); } const props = { required: !param.constraints.optional, id: idPrefix + param.slug, label: param.name, ...param.type === "boolean" ? { defaultChecked: defaultValue == null ? void 0 : !!defaultValue } : { defaultValue: defaultValue == null ? void 0 : defaultValue }, validate: (e) => { var _a2; const optValidationResult = (_a2 = opt == null ? void 0 : opt.validate) == null ? void 0 : _a2.call(opt, e); if (optValidationResult) { return optValidationResult; } if (param.constraints.regex) { const regex = new RegExp(param.constraints.regex); let valsToTest = [e]; if (Array.isArray(e)) { valsToTest = e; } for (const val of valsToTest) { if (typeof val === "string" && !regex.test(val)) { return `${param.name} does not match the following pattern: ${param.constraints.regex}`; } } } if (validateEval.result) { return typeof validateEval.result === "string" ? validateEval.result : JSON.stringify(validateEval.result); } if (hiddenEval.error) { return `Error evaluating hidden expression for ${param.name}: ${hiddenEval.error}`; } if (validateEval.error) { return `Error evaluating validation expression for ${param.name}: ${validateEval.error}`; } return void 0; }, // This is not param?.desc because we don't want to pass an empty string desc description: param.desc || void 0, disabled: opt == null ? void 0 : opt.disabled }; let constraintOptions = void 0; if ((_a = param.constraints) == null ? void 0 : _a.options) { if (isConstraintOptions(param.constraints.options)) { constraintOptions = param.constraints.options; } else if (taskBackedConstraintOptions) { constraintOptions = taskBackedConstraintOptions; } } let options = void 0; if (opt == null ? void 0 : opt.allowedValues) { options = canonicalizeValues(opt.allowedValues, param.type); if (constraintOptions) { options = filterValues(options, constraintOptions, param.type); } } else if (constraintOptions) { options = constraintOptions.map((v) => ({ label: v.label || String(v.value), // Override not set or empty ("") label value: typeof v.value === "number" ? v.value : String(v.value) })); } if (options || isTaskOption(param.constraints.options)) { if (param.multi) { return /* @__PURE__ */ jsx(MultiSelect, { clearable: true, ...props, defaultValue: canonicalizeValues(defaultValue ?? [], param.type), data: options ?? [], loading: taskBackedConstraintLoading, error: taskBackedConstraintError }); } return /* @__PURE__ */ jsx(Select, { clearable: true, ...props, defaultValue: canonicalizeValue(defaultValue, param.type), data: options ?? [], loading: taskBackedConstraintLoading, error: taskBackedConstraintError }); } if (param.multi) { const persistDefaultValueType = param.type === "integer" || param.type === "boolean"; return /* @__PURE__ */ jsx(MultiInput, { id: props.id, label: props.label, description: props.description, disabled: props.disabled, paramType: param.type, paramComponent: param.component, required: props.required, defaultValue: persistDefaultValueType ? defaultValue ?? [] : canonicalizeValues(defaultValue ?? [], param.type), validate: props.validate }); } return (_b = PARAM_CONFIG_MAP[param.type]) == null ? void 0 : _b.getInput(props, param.component); }; const MultiInput = ({ id: propsId, label, description, paramType, paramComponent, disabled, required, defaultValue = [], validate }) => { const id = useComponentId(propsId); const { state, dispatch } = useMultiInputState(id, { initialState: { disabled, value: defaultValue } }); const { inputProps } = useInput({ required, validate }, state, dispatch, () => []); useSyncComponentState(id, state); useRegisterFormInput(id, "multi-input"); const values = state.value ?? []; return /* @__PURE__ */ jsx(MultiInput$1, { id, label, description, onAdd: () => dispatch({ type: "setValue", value: [...state.value ?? [], void 0] }), onRemove: (i) => { dispatch({ type: "setValue", value: [...(state.value ?? []).slice(0, i), ...(state.value ?? []).slice(i + 1)] }); }, values, addDisabled: values.length > 0 && values[values.length - 1] == void 0, required, renderInput: ({ index, value }) => { var _a; return (_a = PARAM_CONFIG_MAP[paramType]) == null ? void 0 : _a.getInput({ required: false, id: `${id}-${index}`, label: "", value, disabled, onChange: (v) => { const currentValues = state.value ?? []; dispatch({ type: "setValue", value: [...currentValues.slice(0, index), v, ...currentValues.slice(index + 1)] }); } }, paramComponent); }, ...inputProps }); }; const validateParameterOptions = (params, opts) => { var _a, _b, _c; for (const param of params) { if (!param.constraints.optional && (opts.shownFields && !opts.shownFields.includes(param.slug) || ((_a = opts.hiddenFields) == null ? void 0 : _a.includes(param.slug))) && (!opts.fieldOptions || ((_b = opts.fieldOptions.find((o) => o.slug === param.slug)) == null ? void 0 : _b.value) === void 0)) { return "All required fields that are hidden must have a value specified"; } } if (opts.fieldOptions) { for (const opt of opts.fieldOptions) { const param = params.find((p) => p.slug === opt.slug); if (!param) { return `Found extraneous param in fieldOptions: ${opt.slug}`; } if (opt.allowedValues && opt.allowedValues.length < 2) { return `allowedValues for param ${opt.slug} must have length at least 2`; } const constraints = isConstraintOptions(param.constraints.options) ? param.constraints.options.map((v) => v.value) : void 0; let valuesToCheck = opt.allowedValues || []; if (opt.defaultValue !== void 0) { if (param.multi && !Array.isArray(opt.defaultValue)) { return `defaultValue for multi param ${opt.slug} must be an array`; } else if (!param.multi && Array.isArray(opt.defaultValue)) { return `defaultValue for ${opt.slug} cannot be an array`; } const dv = Array.isArray(opt.defaultValue) ? opt.defaultValue : [opt.defaultValue]; valuesToCheck = [...valuesToCheck, ...dv.flat()]; } if (opt.value !== void 0) { if (param.multi && !Array.isArray(opt.value)) { return `value for multi param ${opt.slug} must be an array`; } else if (!param.multi && Array.isArray(opt.value)) { return `value for param ${opt.slug} cannot be an array`; } const v = Array.isArray(opt.value) ? opt.value : [opt.value]; valuesToCheck = [...valuesToCheck, ...v.flat()]; } for (const val of valuesToCheck) { const validateResult = (_c = PARAM_CONFIG_MAP[param.type]) == null ? void 0 : _c.validate(val, opt.slug, constraints); if (validateResult) { return validateResult; } } } } }; const canonicalizeValue = (value, type) => { if (value === void 0) { return void 0; } if (value instanceof Date) { return value.toISOString(); } if (typeof value === "number" || typeof value === "boolean") { return String(value); } if (typeof value === "string") { if (type === "date") { const date = dayjs(value); return date.isValid() ? date.format("YYYY-MM-DD") : ""; } else if (type === "datetime") { const date = dayjs(value); return date.isValid() ? date.toISOString() : ""; } else { return value; } } return JSON.stringify(value); }; const canonicalizeValues = (values, type) => { return values.map((v) => canonicalizeValue(v, type)).filter((v) => v !== void 0); }; const filterValues = (values, constraints, type) => { if (type === "date" || type === "datetime") { const constraintValues = constraints.map((v) => new Date(String(v.value)).getTime()); return values.filter((v) => constraintValues.includes(new Date(v).getTime())); } else { const constraintValues = constraints.map((v) => v.value); return values.filter((v) => constraintValues.includes(v)); } }; const useTaskBackedConstraintOptions = ({ param, paramValues: inputParamValues, enabled: inputEnabled = true }) => { const [paramValues, setParamValues] = useState(inputParamValues); const [enabled, setEnabled] = useState(inputEnabled); const debounced = useDebouncedCallback((pv) => { setParamValues(pv); }, 500); useEffect(() => { debounced(inputParamValues); }, [debounced, inputParamValues]); useEffect(() => { setParamValues(inputParamValues); setEnabled(inputEnabled); }, [inputEnabled]); const taskBackedOptions = isTaskOption(param.constraints.options) ? param.constraints.options : void 0; const canContainTemplate = (v) => isTemplate(v) || typeof v === "string"; const taskBackedParams = Object.entries((taskBackedOptions == null ? void 0 : taskBackedOptions.params) ?? {}); const taskBackedParamTemplates = taskBackedParams.map((p) => { return canContainTemplate(p[1]) ? p[1] : ""; }); const { results, errors, initialLoading } = useEvaluateTemplates(enabled ? taskBackedParamTemplates : void 0, { params: paramValues }); const evaluatedTaskBackedParamMap = taskBackedParams.map((p, i) => { if (canContainTemplate(p[1])) { return [p[0], results[i]]; } return p; }); const evaluatedTaskBackedParams = Object.fromEntries(evaluatedTaskBackedParamMap); const { output: taskBackedOutput, error: executeError, loading: isExecuting // eslint-disable-next-line @typescript-eslint/no-explicit-any } = useTaskQuery({ slug: (taskBackedOptions == null ? void 0 : taskBackedOptions.slug) ?? "", params: evaluatedTaskBackedParams, enabled: enabled && !initialLoading && !!(taskBackedOptions == null ? void 0 : taskBackedOptions.slug) }); const data = taskBackedOutput ? outputToData(taskBackedOutput) : null; const constraintOptions = data == null ? void 0 : data.map((d) => ({ value: typeof d === "object" ? d.value : d, label: typeof d === "object" ? d.label || String(d.value) : String(d) })); return { constraintOptions, error: [...errors, executeError == null ? void 0 : executeError.message].filter((e) => !!e).join("\n"), isLoading: initialLoading || isExecuting }; }; export { ParameterInput, validateParameterOptions }; //# sourceMappingURL=parameters.js.map