Edit location dialog new select component (#5647)

This commit is contained in:
Udit Takkar authored and GitHub committed 2022-11-24 11:53:29 +00:00
1 parent 7df04c1d50
commit 454311dd52
9 files changed
+175 -29

No files matched your search

@@ -3,6 +3,7 @@ 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 {
@@ -13,6 +14,7 @@ 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, DialogContent, Form, Icon, PhoneInput } from "@calcom/ui";
@@ -38,6 +40,7 @@ interface ISetLocationDialog {
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
isOpenDialog: boolean;
setSelectedLocation?: (param: OptionTypeBase | undefined) => void;
setEditingLocationType?: (param: string) => void;
}
const LocationInput = (props: {
@@ -75,6 +78,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
isOpenDialog,
defaultValues,
setSelectedLocation,
setEditingLocationType,
} = props;
const { t } = useLocale();
const locationsQuery = trpc.viewer.locationOptions.useQuery();
@@ -278,23 +282,52 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
query={locationsQuery}
success={({ data: locationOptions }) => {
if (!locationOptions.length) return null;
if (booking) {
locationOptions.forEach((location) => {
if (location.label === "phone") {
location.options.filter((l) => l.value !== "phone");
} else if (location.label === "in person") {
location.options.filter((l) => l.value !== "attendeeInPerson");
}
});
}
return (
<Controller
name="locationType"
control={locationFormMethods.control}
render={() => (
<Select
<Select<{ label: string; value: string; icon?: string }>
maxMenuHeight={150}
name="location"
defaultValue={selection}
options={
booking
? locationOptions.filter(
(location) =>
location.value !== "phone" && location.value !== "attendeeInPerson"
)
: locationOptions
}
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) => {
@@ -325,6 +358,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
onClick={() => {
setShowLocationModal(false);
setSelectedLocation?.(undefined);
setEditingLocationType?.("");
locationFormMethods.unregister("locationType");
}}
type="button"
@@ -4,6 +4,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
import { Trans } from "next-i18next";
import { useEffect } from "react";
import { Controller, useForm, useWatch } from "react-hook-form";
import { components } from "react-select";
import { z } from "zod";
import {
@@ -14,6 +15,7 @@ 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, DialogContent, Form, Icon, Label, PhoneInput, Select } from "@calcom/ui";
@@ -273,20 +275,48 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
query={locationsQuery}
success={({ data: locationOptions }) => {
if (!locationOptions.length) return null;
if (booking) {
locationOptions.forEach((location) => {
if (location.label === "phone") {
location.options.filter((l) => l.value !== "phone");
}
});
}
return (
<Controller
name="locationType"
control={locationFormMethods.control}
render={() => (
<Select
<Select<{ label: string; value: string; icon?: string }>
maxMenuHeight={150}
name="location"
defaultValue={selection}
options={
booking
? locationOptions.filter((location) => location.value !== "phone")
: locationOptions
}
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-2.5">
{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={false}
onChange={(val) => {
if (val) {
@@ -13,7 +13,7 @@ import { Button, Icon, Label, Select, Skeleton, TextField } from "@calcom/ui";
import { slugify } from "@lib/slugify";
import { EditLocationDialog } from "@components/eventtype/EditLocationDialog";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
type OptionTypeBase = {
label: string;
@@ -28,6 +28,7 @@ export const EventSetupTab = (
const formMethods = useFormContext<FormValues>();
const { eventType, locationOptions, team } = props;
const [showLocationModal, setShowLocationModal] = useState(false);
const [editingLocationType, setEditingLocationType] = useState<string>("");
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
const openLocationModal = (type: EventLocationType["type"]) => {
@@ -43,14 +44,23 @@ export const EventSetupTab = (
);
};
const addLocation = (newLocationType: EventLocationType["type"], details = {}) => {
const existingIdx = formMethods.getValues("locations").findIndex((loc) => newLocationType === loc.type);
const saveLocation = (newLocationType: EventLocationType["type"], details = {}) => {
const locationType = editingLocationType !== "" ? editingLocationType : newLocationType;
const existingIdx = formMethods.getValues("locations").findIndex((loc) => locationType === loc.type);
if (existingIdx !== -1) {
const copy = formMethods.getValues("locations");
copy[existingIdx] = {
...formMethods.getValues("locations")[existingIdx],
...details,
};
if (editingLocationType !== "") {
copy[existingIdx] = {
...details,
type: newLocationType,
};
} else {
copy[existingIdx] = {
...formMethods.getValues("locations")[existingIdx],
...details,
};
}
formMethods.setValue("locations", copy);
} else {
formMethods.setValue(
@@ -58,6 +68,8 @@ export const EventSetupTab = (
formMethods.getValues("locations").concat({ type: newLocationType, ...details })
);
}
setEditingLocationType("");
setShowLocationModal(false);
};
@@ -115,7 +127,7 @@ export const EventSetupTab = (
if (eventLocationType.organizerInputType) {
openLocationModal(newLocationType);
} else {
addLocation(newLocationType);
saveLocation(newLocationType);
}
}
}}
@@ -150,6 +162,7 @@ export const EventSetupTab = (
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
setEditingLocationType(location.type);
openLocationModal(location.type);
}}
aria-label={t("edit")}
@@ -234,12 +247,13 @@ export const EventSetupTab = (
<EditLocationDialog
isOpenDialog={showLocationModal}
setShowLocationModal={setShowLocationModal}
saveLocation={addLocation}
saveLocation={saveLocation}
defaultValues={formMethods.getValues("locations")}
selection={
selectedLocation ? { value: selectedLocation.value, label: selectedLocation.label } : undefined
}
setSelectedLocation={setSelectedLocation}
setEditingLocationType={setEditingLocationType}
/>
</div>
);
@@ -464,11 +464,14 @@
"booking_confirmation": "Confirm your {{eventTypeTitle}} with {{profileName}}",
"booking_reschedule_confirmation": "Reschedule your {{eventTypeTitle}} with {{profileName}}",
"in_person_meeting": "In-person meeting",
"attendeeInPerson":"In Person (Attendee Address)",
"inPerson":"In Person (Organizer Address)",
"link_meeting": "Link meeting",
"phone_call": "Attendee Phone Number",
"your_number": "Your phone number",
"phone_number": "Phone Number",
"attendee_phone_number": "Attendee Phone Number",
"organizer_phone_number" :"Organizer Phone Number",
"host_phone_number": "Your Phone Number",
"enter_phone_number": "Enter phone number",
"reschedule": "Reschedule",
+4 -2
View File
@@ -149,6 +149,9 @@ select:focus {
--tw-ring-color: var(--brand-color);
border-color: var(--brand-color);
}
/* https://stackoverflow.com/a/69308190 */
input[type='text']:focus { box-shadow: none; }
button[role="switch"][data-state="checked"] {
@apply bg-gray-900;
@@ -496,5 +499,4 @@ hr {
.h-screen {
height: -webkit-fill-available;
}
}
}
+6
View File
@@ -12,6 +12,7 @@ export type DefaultEventLocationType = {
type: DefaultEventLocationTypeEnum;
label: string;
messageForOrganizer: string;
category: string;
iconUrl: string;
@@ -71,6 +72,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
attendeeInputPlaceholder: `Enter Address`,
defaultValueVariable: "attendeeAddress",
iconUrl: "/map-pin.svg",
category: "in person",
},
{
default: true,
@@ -82,6 +84,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
variable: "locationAddress",
defaultValueVariable: "address",
iconUrl: "/map-pin.svg",
category: "in person",
},
{
default: true,
@@ -92,6 +95,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
messageForOrganizer: "Provide a Meeting Link",
defaultValueVariable: "link",
iconUrl: "/link.svg",
category: "other",
},
{
default: true,
@@ -106,6 +110,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
// This isn't inputType phone because organizer doesn't need to provide it.
// inputType: "phone"
iconUrl: "/phone.svg",
category: "phone",
},
{
default: true,
@@ -116,6 +121,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
variable: "locationPhoneNumber",
defaultValueVariable: "hostPhoneNumber",
iconUrl: "/phone.svg",
category: "phone",
},
];
+56
View File
@@ -72,6 +72,62 @@ export function getLocationOptions(integrations: ReturnType<typeof getApps>, t:
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) => {
if (app.locationOption) {
const category = app.category;
const option = { ...app.locationOption, icon: app.imageSrc };
if (apps[category]) {
apps[category] = [...apps[category], option];
} else {
apps[category] = [option];
}
}
});
defaultLocations.forEach((l) => {
const category = l.category;
if (apps[category]) {
apps[category] = [
...apps[category],
{
label: l.label,
value: l.type,
icon: l.iconUrl,
},
];
} else {
apps[category] = [
{
label: l.label,
value: l.type,
icon: l.iconUrl,
},
];
}
});
const locations = [];
// Translating labels and pushing into array
for (const category in apps) {
const tmp = { label: category, options: apps[category] };
if (tmp.label === "in person") {
tmp.options.map((l) => ({ ...l, label: t(l.value) }));
} else {
tmp.options.map((l) => ({
...l,
label: t(l.label.toLowerCase().split(" ").join("_")),
}));
}
tmp.label = t(tmp.label);
locations.push(tmp);
}
return locations;
}
/**
* This should get all available apps to the user based on his saved
* credentials, this should also get globally available apps.
+2 -3
View File
@@ -8,7 +8,7 @@ import ethRouter from "@calcom/app-store/rainbow/trpc/router";
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
import { getCustomerAndCheckoutSession } from "@calcom/app-store/stripepayment/lib/getCustomerAndCheckoutSession";
import stripe, { closePayments } from "@calcom/app-store/stripepayment/lib/server";
import getApps, { getLocationOptions } from "@calcom/app-store/utils";
import getApps, { getLocationGroupedOptions } from "@calcom/app-store/utils";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { DailyLocationType } from "@calcom/core/location";
@@ -769,12 +769,11 @@ const loggedInViewerRouter = router({
appId: true,
},
});
const integrations = getApps(credentials);
const t = await getTranslation(ctx.user.locale ?? "en", "common");
const locationOptions = getLocationOptions(integrations, t);
const locationOptions = getLocationGroupedOptions(integrations, t);
return locationOptions;
}),
+2
View File
@@ -206,6 +206,8 @@ const PlainForm = <T extends FieldValues>(props: FormProps<T>, ref: Ref<HTMLForm
ref={ref}
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
form
.handleSubmit(handleSubmit)(event)
.catch((err) => {