first commit
This commit is contained in:
67
calcom/apps/web/components/ui/form/CheckboxField.tsx
Normal file
67
calcom/apps/web/components/ui/form/CheckboxField.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { InputHTMLAttributes } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { InfoBadge } from "@calcom/ui";
|
||||
|
||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||
label?: React.ReactNode;
|
||||
description: string;
|
||||
descriptionAsLabel?: boolean;
|
||||
informationIconText?: string;
|
||||
};
|
||||
|
||||
const CheckboxField = forwardRef<HTMLInputElement, Props>(
|
||||
({ label, description, informationIconText, ...rest }, ref) => {
|
||||
const descriptionAsLabel = !label || rest.descriptionAsLabel;
|
||||
return (
|
||||
<div className="block items-center sm:flex">
|
||||
{label && (
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
{React.createElement(
|
||||
descriptionAsLabel ? "div" : "label",
|
||||
{
|
||||
className: "flex text-sm font-medium text-default",
|
||||
...(!descriptionAsLabel
|
||||
? {
|
||||
htmlFor: rest.id,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
label
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full">
|
||||
<div className="relative flex items-start">
|
||||
{React.createElement(
|
||||
descriptionAsLabel ? "label" : "div",
|
||||
{
|
||||
className: classNames(
|
||||
"relative flex items-start",
|
||||
descriptionAsLabel ? "text-default" : "text-emphasis"
|
||||
),
|
||||
},
|
||||
<>
|
||||
<div className="flex h-5 items-center">
|
||||
<input
|
||||
{...rest}
|
||||
ref={ref}
|
||||
type="checkbox"
|
||||
className="text-emphasis focus:ring-emphasis dark:text-muted border-default bg-default h-4 w-4 rounded"
|
||||
/>
|
||||
</div>
|
||||
<span className="ms-2 text-sm">{description}</span>
|
||||
</>
|
||||
)}
|
||||
{informationIconText && <InfoBadge content={informationIconText} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CheckboxField.displayName = "CheckboxField";
|
||||
|
||||
export default CheckboxField;
|
||||
57
calcom/apps/web/components/ui/form/CheckedSelect.tsx
Normal file
57
calcom/apps/web/components/ui/form/CheckedSelect.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
import type { Props } from "react-select";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
type CheckedSelectOption = {
|
||||
avatar: string;
|
||||
label: string;
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const CheckedSelect = ({
|
||||
options = [],
|
||||
value = [],
|
||||
...props
|
||||
}: Omit<Props<CheckedSelectOption, true>, "value" | "onChange"> & {
|
||||
value?: readonly CheckedSelectOption[];
|
||||
onChange: (value: readonly CheckedSelectOption[]) => void;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
name={props.name}
|
||||
placeholder={props.placeholder || t("select")}
|
||||
isSearchable={false}
|
||||
options={options}
|
||||
value={value}
|
||||
isMulti
|
||||
{...props}
|
||||
/>
|
||||
{value.map((option) => (
|
||||
<div key={option.value} className="border p-2 font-medium">
|
||||
<Avatar
|
||||
className="inline ltr:mr-2 rtl:ml-2"
|
||||
size="sm"
|
||||
imageSrc={option.avatar}
|
||||
alt={option.label}
|
||||
/>
|
||||
{option.label}
|
||||
<Icon
|
||||
name="x"
|
||||
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
||||
className="text-subtle float-right mt-0.5 h-5 w-5 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckedSelect;
|
||||
33
calcom/apps/web/components/ui/form/DatePicker.tsx
Normal file
33
calcom/apps/web/components/ui/form/DatePicker.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import "react-calendar/dist/Calendar.css";
|
||||
import "react-date-picker/dist/DatePicker.css";
|
||||
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
|
||||
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
type Props = {
|
||||
date: Date;
|
||||
onDatesChange?: ((date: Date) => void) | undefined;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
minDate?: Date;
|
||||
};
|
||||
|
||||
export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }: Props) => {
|
||||
return (
|
||||
<PrimitiveDatePicker
|
||||
className={classNames(
|
||||
"focus:ring-primary-500 focus:border-primary-500 border-default rounded-sm border p-1 pl-2 text-sm",
|
||||
className
|
||||
)}
|
||||
clearIcon={null}
|
||||
calendarIcon={<Icon name="calendar" className="text-subtle h-5 w-5" />}
|
||||
value={date}
|
||||
minDate={minDate}
|
||||
disabled={disabled}
|
||||
onChange={onDatesChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
72
calcom/apps/web/components/ui/form/LocationSelect.tsx
Normal file
72
calcom/apps/web/components/ui/form/LocationSelect.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { GroupBase, Props, SingleValue } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import invertLogoOnDark from "@calcom/lib/invertLogoOnDark";
|
||||
import { Select } from "@calcom/ui";
|
||||
|
||||
export type LocationOption = {
|
||||
label: string;
|
||||
value: EventLocationType["type"];
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
address?: string;
|
||||
credentialId?: number;
|
||||
teamName?: string;
|
||||
};
|
||||
|
||||
export type SingleValueLocationOption = SingleValue<LocationOption>;
|
||||
|
||||
export type GroupOptionType = GroupBase<LocationOption>;
|
||||
|
||||
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{icon && <img src={icon} alt="cover" className={classNames("h-3.5 w-3.5", invertLogoOnDark(icon))} />}
|
||||
<span className={classNames("text-sm font-medium")}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function LocationSelect(props: Props<LocationOption, false, GroupOptionType>) {
|
||||
return (
|
||||
<Select<LocationOption>
|
||||
name="location"
|
||||
id="location-select"
|
||||
data-testid="location-select"
|
||||
components={{
|
||||
Option: (props) => {
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<div data-testid={`location-select-item-${props.data.value}`}>
|
||||
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
},
|
||||
SingleValue: (props) => (
|
||||
<components.SingleValue {...props}>
|
||||
<div data-testid={`location-select-item-${props.data.value}`}>
|
||||
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
|
||||
</div>
|
||||
</components.SingleValue>
|
||||
),
|
||||
}}
|
||||
formatOptionLabel={(e) => (
|
||||
<div className="flex items-center gap-3">
|
||||
{e.icon && (
|
||||
<img
|
||||
src={e.icon}
|
||||
alt="app-icon"
|
||||
className={classNames(e.icon.includes("-dark") && "dark:invert", "h-5 w-5")}
|
||||
/>
|
||||
)}
|
||||
<span>{e.label}</span>
|
||||
</div>
|
||||
)}
|
||||
formatGroupLabel={(e) => <p className="text-default text-xs font-medium">{e.label}</p>}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
43
calcom/apps/web/components/ui/form/MinutesField.tsx
Normal file
43
calcom/apps/web/components/ui/form/MinutesField.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import classNames from "classnames";
|
||||
import type { InputHTMLAttributes, ReactNode } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||
label?: ReactNode;
|
||||
};
|
||||
|
||||
const MinutesField = forwardRef<HTMLInputElement, Props>(({ label, ...rest }, ref) => {
|
||||
return (
|
||||
<div className="block sm:flex">
|
||||
{!!label && (
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
<label htmlFor={rest.id} className="text-default flex h-full items-center text-sm font-medium">
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full">
|
||||
<div className="relative rounded-sm">
|
||||
<input
|
||||
{...rest}
|
||||
ref={ref}
|
||||
type="number"
|
||||
className={classNames(
|
||||
"border-default block w-full rounded-sm pl-2 pr-12 text-sm",
|
||||
rest.className
|
||||
)}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<span className="text-subtle text-sm" id="duration">
|
||||
mins
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MinutesField.displayName = "MinutesField";
|
||||
|
||||
export default MinutesField;
|
||||
192
calcom/apps/web/components/ui/form/Select.tsx
Normal file
192
calcom/apps/web/components/ui/form/Select.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import type { GroupBase, Props, InputProps, SingleValue, MultiValue } from "react-select";
|
||||
import ReactSelect, { components } from "react-select";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useGetTheme } from "@calcom/lib/hooks/useTheme";
|
||||
|
||||
export type SelectProps<
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
> = Props<Option, IsMulti, Group>;
|
||||
|
||||
export const InputComponent = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>({
|
||||
inputClassName,
|
||||
...props
|
||||
}: InputProps<Option, IsMulti, Group>) => {
|
||||
return (
|
||||
<components.Input
|
||||
// disables our default form focus hightlight on the react-select input element
|
||||
inputClassName={classNames("focus:ring-0 focus:ring-offset-0", inputClassName)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function Select<
|
||||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
|
||||
const [mounted, setMounted] = useState<boolean>(false);
|
||||
const { resolvedTheme, forcedTheme } = useGetTheme();
|
||||
const hasDarkTheme = !forcedTheme && resolvedTheme === "dark";
|
||||
const darkThemeColors = {
|
||||
/** Dark Theme starts */
|
||||
//primary - Border when selected and Selected Option background
|
||||
primary: "rgb(41 41 41 / var(--tw-border-opacity))",
|
||||
|
||||
neutral0: "rgb(62 62 62 / var(--tw-bg-opacity))",
|
||||
// Down Arrow hover color
|
||||
neutral5: "white",
|
||||
|
||||
neutral10: "rgb(41 41 41 / var(--tw-border-opacity))",
|
||||
|
||||
// neutral20 - border color + down arrow default color
|
||||
neutral20: "rgb(41 41 41 / var(--tw-border-opacity))",
|
||||
|
||||
// neutral30 - hover border color
|
||||
neutral30: "rgb(41 41 41 / var(--tw-border-opacity))",
|
||||
|
||||
neutral40: "white",
|
||||
|
||||
danger: "white",
|
||||
|
||||
// Cross button in multiselect
|
||||
dangerLight: "rgb(41 41 41 / var(--tw-border-opacity))",
|
||||
|
||||
// neutral50 - MultiSelect - "Select Text" color
|
||||
neutral50: "white",
|
||||
|
||||
// neutral60 - Down Arrow color
|
||||
neutral60: "white",
|
||||
|
||||
neutral70: "red",
|
||||
|
||||
// neutral80 - Selected option
|
||||
neutral80: "white",
|
||||
|
||||
neutral90: "blue",
|
||||
|
||||
primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
|
||||
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
|
||||
/** Dark Theme ends */
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Till we know in JS the theme is ready, we can't render react-select as it would render with light theme instead
|
||||
if (!mounted) {
|
||||
return <input type="text" className={className} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ReactSelect
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
borderRadius: 6,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
...(hasDarkTheme
|
||||
? darkThemeColors
|
||||
: {
|
||||
/** Light Theme starts */
|
||||
primary: "var(--brand-color)",
|
||||
primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
|
||||
primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
|
||||
/** Light Theme Ends */
|
||||
}),
|
||||
},
|
||||
})}
|
||||
styles={{
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
color: state.isSelected ? "var(--brand-text-color)" : "black",
|
||||
":active": {
|
||||
backgroundColor: state.isSelected ? "" : "var(--brand-color)",
|
||||
color: "var(--brand-text-color)",
|
||||
},
|
||||
}),
|
||||
}}
|
||||
components={{
|
||||
...components,
|
||||
IndicatorSeparator: () => null,
|
||||
Input: InputComponent,
|
||||
}}
|
||||
className={classNames("border-0 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SelectWithValidation<
|
||||
Option extends { label: string; value: string },
|
||||
isMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({
|
||||
required = false,
|
||||
onChange,
|
||||
value,
|
||||
...remainingProps
|
||||
}: SelectProps<Option, isMulti, Group> & { required?: boolean }) {
|
||||
const [hiddenInputValue, _setHiddenInputValue] = useState(() => {
|
||||
if (value instanceof Array || !value) {
|
||||
return;
|
||||
}
|
||||
return value.value || "";
|
||||
});
|
||||
|
||||
const setHiddenInputValue = useCallback((value: MultiValue<Option> | SingleValue<Option>) => {
|
||||
let hiddenInputValue = "";
|
||||
if (value instanceof Array) {
|
||||
hiddenInputValue = value.map((val) => val.value).join(",");
|
||||
} else {
|
||||
hiddenInputValue = value?.value || "";
|
||||
}
|
||||
_setHiddenInputValue(hiddenInputValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
setHiddenInputValue(value);
|
||||
}, [value, setHiddenInputValue]);
|
||||
|
||||
return (
|
||||
<div className={classNames("relative", remainingProps.className)}>
|
||||
<Select
|
||||
value={value}
|
||||
{...remainingProps}
|
||||
onChange={(value, ...remainingArgs) => {
|
||||
setHiddenInputValue(value);
|
||||
if (onChange) {
|
||||
onChange(value, ...remainingArgs);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{required && (
|
||||
<input
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
style={{
|
||||
opacity: 0,
|
||||
width: "100%",
|
||||
height: 1,
|
||||
position: "absolute",
|
||||
}}
|
||||
value={hiddenInputValue}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onChange={() => {}}
|
||||
// TODO:Not able to get focus to work
|
||||
// onFocus={() => selectRef.current?.focus()}
|
||||
required={required}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default Select;
|
||||
Reference in New Issue
Block a user