first commit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import type { ChangeEvent } from "react";
|
||||
import type { Settings, Widgets, SelectWidgetProps } from "react-awesome-query-builder";
|
||||
// Figure out why routing-forms/env.d.ts doesn't work
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
import BasicConfig from "react-awesome-query-builder/lib/config/basic";
|
||||
|
||||
import { EmailField } from "@calcom/ui";
|
||||
|
||||
import widgetsComponents from "../widgets";
|
||||
|
||||
const {
|
||||
TextWidget,
|
||||
TextAreaWidget,
|
||||
MultiSelectWidget,
|
||||
SelectWidget,
|
||||
NumberWidget,
|
||||
FieldSelect,
|
||||
Conjs,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Provider,
|
||||
} = widgetsComponents;
|
||||
|
||||
const renderComponent = function <T1>(props: T1 | undefined, Component: React.FC<T1>) {
|
||||
if (!props) {
|
||||
return <div />;
|
||||
}
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
const settings: Settings = {
|
||||
...BasicConfig.settings,
|
||||
|
||||
renderField: (props) => renderComponent(props, FieldSelect),
|
||||
renderOperator: (props) => renderComponent(props, FieldSelect),
|
||||
renderFunc: (props) => renderComponent(props, FieldSelect),
|
||||
renderConjs: (props) => renderComponent(props, Conjs),
|
||||
renderButton: (props) => renderComponent(props, Button),
|
||||
renderButtonGroup: (props) => renderComponent(props, ButtonGroup),
|
||||
renderProvider: (props) => renderComponent(props, Provider),
|
||||
|
||||
groupActionsPosition: "bottomCenter",
|
||||
|
||||
// Disable groups
|
||||
maxNesting: 1,
|
||||
};
|
||||
|
||||
// react-query-builder types have missing type property on Widget
|
||||
//TODO: Reuse FormBuilder Components - FormBuilder components are built considering Cal.com design system and coding guidelines. But when awesome-query-builder renders these components, it passes its own props which are different from what our Components expect.
|
||||
// So, a mapper should be written here that maps the props provided by awesome-query-builder to the props that our components expect.
|
||||
const widgets: Widgets & { [key in keyof Widgets]: Widgets[key] & { type: string } } = {
|
||||
...BasicConfig.widgets,
|
||||
text: {
|
||||
...BasicConfig.widgets.text,
|
||||
factory: (props) => renderComponent(props, TextWidget),
|
||||
},
|
||||
textarea: {
|
||||
...BasicConfig.widgets.textarea,
|
||||
factory: (props) => renderComponent(props, TextAreaWidget),
|
||||
},
|
||||
number: {
|
||||
...BasicConfig.widgets.number,
|
||||
factory: (props) => renderComponent(props, NumberWidget),
|
||||
},
|
||||
multiselect: {
|
||||
...BasicConfig.widgets.multiselect,
|
||||
factory: (
|
||||
props: SelectWidgetProps & {
|
||||
listValues: { title: string; value: string }[];
|
||||
}
|
||||
) => renderComponent(props, MultiSelectWidget),
|
||||
},
|
||||
select: {
|
||||
...BasicConfig.widgets.select,
|
||||
factory: (
|
||||
props: SelectWidgetProps & {
|
||||
listValues: { title: string; value: string }[];
|
||||
}
|
||||
) => renderComponent(props, SelectWidget),
|
||||
},
|
||||
phone: {
|
||||
...BasicConfig.widgets.text,
|
||||
factory: (props) => {
|
||||
if (!props) {
|
||||
return <div />;
|
||||
}
|
||||
return <TextWidget type="tel" {...props} />;
|
||||
},
|
||||
valuePlaceholder: "Enter Phone Number",
|
||||
},
|
||||
email: {
|
||||
...BasicConfig.widgets.text,
|
||||
factory: (props) => {
|
||||
if (!props) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmailField
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
props.setValue(val);
|
||||
}}
|
||||
containerClassName="w-full"
|
||||
className="dark:placeholder:text-darkgray-600 focus:border-brand border-subtle dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const types = {
|
||||
...BasicConfig.types,
|
||||
phone: {
|
||||
...BasicConfig.types.text,
|
||||
widgets: {
|
||||
...BasicConfig.types.text.widgets,
|
||||
},
|
||||
},
|
||||
email: {
|
||||
...BasicConfig.types.text,
|
||||
widgets: {
|
||||
...BasicConfig.types.text.widgets,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const operators = BasicConfig.operators;
|
||||
operators.equal.label = operators.select_equals.label = "Equals";
|
||||
operators.greater_or_equal.label = "Greater than or equal to";
|
||||
operators.greater.label = "Greater than";
|
||||
operators.less_or_equal.label = "Less than or equal to";
|
||||
operators.less.label = "Less than";
|
||||
operators.not_equal.label = operators.select_not_equals.label = "Does not equal";
|
||||
operators.between.label = "Between";
|
||||
|
||||
delete operators.proximity;
|
||||
delete operators.is_null;
|
||||
delete operators.is_not_null;
|
||||
|
||||
/**
|
||||
* Not supported with JSONLogic. Implement them and add these back -> https://github.com/jwadhams/json-logic-js/issues/81
|
||||
*/
|
||||
delete operators.starts_with;
|
||||
delete operators.ends_with;
|
||||
|
||||
const config = {
|
||||
conjunctions: BasicConfig.conjunctions,
|
||||
operators,
|
||||
types,
|
||||
widgets,
|
||||
settings,
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,128 @@
|
||||
.cal-query-builder .query-builder,
|
||||
.cal-query-builder .qb-draggable,
|
||||
.cal-query-builder .qb-drag-handler {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* hide connectors */
|
||||
.cal-query-builder .group-or-rule::before,
|
||||
.cal-query-builder .group-or-rule::after {
|
||||
/* !important to ensure that styles added by react-query-awesome-builder are overriden */
|
||||
content: unset !important;
|
||||
}
|
||||
|
||||
.cal-query-builder .group--children {
|
||||
/* !important to ensure that styles added by react-query-awesome-builder are overriden */
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
/* Hide "and" for between numbers */
|
||||
.cal-query-builder .widget--sep {
|
||||
/* !important to ensure that styles added by react-query-awesome-builder are overriden */
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Layout of all fields- Distance b/w them, positioning, width */
|
||||
.cal-query-builder .rule--body--wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cal-query-builder .rule--body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cal-query-builder .rule--field,
|
||||
.cal-query-builder .rule--operator,
|
||||
.cal-query-builder .rule--value {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.cal-query-builder .rule--widget {
|
||||
display: "inline-block";
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cal-query-builder .widget--widget,
|
||||
.cal-query-builder .widget--widget,
|
||||
.cal-query-builder .widget--widget > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cal-query-builder .rule--drag-handler,
|
||||
.cal-query-builder .rule--header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 8px;
|
||||
/* !important to ensure that styles added by react-query-awesome-builder are overriden */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Disable Reordering of rules - It is not required with the current functionality plus it's not working correctly even if someone wants to re-order */
|
||||
.cal-query-builder .rule--drag-handler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cal-query-builder .rule--func--wrapper,
|
||||
.cal-query-builder .rule--func,
|
||||
.cal-query-builder .rule--func--args,
|
||||
.cal-query-builder .rule--func--arg,
|
||||
.cal-query-builder .rule--func--arg-value,
|
||||
.cal-query-builder .rule--func--bracket-before,
|
||||
.cal-query-builder .rule--func--bracket-after,
|
||||
.cal-query-builder .rule--func--arg-sep,
|
||||
.cal-query-builder .rule--func--arg-label,
|
||||
.cal-query-builder .rule--func--arg-label-sep {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.cal-query-builder .rule--field,
|
||||
.cal-query-builder .group--field,
|
||||
.cal-query-builder .rule--operator,
|
||||
.cal-query-builder .rule--value,
|
||||
.cal-query-builder .rule--operator-options,
|
||||
.cal-query-builder .widget--widget,
|
||||
.cal-query-builder .widget--valuesrc,
|
||||
.cal-query-builder .operator--options--sep,
|
||||
.cal-query-builder .rule--before-widget,
|
||||
.cal-query-builder .rule--after-widget {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.cal-query-builder .rule--operator,
|
||||
.cal-query-builder .widget--widget,
|
||||
.cal-query-builder .widget--valuesrc,
|
||||
.cal-query-builder .widget--sep {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.cal-query-builder .widget--valuesrc {
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
.cal-query-builder .group--header,
|
||||
.cal-query-builder .group--footer {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cal-query-builder .group-or-rule-container {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cal-query-builder .rule {
|
||||
border: 1px solid transparent;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
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";
|
||||
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[]>) => {
|
||||
//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,
|
||||
};
|
||||
});
|
||||
|
||||
const optionsFromList = selectItems.filter((item) => value?.includes(item.value));
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user