feat: add LocationSelect component (#6571)

* feat: add LocationSelect component

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: type error

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore: type error

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
This commit is contained in:
Udit Takkar 2023-01-21 22:22:21 +05:30 committed by GitHub
parent 0e04f5d338
commit 5bb6904ffd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 103 additions and 84 deletions

View File

@ -3,7 +3,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { components } from "react-select";
import { z } from "zod";
import {
@ -14,32 +13,25 @@ import {
LocationObject,
LocationType,
} from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, Form, Icon, PhoneInput } from "@calcom/ui";
import { Button, Dialog, DialogContent, DialogFooter, Form, Icon, PhoneInput } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
import CheckboxField from "@components/ui/form/CheckboxField";
import Select from "@components/ui/form/Select";
import LocationSelect, { LocationOption } from "@components/ui/form/LocationSelect";
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
type OptionTypeBase = {
label: string;
value: EventLocationType["type"];
disabled?: boolean;
};
interface ISetLocationDialog {
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
selection?: OptionTypeBase;
selection?: LocationOption;
booking?: BookingItem;
defaultValues?: LocationObject[];
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
setSelectedLocation?: (param: LocationOption | undefined) => void;
setEditingLocationType?: (param: string) => void;
}
@ -211,7 +203,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
})();
return (
<Dialog open={isOpenDialog}>
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
<DialogContent disableOverflow>
<div className="flex flex-row space-x-3">
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
@ -296,38 +288,11 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
name="locationType"
control={locationFormMethods.control}
render={() => (
<Select<{ label: string; value: string; icon?: string }>
<LocationSelect
maxMenuHeight={300}
name="location"
defaultValue={selection}
options={locationOptions}
components={{
Option: (props) => (
<components.Option {...props}>
<div className="flex items-center gap-3">
{props.data.icon && (
<img src={props.data.icon} alt="cover" className="h-3.5 w-3.5" />
)}
<span
className={classNames(
"text-sm font-medium",
props.isSelected ? "text-white" : "text-gray-900"
)}>
{props.data.label}
</span>
</div>
</components.Option>
),
}}
formatOptionLabel={(e) => (
<div className="flex items-center gap-3">
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
<span>{e.label}</span>
</div>
)}
formatGroupLabel={(e) => (
<p className="text-xs font-medium text-gray-600">{e.label}</p>
)}
isSearchable
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm"
onChange={(val) => {

View File

@ -17,11 +17,21 @@ import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from
import { slugify } from "@lib/slugify";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import LocationSelect, {
SingleValueLocationOption,
LocationOption,
} from "@components/ui/form/LocationSelect";
type OptionTypeBase = {
label: string;
value: EventLocationType["type"];
disabled?: boolean;
const getLocationFromType = (
type: EventLocationType["type"],
locationOptions: Pick<EventTypeSetupProps, "locationOptions">["locationOptions"]
) => {
for (const locationOption of locationOptions) {
const option = locationOption.options.find((option) => option.value === type);
if (option) {
return option;
}
}
};
export const EventSetupTab = (
@ -32,7 +42,7 @@ export const EventSetupTab = (
const { eventType, locationOptions, team } = props;
const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
@ -51,7 +61,8 @@ export const EventSetupTab = (
);
const openLocationModal = (type: EventLocationType["type"]) => {
setSelectedLocation(locationOptions.find((option) => option.value === type));
const option = getLocationFromType(type, locationOptions);
setSelectedLocation(option);
setShowLocationModal(true);
};
@ -131,14 +142,14 @@ export const EventSetupTab = (
<div className="w-full">
{validLocations.length === 0 && (
<div className="flex">
<Select
<LocationSelect
placeholder={t("select")}
options={locationOptions}
isSearchable={false}
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
onChange={(e) => {
onChange={(e: SingleValueLocationOption) => {
if (e?.value) {
const newLocationType: EventLocationType["type"] = e.value;
const newLocationType = e.value;
const eventLocationType = getEventLocationType(newLocationType);
if (!eventLocationType) {
return;
@ -362,7 +373,9 @@ export const EventSetupTab = (
saveLocation={saveLocation}
defaultValues={formMethods.getValues("locations")}
selection={
selectedLocation ? { value: selectedLocation.value, label: t(selectedLocation.label) } : undefined
selectedLocation
? { value: selectedLocation.value, label: t(selectedLocation.label), icon: selectedLocation.icon }
: undefined
}
setSelectedLocation={setSelectedLocation}
setEditingLocationType={setEditingLocationType}

View File

@ -0,0 +1,64 @@
import { components, GroupBase, Props, SingleValue } from "react-select";
import type { EventLocationType } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import Select from "@components/ui/form/Select";
export type LocationOption = {
label: string;
value: EventLocationType["type"];
icon?: string;
disabled?: boolean;
};
export type SingleValueLocationOption = SingleValue<LocationOption>;
export type GroupOptionType = GroupBase<LocationOption>;
const OptionWithIcon = ({
icon,
isSelected,
label,
}: {
icon?: string;
isSelected?: boolean;
label: string;
}) => {
return (
<div className="flex items-center gap-3">
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5" />}
<span className={classNames("text-sm font-medium", isSelected ? "text-white" : "text-gray-900")}>
{label}
</span>
</div>
);
};
export default function LocationSelect(props: Props<LocationOption, false, GroupOptionType>) {
return (
<Select<LocationOption>
name="location"
components={{
Option: (props) => (
<components.Option {...props}>
<OptionWithIcon icon={props.data.icon} label={props.data.label} isSelected={props.isSelected} />
</components.Option>
),
SingleValue: (props) => (
<components.SingleValue {...props}>
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
</components.SingleValue>
),
}}
formatOptionLabel={(e) => (
<div className="flex items-center gap-3">
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
<span>{e.label}</span>
</div>
)}
formatGroupLabel={(e) => <p className="text-xs font-medium text-gray-600">{e.label}</p>}
{...props}
/>
);
}

View File

@ -10,6 +10,13 @@ import { EventTypeModel } from "@calcom/prisma/zod";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { App, AppMeta } from "@calcom/types/App";
type LocationOption = {
label: string;
value: EventLocationType["type"];
icon?: string;
disabled?: boolean;
};
export type EventTypeApps = NonNullable<NonNullable<z.infer<typeof EventTypeMetaDataSchema>>["apps"]>;
export type EventTypeAppsList = keyof EventTypeApps;
@ -48,36 +55,6 @@ export const InstalledAppVariants = [
export const ALL_APPS = Object.values(ALL_APPS_MAP);
type OptionTypeBase = {
label: string;
value: EventLocationType["type"];
disabled?: boolean;
};
function translateLocations(locations: OptionTypeBase[], t: TFunction) {
return locations.map((l) => ({
...l,
label: t(l.label),
}));
}
export function getLocationOptions(integrations: ReturnType<typeof getApps>, t: TFunction) {
const locations: OptionTypeBase[] = [];
defaultLocations.forEach((l) => {
locations.push({
label: l.label,
value: l.type,
});
});
integrations.forEach((app) => {
if (app.locationOption) {
locations.push(app.locationOption);
}
});
return translateLocations(locations, t);
}
export function getLocationGroupedOptions(integrations: ReturnType<typeof getApps>, t: TFunction) {
const apps: Record<string, { label: string; value: string; disabled?: boolean; icon?: string }[]> = {};
integrations.forEach((app) => {
@ -147,7 +124,7 @@ export function getLocationGroupedOptions(integrations: ReturnType<typeof getApp
function getApps(userCredentials: CredentialData[]) {
const apps = ALL_APPS.map((appMeta) => {
const credentials = userCredentials.filter((credential) => credential.type === appMeta.type);
let locationOption: OptionTypeBase | null = null;
let locationOption: LocationOption | null = null;
/** If the app is a globally installed one, let's inject it's key */
if (appMeta.isGlobal) {

View File

@ -1,7 +1,7 @@
import { Prisma, PrismaClient } from "@prisma/client";
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
import { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils";
import { getEventTypeAppData, getLocationGroupedOptions } from "@calcom/app-store/utils";
import { LocationObject } from "@calcom/core/location";
import { parseBookingLimit, parseRecurringEvent } from "@calcom/lib";
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
@ -251,7 +251,7 @@ export default async function getEventTypeById({
const currentUser = eventType.users.find((u) => u.id === userId);
const t = await getTranslation(currentUser?.locale ?? "en", "common");
const integrations = await getEnabledApps(credentials);
const locationOptions = getLocationOptions(integrations, t);
const locationOptions = getLocationGroupedOptions(integrations, t);
const eventTypeObject = Object.assign({}, eventType, {
periodStartDate: eventType.periodStartDate?.toString() ?? null,