Files
calendar/packages/app-store/routing-forms/components/react-awesome-query-builder/widgets.tsx
T
d6546c3107 feat: upgrade tailwind v4 (#24598)
* chore: refactor config files to prevent migration tool errors

* refactor: upgrade with the tailwind migration tool

* chore: restore pre-commit command + mc

* refactor(wip): update dependencies and migrate to Tailwind CSS v4 (mainly web)

* chore: resolve Tailwind v4 migration conflicts from merging main

* chore: remove unused Tailwind packages from config and update dependencies for v4 migration

* chore: uncomment Tailwind CSS utility classes in globals.css

* fix: resolve token conflicts between calcom and coss ui

* fix: textarea scrollbar

* Fix CUI-16

* fix: added @tailwindcss/forms plugin and cleaned up CSS classes in various components to remove unnecessary dark mode styles

* fix: selects and inputs of different sizes

* fix: remove unnecessary leading-20 class from modal titles in various components

* fix: update Checkbox component styles to remove unnecessary border on checked state

* fix: clean up styles in RequiresConfirmationController, Checkbox, and Radio components to enhance consistency

* fix: update button and filter component styles to remove unnecessary rounded classes for consistency

* fix: calendar

* fix: update KBarSearch

* fix: refine styles in Empty and Checkbox components for improved consistency

* Fix focus state email input

* fix: update button hover and active states to use 'not-disabled' instead of 'enabled'

* fix: line-height issues

* fix: update class name for muted background in BookingListItem component

* fix: sidebar spacing

* chore: update class names to use new Tailwind CSS color utilities

* fix embed

* chore: upgrade Tailwind CSS to version 4.1.16 and update related dependencies

* Map css variables and add a playground test for heavy css customization

* suggestion for coss-ui

* refactor: update CSS variable usage and clean up styles

- Replace instances of `--cal-brand-color` with `--cal-brand` in embed-related HTML files.
- Remove the now-unnecessary `addAppCssVars` function from the embed core.
- Import theme tokens in the embed core styles for better consistency.
- Clean up whitespace and formatting in CSS files for improved readability.
- Add a comment in `tokens.css` regarding its usage in both embed and webapp contexts.

* Handle within tokens.css instead of fixing coss-ui

* Remove initial, not needed. Also, remove tailwind.config.js as tailwidn scans the html automaically

* fix: examples app breaking

* fix: modal not resizing correctly

* feat: upgrade atoms to tailwind v4

* fix: atoms build breaking

* fix: atoms build breaking

* chore: upgrate examples/base to tailwind 4

* chore: update globals.css

* fix: add missing scheduler css variables

* fix: PlatformAdditionalCalendarSelector

* chore: update global styles

* chore: update tailwindcss and postcss dependencies to stable versions

* chore: remove unneeded class

* fix: dialog and toast animation

* fix: replace flex-shrink-0 with shrink-0 for consistent styling in various components

* fix: dialog modal for Apple connect

* add margin in SaveFilterSegmentButton

* Fix radix button nested states

* add cursor pointer to buttons but keep dsabled state

* Fix commandK selectors and adds cursor pointer

* Fix teams filter

* fix - round checkboxes

* fix filter checkbox

* fix select indicator's margin

* command group font size

* style: fix badge and tooltip radius

* chore: remove unneeded files

* Delete PR_REVIEW_MANAGED_EVENT_REASSIGNMENT.md

* remove ui-playground leftover

* fix: add missing react phone input styles in atoms

* Delete managed-event-reassignment-flow-and-architecture.mermaid

* fix: inter font not loading

* Add theme to skeleton container so that it can support dark mode

* fix: create custom stack-y-* utilities post tw4 upgrade

* fix: typo

* fix: atoms stack class + remove unused css file

* fix default radius valiue

* fix space-y in embed

* fix skeleton background

* Hardcode radius values to match production

* fix border in embed

* add missing externalThemeClass

* feat: create a custom stack-y-* utility

* fix: add stack utility to atom global css

* fix: Skeleton loader class modalbox

* Add stack-y utility in embed

* fix: add missing stack utilities in atoms globals.css

* update yarn.lock

* add popover portla

* update

---------

Co-authored-by: Sean Brydon <sean@cal.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Anik Dhabal Babu <adhabal2002@gmail.com>
2025-11-25 17:32:28 -03:00

387 lines
9.7 KiB
TypeScript

"use client";
import dynamic from "next/dynamic";
import type { ChangeEvent } from "react";
import type {
ButtonGroupProps,
ButtonProps,
ConjsProps,
FieldProps,
ProviderProps,
} from "react-awesome-query-builder";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button as CalButton } from "@calcom/ui/components/button";
import { TextArea } from "@calcom/ui/components/form";
import { TextField } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
const Select = dynamic(
async () => (await import("@calcom/ui/components/form")).SelectWithValidation
) as unknown as typeof import("@calcom/ui/components/form").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}
className="mb-2"
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
size="sm"
containerClassName="w-full mb-2"
type={type}
value={textValue}
noLabel={noLabel}
placeholder={placeholder}
disabled={readOnly}
onChange={onChange}
{...remainingProps}
{...customProps}
/>
);
};
function NumberWidget({ value, setValue, ...remainingProps }: TextLikeComponentPropsRAQB) {
return (
<TextField
size="sm"
type="number"
labelSrOnly={remainingProps.noLabel}
containerClassName="w-full"
className="mb-2"
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
size="sm"
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
size="sm"
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) {
const { t } = useLocale();
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 ? t("add_filter") : t("add_rule");
dataTestId = "add-rule";
} else if (type == "addGroup") {
label = t("add_rule_group");
dataTestId = "add-rule-group";
}
return (
<CalButton
size="sm"
StartIcon="plus"
data-testid={dataTestId}
type="button"
color="minimal"
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}>{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 ? "If booker selects" : "Query where";
return (
<div className="mb-px flex items-center text-sm">
<span>{summary}</span>
<Select
className="flex px-2"
size="sm"
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
size="sm"
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;