@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
JavaScript
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