* feat:add prefilcheck check box in the field creation * chore:formatted the li8 translations * feat:add e2e test case for this feature * refactor:reanmed the key from prefilledCheck to disableOnPrefill and moved the readOnly logic to formbuilderField * chore:fixed the type check * fix:if prefilled has error. do not disabble it * chore:removed the e2e tests for prefill in e2e * chore:revert back to old changes * chore:remove unwanted code frome2e * Support name field as well * refactor:handled the feedback changes. still unit test cases pending * feat: add unit tests for disable field on prefill logic * chore: resolved the type check and typos * chore:renamed the files and type clean up * Apply suggestions from code review * chore:renaming test files and moving the disableonprefill hook to formbuilderFeild. * refactor: add unit test for serach params and form values check and minor code formatting and file name changes * fix: validating the input value partially at the intialising phase. so that form response values and from ui values are same and making sure invalid data not sent to backend on submit. * chore:fix the type check issue * fix: field value valdiation at initilisation and add the unit test cases for that change * chore:add comments to the tests * chore:resolve type check * chore: moved the tests to the formBuilder * Simplify codebase by ensuring select and multiselect changes the value when no option is found * Relocate and refactor tests * handle special cases * Dont disable for dirty fields * Update schema.prisma --------- Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: Hariom <hariombalhara@gmail.com>
377 lines
9.5 KiB
TypeScript
377 lines
9.5 KiB
TypeScript
import dynamic from "next/dynamic";
|
|
import type { ChangeEvent } from "react";
|
|
import type {
|
|
ButtonGroupProps,
|
|
ButtonProps,
|
|
ConjsProps,
|
|
FieldProps,
|
|
ProviderProps,
|
|
} from "react-awesome-query-builder";
|
|
|
|
import { Button as CalButton, TextField, TextArea } from "@calcom/ui";
|
|
import { Icon } from "@calcom/ui";
|
|
|
|
const Select = dynamic(
|
|
async () => (await import("@calcom/ui")).SelectWithValidation
|
|
) as unknown as typeof import("@calcom/ui").SelectWithValidation;
|
|
|
|
export type CommonProps<
|
|
TVal extends
|
|
| string
|
|
| boolean
|
|
| string[]
|
|
| {
|
|
value: string;
|
|
optionValue: string;
|
|
}
|
|
> = {
|
|
placeholder?: string;
|
|
readOnly?: boolean;
|
|
className?: string;
|
|
name?: string;
|
|
label?: string;
|
|
value: TVal;
|
|
setValue: (value: TVal) => void;
|
|
/**
|
|
* required and other validations are supported using zodResolver from react-hook-form
|
|
*/
|
|
// required?: boolean;
|
|
};
|
|
|
|
export type SelectLikeComponentProps<
|
|
TVal extends
|
|
| string
|
|
| string[]
|
|
| {
|
|
value: string;
|
|
optionValue: string;
|
|
} = string
|
|
> = {
|
|
options: {
|
|
label: string;
|
|
value: TVal extends (infer P)[]
|
|
? P
|
|
: TVal extends {
|
|
value: string;
|
|
}
|
|
? TVal["value"]
|
|
: TVal;
|
|
}[];
|
|
} & CommonProps<TVal>;
|
|
|
|
export type SelectLikeComponentPropsRAQB<TVal extends string | string[] = string> = {
|
|
listValues: { title: string; value: TVal extends (infer P)[] ? P : TVal }[];
|
|
} & CommonProps<TVal>;
|
|
|
|
export type TextLikeComponentProps<TVal extends string | string[] | boolean = string> = CommonProps<TVal> & {
|
|
name?: string;
|
|
};
|
|
|
|
export type TextLikeComponentPropsRAQB<TVal extends string | boolean = string> =
|
|
TextLikeComponentProps<TVal> & {
|
|
customProps?: object;
|
|
type?: "text" | "number" | "email" | "tel" | "url";
|
|
maxLength?: number;
|
|
noLabel?: boolean;
|
|
};
|
|
|
|
const TextAreaWidget = (props: TextLikeComponentPropsRAQB) => {
|
|
const { value, setValue, readOnly, placeholder, maxLength, customProps, ...remainingProps } = props;
|
|
|
|
const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
const val = e.target.value;
|
|
setValue(val);
|
|
};
|
|
|
|
const textValue = value || "";
|
|
return (
|
|
<TextArea
|
|
value={textValue}
|
|
placeholder={placeholder}
|
|
disabled={readOnly}
|
|
onChange={onChange}
|
|
maxLength={maxLength}
|
|
{...customProps}
|
|
{...remainingProps}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const TextWidget = (props: TextLikeComponentPropsRAQB) => {
|
|
const {
|
|
value,
|
|
noLabel,
|
|
setValue,
|
|
readOnly,
|
|
placeholder,
|
|
customProps,
|
|
type = "text",
|
|
...remainingProps
|
|
} = props;
|
|
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
const val = e.target.value;
|
|
setValue(val);
|
|
};
|
|
const textValue = value || "";
|
|
return (
|
|
<TextField
|
|
containerClassName="w-full"
|
|
type={type}
|
|
value={textValue}
|
|
labelSrOnly={noLabel}
|
|
placeholder={placeholder}
|
|
disabled={readOnly}
|
|
onChange={onChange}
|
|
{...remainingProps}
|
|
{...customProps}
|
|
/>
|
|
);
|
|
};
|
|
|
|
function NumberWidget({ value, setValue, ...remainingProps }: TextLikeComponentPropsRAQB) {
|
|
return (
|
|
<TextField
|
|
type="number"
|
|
labelSrOnly={remainingProps.noLabel}
|
|
containerClassName="w-full"
|
|
className="bg-default border-default disabled:bg-emphasis focus:ring-brand-default dark:focus:border-emphasis focus:border-subtle block w-full rounded-md text-sm disabled:hover:cursor-not-allowed"
|
|
value={value}
|
|
onChange={(e) => {
|
|
setValue(e.target.value);
|
|
}}
|
|
{...remainingProps}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const MultiSelectWidget = ({
|
|
listValues,
|
|
setValue,
|
|
value,
|
|
...remainingProps
|
|
}: SelectLikeComponentPropsRAQB<string[]>) => {
|
|
if (!listValues) {
|
|
return null;
|
|
}
|
|
const selectItems = listValues.map((item) => {
|
|
return {
|
|
label: item.title,
|
|
value: item.value,
|
|
};
|
|
});
|
|
|
|
const optionsFromList = selectItems.filter((item) => value?.includes(item.value));
|
|
|
|
// If no value could be found in the list, then we set the value to undefined.
|
|
// This is to update the value back to the source that we couldn't set it. This is important otherwise the outside party thinks that the value is set but it is not.
|
|
// Do it only when it is not already empty, this is to avoid infinite state updates
|
|
// NOTE: value is some times sent as undefined even though the type will tell you that it can't be
|
|
if (optionsFromList.length === 0 && value?.length) {
|
|
setValue([]);
|
|
}
|
|
|
|
return (
|
|
<Select
|
|
aria-label="multi-select-dropdown"
|
|
className="mb-2"
|
|
onChange={(items) => {
|
|
setValue(items?.map((item) => item.value));
|
|
}}
|
|
value={optionsFromList}
|
|
isMulti={true}
|
|
isDisabled={remainingProps.readOnly}
|
|
options={selectItems}
|
|
{...remainingProps}
|
|
/>
|
|
);
|
|
};
|
|
|
|
function SelectWidget({ listValues, setValue, value, ...remainingProps }: SelectLikeComponentPropsRAQB) {
|
|
if (!listValues) {
|
|
return null;
|
|
}
|
|
const selectItems = listValues.map((item) => {
|
|
return {
|
|
label: item.title,
|
|
value: item.value,
|
|
};
|
|
});
|
|
const optionFromList = selectItems.find((item) => item.value === value);
|
|
|
|
// If the value is not in the list, then we set the value to undefined.
|
|
// This is to update the value back to the source that we couldn't set it. This is important otherwise the outside party thinks that the value is set but it is not.
|
|
// Do it only when it is not already empty string, this is to avoid infinite state updates
|
|
if (!optionFromList && value) {
|
|
setValue("");
|
|
}
|
|
|
|
return (
|
|
<Select
|
|
aria-label="select-dropdown"
|
|
className="data-testid-select mb-2"
|
|
onChange={(item) => {
|
|
if (!item) {
|
|
return;
|
|
}
|
|
setValue(item.value);
|
|
}}
|
|
isDisabled={remainingProps.readOnly}
|
|
value={optionFromList}
|
|
options={selectItems}
|
|
{...remainingProps}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function Button({ config, type, label, onClick, readonly }: ButtonProps) {
|
|
if (type === "delRule" || type == "delGroup") {
|
|
return (
|
|
<button className="ml-5">
|
|
<Icon name="trash" className="text-subtle m-0 h-4 w-4" onClick={onClick} />
|
|
</button>
|
|
);
|
|
}
|
|
let dataTestId = "";
|
|
if (type === "addRule") {
|
|
label = config?.operators.__calReporting ? "Add Filter" : "Add rule";
|
|
dataTestId = "add-rule";
|
|
} else if (type == "addGroup") {
|
|
label = "Add rule group";
|
|
dataTestId = "add-rule-group";
|
|
}
|
|
return (
|
|
<CalButton
|
|
StartIcon="plus"
|
|
data-testid={dataTestId}
|
|
type="button"
|
|
color="secondary"
|
|
disabled={readonly}
|
|
onClick={onClick}>
|
|
{label}
|
|
</CalButton>
|
|
);
|
|
}
|
|
|
|
function ButtonGroup({ children }: ButtonGroupProps) {
|
|
if (!(children instanceof Array)) {
|
|
return null;
|
|
}
|
|
return (
|
|
<>
|
|
{children.map((button, key) => {
|
|
if (!button) {
|
|
return null;
|
|
}
|
|
return (
|
|
<div key={key} className="mb-2">
|
|
{button}
|
|
</div>
|
|
);
|
|
})}
|
|
</>
|
|
);
|
|
}
|
|
|
|
function Conjs({ not, setNot, config, conjunctionOptions, setConjunction, disabled }: ConjsProps) {
|
|
if (!config || !conjunctionOptions) {
|
|
return null;
|
|
}
|
|
const conjsCount = Object.keys(conjunctionOptions).length;
|
|
|
|
const lessThenTwo = disabled;
|
|
const { forceShowConj } = config.settings;
|
|
const showConj = forceShowConj || (conjsCount > 1 && !lessThenTwo);
|
|
const options = [
|
|
{ label: "All", value: "all" },
|
|
{ label: "Any", value: "any" },
|
|
{ label: "None", value: "none" },
|
|
];
|
|
const renderOptions = () => {
|
|
const { checked: andSelected } = conjunctionOptions["AND"];
|
|
const { checked: orSelected } = conjunctionOptions["OR"];
|
|
const notSelected = not;
|
|
// Default to All
|
|
let value = andSelected ? "all" : orSelected ? "any" : "all";
|
|
|
|
if (notSelected) {
|
|
// not of All -> None
|
|
// not of Any -> All
|
|
value = value == "any" ? "none" : "all";
|
|
}
|
|
const selectValue = options.find((option) => option.value === value);
|
|
const summary = !config.operators.__calReporting ? "Rule group when" : "Query where";
|
|
return (
|
|
<div className="flex items-center text-sm">
|
|
<span>{summary}</span>
|
|
<Select
|
|
className="flex px-2"
|
|
defaultValue={selectValue}
|
|
options={options}
|
|
onChange={(option) => {
|
|
if (!option) return;
|
|
if (option.value === "all") {
|
|
setConjunction("AND");
|
|
setNot(false);
|
|
} else if (option.value === "any") {
|
|
setConjunction("OR");
|
|
setNot(false);
|
|
} else if (option.value === "none") {
|
|
setConjunction("OR");
|
|
setNot(true);
|
|
}
|
|
}}
|
|
/>
|
|
<span>match</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return showConj ? renderOptions() : null;
|
|
}
|
|
|
|
const FieldSelect = function FieldSelect(props: FieldProps) {
|
|
const { items, setField, selectedKey } = props;
|
|
const selectItems = items.map((item) => {
|
|
return {
|
|
...item,
|
|
value: item.key,
|
|
};
|
|
});
|
|
|
|
const defaultValue = selectItems.find((item) => {
|
|
return item.value === selectedKey;
|
|
});
|
|
|
|
return (
|
|
<Select
|
|
className="data-testid-field-select mb-2"
|
|
menuPosition="fixed"
|
|
onChange={(item) => {
|
|
if (!item) {
|
|
return;
|
|
}
|
|
setField(item.value);
|
|
}}
|
|
defaultValue={defaultValue}
|
|
options={selectItems}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const Provider = ({ children }: ProviderProps) => children;
|
|
|
|
const widgets = {
|
|
TextWidget,
|
|
TextAreaWidget,
|
|
SelectWidget,
|
|
NumberWidget,
|
|
MultiSelectWidget,
|
|
FieldSelect,
|
|
Button,
|
|
ButtonGroup,
|
|
Conjs,
|
|
Provider,
|
|
};
|
|
|
|
export default widgets;
|