Edit location dialog new select component (#5647)
This commit is contained in:
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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user