2023-02-16 22:39:57 +00:00
|
|
|
import type { ChangeEvent } from "react";
|
|
|
|
import type {
|
2022-07-14 12:40:53 +00:00
|
|
|
ButtonGroupProps,
|
2022-11-23 02:55:25 +00:00
|
|
|
ButtonProps,
|
|
|
|
ConjsProps,
|
|
|
|
FieldProps,
|
2022-07-14 12:40:53 +00:00
|
|
|
ProviderProps,
|
|
|
|
} from "react-awesome-query-builder";
|
|
|
|
|
2023-04-10 16:12:15 +00:00
|
|
|
import { Button as CalButton, SelectWithValidation as Select, TextField, TextArea } from "@calcom/ui";
|
2023-04-12 15:26:31 +00:00
|
|
|
import { Trash, Plus } from "@calcom/ui/components/icon";
|
2022-07-14 12:40:53 +00:00
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
export type CommonProps<
|
|
|
|
TVal extends
|
|
|
|
| string
|
|
|
|
| boolean
|
|
|
|
| string[]
|
|
|
|
| {
|
|
|
|
value: string;
|
|
|
|
optionValue: string;
|
|
|
|
}
|
|
|
|
> = {
|
|
|
|
placeholder?: string;
|
|
|
|
readOnly?: boolean;
|
|
|
|
className?: string;
|
2023-04-15 13:22:51 +00:00
|
|
|
name?: string;
|
2023-03-02 18:15:28 +00:00
|
|
|
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";
|
|
|
|
maxLength?: number;
|
|
|
|
noLabel?: boolean;
|
|
|
|
};
|
2022-07-14 12:40:53 +00:00
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
const TextAreaWidget = (props: TextLikeComponentPropsRAQB) => {
|
|
|
|
const { value, setValue, readOnly, placeholder, maxLength, customProps, ...remainingProps } = props;
|
2022-07-14 12:40:53 +00:00
|
|
|
|
|
|
|
const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
|
|
const val = e.target.value;
|
|
|
|
setValue(val);
|
|
|
|
};
|
|
|
|
|
|
|
|
const textValue = value || "";
|
|
|
|
return (
|
2023-04-10 16:12:15 +00:00
|
|
|
<TextArea
|
2022-07-14 12:40:53 +00:00
|
|
|
value={textValue}
|
|
|
|
placeholder={placeholder}
|
2023-03-02 18:15:28 +00:00
|
|
|
disabled={readOnly}
|
2022-07-14 12:40:53 +00:00
|
|
|
onChange={onChange}
|
|
|
|
maxLength={maxLength}
|
|
|
|
{...customProps}
|
|
|
|
{...remainingProps}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
const TextWidget = (props: TextLikeComponentPropsRAQB) => {
|
|
|
|
const {
|
|
|
|
value,
|
|
|
|
noLabel,
|
|
|
|
setValue,
|
|
|
|
readOnly,
|
|
|
|
placeholder,
|
|
|
|
customProps,
|
|
|
|
type = "text",
|
|
|
|
...remainingProps
|
|
|
|
} = props;
|
2022-07-14 12:40:53 +00:00
|
|
|
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
|
|
const val = e.target.value;
|
|
|
|
setValue(val);
|
|
|
|
};
|
|
|
|
const textValue = value || "";
|
|
|
|
return (
|
2022-09-02 19:00:41 +00:00
|
|
|
<TextField
|
2022-11-03 15:17:52 +00:00
|
|
|
containerClassName="w-full"
|
2022-07-14 12:40:53 +00:00
|
|
|
type={type}
|
|
|
|
value={textValue}
|
2023-03-02 18:15:28 +00:00
|
|
|
labelSrOnly={noLabel}
|
2022-07-14 12:40:53 +00:00
|
|
|
placeholder={placeholder}
|
2023-03-02 18:15:28 +00:00
|
|
|
disabled={readOnly}
|
2022-07-14 12:40:53 +00:00
|
|
|
onChange={onChange}
|
|
|
|
{...remainingProps}
|
|
|
|
{...customProps}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
function NumberWidget({ value, setValue, ...remainingProps }: TextLikeComponentPropsRAQB) {
|
2022-07-14 12:40:53 +00:00
|
|
|
return (
|
2022-09-02 19:00:41 +00:00
|
|
|
<TextField
|
2022-07-14 12:40:53 +00:00
|
|
|
type="number"
|
2023-03-02 18:15:28 +00:00
|
|
|
labelSrOnly={remainingProps.noLabel}
|
2022-11-03 15:17:52 +00:00
|
|
|
containerClassName="w-full"
|
2023-04-05 18:14:46 +00:00
|
|
|
className="focus:border-brand-default bg-default dark:bg-muted border-default disabled:bg-emphasis focus:ring-brand block w-full rounded-md text-sm disabled:hover:cursor-not-allowed"
|
2022-07-14 12:40:53 +00:00
|
|
|
value={value}
|
|
|
|
onChange={(e) => {
|
|
|
|
setValue(e.target.value);
|
|
|
|
}}
|
|
|
|
{...remainingProps}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const MultiSelectWidget = ({
|
|
|
|
listValues,
|
|
|
|
setValue,
|
|
|
|
value,
|
|
|
|
...remainingProps
|
2023-03-02 18:15:28 +00:00
|
|
|
}: SelectLikeComponentPropsRAQB<string[]>) => {
|
2022-07-14 12:40:53 +00:00
|
|
|
//TODO: Use Select here.
|
|
|
|
//TODO: Let's set listValue itself as label and value instead of using title.
|
|
|
|
if (!listValues) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const selectItems = listValues.map((item) => {
|
|
|
|
return {
|
|
|
|
label: item.title,
|
|
|
|
value: item.value,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2022-08-13 11:04:57 +00:00
|
|
|
const defaultValue = selectItems.filter((item) => value?.includes(item.value));
|
2022-07-14 12:40:53 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Select
|
2023-04-05 18:14:46 +00:00
|
|
|
className="mb-2"
|
2022-07-14 12:40:53 +00:00
|
|
|
onChange={(items) => {
|
|
|
|
setValue(items?.map((item) => item.value));
|
|
|
|
}}
|
|
|
|
defaultValue={defaultValue}
|
|
|
|
isMulti={true}
|
2023-03-02 18:15:28 +00:00
|
|
|
isDisabled={remainingProps.readOnly}
|
2022-07-14 12:40:53 +00:00
|
|
|
options={selectItems}
|
|
|
|
{...remainingProps}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-03-02 18:15:28 +00:00
|
|
|
function SelectWidget({ listValues, setValue, value, ...remainingProps }: SelectLikeComponentPropsRAQB) {
|
2022-07-14 12:40:53 +00:00
|
|
|
if (!listValues) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const selectItems = listValues.map((item) => {
|
|
|
|
return {
|
|
|
|
label: item.title,
|
|
|
|
value: item.value,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
const defaultValue = selectItems.find((item) => item.value === value);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Select
|
2023-04-05 18:14:46 +00:00
|
|
|
className="data-testid-select mb-2"
|
2022-07-14 12:40:53 +00:00
|
|
|
onChange={(item) => {
|
|
|
|
if (!item) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setValue(item.value);
|
|
|
|
}}
|
2023-03-02 18:15:28 +00:00
|
|
|
isDisabled={remainingProps.readOnly}
|
2022-07-14 12:40:53 +00:00
|
|
|
defaultValue={defaultValue}
|
|
|
|
options={selectItems}
|
|
|
|
{...remainingProps}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-11-11 09:57:44 +00:00
|
|
|
function Button({ config, type, label, onClick, readonly }: ButtonProps) {
|
2022-07-14 12:40:53 +00:00
|
|
|
if (type === "delRule" || type == "delGroup") {
|
|
|
|
return (
|
|
|
|
<button className="ml-5">
|
2023-04-12 15:26:31 +00:00
|
|
|
<Trash className="text-subtle m-0 h-4 w-4" onClick={onClick} />
|
2022-07-14 12:40:53 +00:00
|
|
|
</button>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
let dataTestId = "";
|
|
|
|
if (type === "addRule") {
|
2022-11-11 09:57:44 +00:00
|
|
|
label = config?.operators.__calReporting ? "Add Filter" : "Add rule";
|
2022-07-14 12:40:53 +00:00
|
|
|
dataTestId = "add-rule";
|
|
|
|
} else if (type == "addGroup") {
|
|
|
|
label = "Add rule group";
|
|
|
|
dataTestId = "add-rule-group";
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<CalButton
|
2023-04-12 15:26:31 +00:00
|
|
|
StartIcon={Plus}
|
2022-07-14 12:40:53 +00:00
|
|
|
data-testid={dataTestId}
|
|
|
|
type="button"
|
|
|
|
color="secondary"
|
|
|
|
disabled={readonly}
|
|
|
|
onClick={onClick}>
|
|
|
|
{label}
|
|
|
|
</CalButton>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function ButtonGroup({ children }: ButtonGroupProps) {
|
|
|
|
if (!(children instanceof Array)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<>
|
2022-11-11 09:57:44 +00:00
|
|
|
{children.map((button, key) => {
|
2022-07-14 12:40:53 +00:00
|
|
|
if (!button) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-11-11 09:57:44 +00:00
|
|
|
return (
|
|
|
|
<div key={key} className="mb-2">
|
|
|
|
{button}
|
|
|
|
</div>
|
|
|
|
);
|
2022-07-14 12:40:53 +00:00
|
|
|
})}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
2022-11-11 09:57:44 +00:00
|
|
|
const summary = !config.operators.__calReporting ? "Rule group when" : "Query where";
|
2022-07-14 12:40:53 +00:00
|
|
|
return (
|
|
|
|
<div className="flex items-center text-sm">
|
2022-11-11 09:57:44 +00:00
|
|
|
<span>{summary}</span>
|
2022-07-14 12:40:53 +00:00
|
|
|
<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
|
2022-11-11 09:57:44 +00:00
|
|
|
className="data-testid-field-select mb-2"
|
2022-07-14 12:40:53 +00:00
|
|
|
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;
|