Compare commits
12 Commits
main
...
feature/ac
Author | SHA1 | Date | |
---|---|---|---|
52dbd3d0a8 | |||
9e627a3e21 | |||
b2aeb1873d | |||
302e62b020 | |||
5f38cd1732 | |||
3f11fd1f2f | |||
46d5e49c24 | |||
c56620aa18 | |||
5eca49bb94 | |||
7adb52b172 | |||
b49d7d335d | |||
8d111cede5 |
|
@ -1,193 +0,0 @@
|
||||||
/**
|
|
||||||
* @deprecated modifications to this file should be v2 only
|
|
||||||
* Use `/apps/web/pages/v2/availability/[schedule].tsx` instead
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-nocheck
|
|
||||||
import { GetStaticPaths, GetStaticProps } from "next";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { DEFAULT_SCHEDULE, availabilityAsString } from "@calcom/lib/availability";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
|
||||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
import { BadgeCheckIcon } from "@calcom/ui/Icon";
|
|
||||||
import Shell from "@calcom/ui/Shell";
|
|
||||||
import Switch from "@calcom/ui/Switch";
|
|
||||||
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
|
|
||||||
import { Form } from "@calcom/ui/form/fields";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
|
|
||||||
import Schedule from "@components/availability/Schedule";
|
|
||||||
import EditableHeading from "@components/ui/EditableHeading";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated modifications to this file should be v2 only
|
|
||||||
* Use `/apps/web/pages/v2/availability/[schedule].tsx` instead
|
|
||||||
*/
|
|
||||||
export function AvailabilityForm(props: inferQueryOutput<"viewer.availability.schedule">) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const router = useRouter();
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
schedule: props.availability || DEFAULT_SCHEDULE,
|
|
||||||
isDefault: !!props.isDefault,
|
|
||||||
timeZone: props.timeZone,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateMutation = trpc.useMutation("viewer.availability.schedule.update", {
|
|
||||||
onSuccess: async ({ schedule }) => {
|
|
||||||
await utils.invalidateQueries(["viewer.availability.schedule"]);
|
|
||||||
await router.push("/availability");
|
|
||||||
showToast(
|
|
||||||
t("availability_updated_successfully", {
|
|
||||||
scheduleName: schedule.name,
|
|
||||||
}),
|
|
||||||
"success"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
if (err instanceof HttpError) {
|
|
||||||
const message = `${err.statusCode}: ${err.message}`;
|
|
||||||
showToast(message, "error");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
handleSubmit={async (values) => {
|
|
||||||
updateMutation.mutate({
|
|
||||||
scheduleId: parseInt(router.query.schedule as string, 10),
|
|
||||||
name: props.schedule.name,
|
|
||||||
...values,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="grid grid-cols-3 gap-2">
|
|
||||||
<div className="col-span-3 space-y-2 lg:col-span-2">
|
|
||||||
<div className="divide-y rounded-sm border border-gray-200 bg-white px-4 py-5 sm:p-6">
|
|
||||||
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">{t("change_start_end")}</h3>
|
|
||||||
<Schedule name="schedule" />
|
|
||||||
</div>
|
|
||||||
<div className="space-x-2 text-right">
|
|
||||||
<Button color="secondary" href="/availability" tabIndex={-1}>
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button>{t("save")}</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="min-w-40 col-span-3 ml-2 space-y-2 lg:col-span-1">
|
|
||||||
{props.isDefault ? (
|
|
||||||
<div className="inline-block cursor-default rounded border border-gray-300 bg-gray-200 px-2 py-0.5 pl-1.5 text-sm font-medium text-neutral-800">
|
|
||||||
<span className="flex items-center">
|
|
||||||
<BadgeCheckIcon className="mr-1 h-4 w-4" /> {t("default")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Controller
|
|
||||||
name="isDefault"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<Switch label={t("set_to_default")} onCheckedChange={onChange} checked={value} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("timezone")}
|
|
||||||
</label>
|
|
||||||
<div className="mt-1">
|
|
||||||
<Controller
|
|
||||||
name="timeZone"
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<TimezoneSelect
|
|
||||||
value={value}
|
|
||||||
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 text-sm"
|
|
||||||
onChange={(timezone) => onChange(timezone.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 rounded-sm border border-gray-200 px-4 py-5 sm:p-6 ">
|
|
||||||
<h3 className="text-base font-medium leading-6 text-gray-900">
|
|
||||||
{t("something_doesnt_look_right")}
|
|
||||||
</h3>
|
|
||||||
<div className="mt-2 max-w-xl text-sm text-gray-500">
|
|
||||||
<p>{t("troubleshoot_availability")}</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5">
|
|
||||||
<Button href="/availability/troubleshoot" color="secondary">
|
|
||||||
{t("launch_troubleshooter")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const querySchema = z.object({
|
|
||||||
schedule: stringOrNumber,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function Availability() {
|
|
||||||
const router = useRouter();
|
|
||||||
const { i18n } = useLocale();
|
|
||||||
const { schedule: scheduleId } = router.isReady ? querySchema.parse(router.query) : { schedule: -1 };
|
|
||||||
const query = trpc.useQuery(["viewer.availability.schedule", { scheduleId }], { enabled: router.isReady });
|
|
||||||
const [name, setName] = useState<string>();
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<QueryCell
|
|
||||||
query={query}
|
|
||||||
success={({ data }) => {
|
|
||||||
return (
|
|
||||||
<Shell
|
|
||||||
heading={<EditableHeading title={name || data.schedule.name} onChange={setName} />}
|
|
||||||
subtitle={data.schedule.availability.map((availability) => (
|
|
||||||
<span key={availability.id}>
|
|
||||||
{availabilityAsString(availability, { locale: i18n.language })}
|
|
||||||
<br />
|
|
||||||
</span>
|
|
||||||
))}>
|
|
||||||
<AvailabilityForm
|
|
||||||
{...{ ...data, schedule: { ...data.schedule, name: name || data.schedule.name } }}
|
|
||||||
/>
|
|
||||||
</Shell>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = (ctx) => {
|
|
||||||
const params = querySchema.safeParse(ctx.params);
|
|
||||||
|
|
||||||
if (!params.success) return { notFound: true };
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
schedule: params.data.schedule,
|
|
||||||
},
|
|
||||||
revalidate: 10, // seconds
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = () => {
|
|
||||||
return {
|
|
||||||
paths: [],
|
|
||||||
fallback: "blocking",
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,74 +0,0 @@
|
||||||
/**
|
|
||||||
* @deprecated modifications to this file should be v2 only
|
|
||||||
* Use `/apps/web/pages/v2/availability/index.tsx` instead
|
|
||||||
*/
|
|
||||||
import { ScheduleListItem } from "@calcom/features/schedules/components/ScheduleListItem";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
|
||||||
import EmptyScreen from "@calcom/ui/EmptyScreen";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
import Shell from "@calcom/ui/Shell";
|
|
||||||
|
|
||||||
import { withQuery } from "@lib/QueryCell";
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
|
|
||||||
import { NewScheduleButton } from "@components/availability/NewScheduleButton";
|
|
||||||
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated modifications to this file should be v2 only
|
|
||||||
* Use `/apps/web/pages/v2/availability/index.tsx` instead
|
|
||||||
*/
|
|
||||||
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
const deleteMutation = trpc.useMutation("viewer.availability.schedule.delete", {
|
|
||||||
onSuccess: async () => {
|
|
||||||
await utils.invalidateQueries(["viewer.availability.list"]);
|
|
||||||
showToast(t("schedule_deleted_successfully"), "success");
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
if (err instanceof HttpError) {
|
|
||||||
const message = `${err.statusCode}: ${err.message}`;
|
|
||||||
showToast(message, "error");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{schedules.length === 0 ? (
|
|
||||||
<EmptyScreen
|
|
||||||
Icon={Icon.FiClock}
|
|
||||||
headline={t("new_schedule_heading")}
|
|
||||||
description={t("new_schedule_description")}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="-mx-4 mb-16 overflow-hidden rounded-sm border border-gray-200 bg-white sm:mx-0">
|
|
||||||
<ul className="divide-y divide-neutral-200" data-testid="schedules">
|
|
||||||
{schedules.map((schedule) => (
|
|
||||||
<ScheduleListItem
|
|
||||||
key={schedule.id}
|
|
||||||
schedule={schedule}
|
|
||||||
deleteFunction={deleteMutation.mutate}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const WithQuery = withQuery(["viewer.availability.list"]);
|
|
||||||
|
|
||||||
export default function AvailabilityPage() {
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Shell heading={t("availability")} subtitle={t("configure_availability")} CTA={<NewScheduleButton />}>
|
|
||||||
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
|
|
||||||
</Shell>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
/**
|
|
||||||
* @deprecated modifications to this file should be v2 only
|
|
||||||
* Use `/apps/web/pages/v2/availability/troubleshoot.tsx` instead
|
|
||||||
*/
|
|
||||||
import type { IBusySlot } from "pages/v2/availability/troubleshoot";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
|
||||||
import Shell from "@calcom/ui/Shell";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
|
||||||
|
|
||||||
import Loader from "@components/Loader";
|
|
||||||
|
|
||||||
type User = inferQueryOutput<"viewer.me">;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated modifications to this file should be v2 only
|
|
||||||
* Use `/apps/web/pages/v2/availability/troubleshoot.tsx` instead
|
|
||||||
*/
|
|
||||||
const AvailabilityView = ({ user }: { user: User }) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const [selectedDate, setSelectedDate] = useState(dayjs());
|
|
||||||
|
|
||||||
const { data, isLoading } = trpc.useQuery(
|
|
||||||
[
|
|
||||||
"viewer.availability.user",
|
|
||||||
{
|
|
||||||
username: user.username!,
|
|
||||||
dateFrom: selectedDate.startOf("day").utc().format(),
|
|
||||||
dateTo: selectedDate.endOf("day").utc().format(),
|
|
||||||
withSource: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
enabled: !!user.username,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-xl overflow-hidden rounded-sm bg-white shadow">
|
|
||||||
<div className="px-4 py-5 sm:p-6">
|
|
||||||
{t("overview_of_day")}{" "}
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
className="inline h-8 border-none p-0"
|
|
||||||
defaultValue={selectedDate.format("YYYY-MM-DD")}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.value) setSelectedDate(dayjs(e.target.value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
|
|
||||||
<div className="mt-4 space-y-4">
|
|
||||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-sm">
|
|
||||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
|
||||||
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loader />
|
|
||||||
) : data && data.busy.length > 0 ? (
|
|
||||||
data.busy
|
|
||||||
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
|
||||||
.map((slot: IBusySlot) => (
|
|
||||||
<div
|
|
||||||
key={`${slot.start}-${slot.title ?? "untitled"}`}
|
|
||||||
className="overflow-hidden rounded-sm bg-neutral-100">
|
|
||||||
<div className="px-4 py-5 text-black sm:p-6">
|
|
||||||
{t("calendar_shows_busy_between")}{" "}
|
|
||||||
<span className="font-medium text-neutral-800" title={slot.start}>
|
|
||||||
{dayjs(slot.start).format("HH:mm")}
|
|
||||||
</span>{" "}
|
|
||||||
{t("and")}{" "}
|
|
||||||
<span className="font-medium text-neutral-800" title={slot.end}>
|
|
||||||
{dayjs(slot.end).format("HH:mm")}
|
|
||||||
</span>{" "}
|
|
||||||
{t("on")} {dayjs(slot.start).format("D")}{" "}
|
|
||||||
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
|
|
||||||
{slot.title && ` - (${slot.title})`}
|
|
||||||
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="overflow-hidden rounded-sm bg-neutral-100">
|
|
||||||
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-sm">
|
|
||||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
|
||||||
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Troubleshoot() {
|
|
||||||
const query = trpc.useQuery(["viewer.me"]);
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Shell heading={t("troubleshoot")} subtitle={t("troubleshoot_description")}>
|
|
||||||
<QueryCell query={query} success={({ data }) => <AvailabilityView user={data} />} />
|
|
||||||
</Shell>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertMinsToHrsMins(mins: number) {
|
|
||||||
const h = Math.floor(mins / 60);
|
|
||||||
const m = mins % 60;
|
|
||||||
const hs = h < 10 ? "0" + h : h;
|
|
||||||
const ms = m < 10 ? "0" + m : m;
|
|
||||||
return `${hs}:${ms}`;
|
|
||||||
}
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import { DropdownMenuCheckboxItemProps, DropdownMenuItemIndicator } from "@radix-ui/react-dropdown-menu";
|
||||||
|
import classNames from "classnames";
|
||||||
import { GetStaticPaths, GetStaticProps } from "next";
|
import { GetStaticPaths, GetStaticProps } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import React, { useMemo } from "react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
@ -14,6 +17,15 @@ import type { Schedule as ScheduleType } from "@calcom/types/schedule";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
|
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
|
||||||
import Button from "@calcom/ui/v2/core/Button";
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
|
import Dropdown, {
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuCheckboxItem as PrimitiveDropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
} from "@calcom/ui/v2/core/Dropdown";
|
||||||
import Shell from "@calcom/ui/v2/core/Shell";
|
import Shell from "@calcom/ui/v2/core/Shell";
|
||||||
import Switch from "@calcom/ui/v2/core/Switch";
|
import Switch from "@calcom/ui/v2/core/Switch";
|
||||||
import VerticalDivider from "@calcom/ui/v2/core/VerticalDivider";
|
import VerticalDivider from "@calcom/ui/v2/core/VerticalDivider";
|
||||||
|
@ -37,6 +49,163 @@ type AvailabilityFormValues = {
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EventTypeGroup = { groupName: string; eventTypes: { title: string; isActive: boolean; id: number }[] };
|
||||||
|
|
||||||
|
export const DropdownMenuCheckboxItem = React.forwardRef<HTMLDivElement, DropdownMenuCheckboxItemProps>(
|
||||||
|
({ children, defaultChecked, disabled, ...passThroughProps }, forwardedRef) => {
|
||||||
|
const [checked, setChecked] = useState(defaultChecked);
|
||||||
|
return (
|
||||||
|
<PrimitiveDropdownMenuCheckboxItem
|
||||||
|
ref={forwardedRef}
|
||||||
|
className="flex w-full items-center justify-between space-x-4 p-1 font-normal"
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setChecked(checked);
|
||||||
|
passThroughProps.onCheckedChange && passThroughProps.onCheckedChange(checked);
|
||||||
|
}}
|
||||||
|
disabled={defaultChecked && disabled}
|
||||||
|
ItemIndicator={() => (
|
||||||
|
<DropdownMenuItemIndicator asChild>
|
||||||
|
<span className="hidden" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
)}
|
||||||
|
onSelect={(e) => e.preventDefault()}>
|
||||||
|
{children}
|
||||||
|
<input
|
||||||
|
readOnly
|
||||||
|
disabled={defaultChecked && disabled}
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
className="inline-block rounded-[4px] border-gray-300 text-neutral-900 focus:ring-neutral-500 disabled:text-neutral-400"
|
||||||
|
/>
|
||||||
|
</PrimitiveDropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
|
||||||
|
|
||||||
|
const ActiveOnEventTypeSelect = ({ scheduleId, isDefault }: { scheduleId: number; isDefault: boolean }) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
// I was doubtful to make this RHF but this has no point: because the DropdownMenuCheckboxItem is
|
||||||
|
// controlled anyway; requiring the use of Controller regardless.
|
||||||
|
const [eventTypeIds, setEventTypeIds] = useState<{ [K: number]: boolean }>({});
|
||||||
|
const [isOpen, setOpen] = useState(false);
|
||||||
|
const { data } = trpc.useQuery(["viewer.eventTypes"]);
|
||||||
|
const mutation = trpc.useMutation("viewer.availability.switchActiveOnEventTypes");
|
||||||
|
const { data: user } = useMeQuery();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
|
const eventTypeGroups = useMemo(
|
||||||
|
() =>
|
||||||
|
data?.eventTypeGroups.reduce((aggregate, eventTypeGroups) => {
|
||||||
|
aggregate.push({
|
||||||
|
groupName:
|
||||||
|
eventTypeGroups.eventTypes[0].team?.name || eventTypeGroups.eventTypes[0].users[0].name || "",
|
||||||
|
eventTypes: [
|
||||||
|
...eventTypeGroups.eventTypes.map((eventType) => ({
|
||||||
|
title: eventType.title,
|
||||||
|
id: eventType.id,
|
||||||
|
isActive: eventType.scheduleId
|
||||||
|
? scheduleId === eventType.scheduleId
|
||||||
|
: scheduleId === user?.defaultScheduleId,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return aggregate;
|
||||||
|
}, [] as EventTypeGroup[]),
|
||||||
|
[data?.eventTypeGroups, user?.defaultScheduleId, scheduleId]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
if (eventTypeGroups) {
|
||||||
|
const eventTypeIdsLocal: { [K: number]: boolean } = {};
|
||||||
|
for (const item of eventTypeGroups) {
|
||||||
|
for (const eventType of item.eventTypes) {
|
||||||
|
if (isDefault && !eventType.isActive) {
|
||||||
|
eventTypeIdsLocal[eventType.id] = eventType.isActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setEventTypeIds(eventTypeIdsLocal);
|
||||||
|
}
|
||||||
|
}, [data, eventTypeGroups, isDefault]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown onOpenChange={setOpen} open={isOpen}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="base"
|
||||||
|
color="secondary"
|
||||||
|
className="w-full px-3 !font-light sm:w-72"
|
||||||
|
EndIcon={({ className, ...props }) =>
|
||||||
|
isOpen ? (
|
||||||
|
<Icon.FiChevronUp
|
||||||
|
{...props}
|
||||||
|
className={classNames(className, "!h-5 !w-5 !font-extrabold text-gray-300")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon.FiChevronDown
|
||||||
|
{...props}
|
||||||
|
className={classNames(className, "!h-5 !w-5 !font-extrabold text-gray-300")}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
{t("nr_event_type", {
|
||||||
|
count: eventTypeGroups?.reduce(
|
||||||
|
(count, group) => count + group.eventTypes.filter((eventType) => eventType.isActive).length,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
{(eventTypeGroups || []).map((eventTypeGroup) => (
|
||||||
|
<DropdownMenuGroup key={eventTypeGroup.groupName} className="space-y-3 p-4 px-3 sm:w-72">
|
||||||
|
<DropdownMenuLabel asChild>
|
||||||
|
<span className="h6 pb-3 pl-1 text-xs font-medium uppercase text-neutral-400">
|
||||||
|
{eventTypeGroup.groupName}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{eventTypeGroup.eventTypes.map((eventType) => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={eventType.title}
|
||||||
|
disabled={isDefault}
|
||||||
|
defaultChecked={eventType.isActive}
|
||||||
|
onCheckedChange={(checked) => setEventTypeIds({ ...eventTypeIds, [eventType.id]: checked })}>
|
||||||
|
<span className="truncate">{eventType.title}</span>
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSeparator asChild>
|
||||||
|
<hr />
|
||||||
|
</DropdownMenuSeparator>
|
||||||
|
<div className="flex justify-end space-x-2 px-4 pt-3 pb-2">
|
||||||
|
<Button color="minimalSecondary" onClick={() => setOpen(false)}>
|
||||||
|
{t("cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
onClick={async () => {
|
||||||
|
await mutation.mutate({
|
||||||
|
scheduleId,
|
||||||
|
eventTypeIds,
|
||||||
|
});
|
||||||
|
await utils.invalidateQueries("viewer.eventTypes");
|
||||||
|
}}>
|
||||||
|
{t("apply")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Availability({ schedule }: { schedule: number }) {
|
export default function Availability({ schedule }: { schedule: number }) {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -144,7 +313,7 @@ export default function Availability({ schedule }: { schedule: number }) {
|
||||||
}}
|
}}
|
||||||
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
|
<div className="mb-4 rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
|
||||||
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
|
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
|
||||||
{t("change_start_end")}
|
{t("change_start_end")}
|
||||||
</h3>
|
</h3>
|
||||||
|
@ -162,7 +331,8 @@ export default function Availability({ schedule }: { schedule: number }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
||||||
<div className="xl:max-w-80 w-full pr-4 sm:p-0">
|
<div className="xl:max-w-80 w-full space-y-4 pr-4 sm:p-0">
|
||||||
|
<div className="xl:max-w-80 w-full space-y-4 pr-4 sm:p-0">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||||
{t("timezone")}
|
{t("timezone")}
|
||||||
|
@ -173,15 +343,20 @@ export default function Availability({ schedule }: { schedule: number }) {
|
||||||
value ? (
|
value ? (
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
value={value}
|
value={value}
|
||||||
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
|
className="focus:border-brand mt-1 block w-full rounded-md border-gray-300 text-sm sm:w-72"
|
||||||
onChange={(timezone) => onChange(timezone.value)}
|
onChange={(timezone) => onChange(timezone.value)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SelectSkeletonLoader className="w-72" />
|
<SelectSkeletonLoader className="w-full sm:w-72" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Label className="mt-1 cursor-pointer space-y-2 sm:w-full md:w-1/2 lg:w-full">
|
||||||
|
<span>Active on</span>
|
||||||
|
<ActiveOnEventTypeSelect isDefault={form.watch("isDefault")} scheduleId={schedule} />
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
<hr className="my-8" />
|
<hr className="my-8" />
|
||||||
<div className="rounded-md">
|
<div className="rounded-md">
|
||||||
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
|
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
|
||||||
|
|
|
@ -175,6 +175,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
hideBranding: user.hideBranding,
|
hideBranding: user.hideBranding,
|
||||||
metadata: user.metadata,
|
metadata: user.metadata,
|
||||||
|
defaultScheduleId: user.defaultScheduleId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -276,6 +277,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||||
successRedirectUrl: true,
|
successRedirectUrl: true,
|
||||||
hashedLink: true,
|
hashedLink: true,
|
||||||
destinationCalendar: true,
|
destinationCalendar: true,
|
||||||
|
scheduleId: true,
|
||||||
team: true,
|
team: true,
|
||||||
users: {
|
users: {
|
||||||
select: {
|
select: {
|
||||||
|
|
|
@ -37,6 +37,24 @@ export const availabilityRouter = createProtectedRouter()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
.mutation("switchActiveOnEventTypes", {
|
||||||
|
input: z.object({
|
||||||
|
scheduleId: z.number(),
|
||||||
|
eventTypeIds: z.record(z.boolean()),
|
||||||
|
}),
|
||||||
|
async resolve({ input }) {
|
||||||
|
return await Promise.all(
|
||||||
|
Object.entries(input.eventTypeIds).map(([eventTypeId, isActive]) =>
|
||||||
|
prisma?.eventType.update({
|
||||||
|
where: { id: Number(eventTypeId) },
|
||||||
|
data: {
|
||||||
|
scheduleId: isActive ? input.scheduleId : null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})
|
||||||
.query("schedule", {
|
.query("schedule", {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
scheduleId: z.optional(z.number()),
|
scheduleId: z.optional(z.number()),
|
||||||
|
|
|
@ -87,7 +87,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
|
||||||
ref: forwardedRef,
|
ref: forwardedRef,
|
||||||
className: classNames(
|
className: classNames(
|
||||||
// base styles independent what type of button it is
|
// base styles independent what type of button it is
|
||||||
"inline-flex items-center text-sm font-medium relative",
|
"inline-flex place-content-between items-center text-sm font-medium relative",
|
||||||
// different styles depending on size
|
// different styles depending on size
|
||||||
size === "base" && "h-9 px-4 py-2.5 ",
|
size === "base" && "h-9 px-4 py-2.5 ",
|
||||||
size === "lg" && "h-[36px] px-4 py-2.5 ",
|
size === "lg" && "h-[36px] px-4 py-2.5 ",
|
||||||
|
|
|
@ -29,6 +29,8 @@ DropdownMenuTrigger.displayName = "DropdownMenuTrigger";
|
||||||
|
|
||||||
export const DropdownMenuTriggerItem = DropdownMenuPrimitive.Trigger;
|
export const DropdownMenuTriggerItem = DropdownMenuPrimitive.Trigger;
|
||||||
|
|
||||||
|
export const DropdownMenuItemIndicator = DropdownMenuPrimitive.ItemIndicator;
|
||||||
|
|
||||||
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||||
|
|
||||||
type DropdownMenuContentProps = ComponentProps<typeof DropdownMenuPrimitive["Content"]>;
|
type DropdownMenuContentProps = ComponentProps<typeof DropdownMenuPrimitive["Content"]>;
|
||||||
|
@ -66,19 +68,31 @@ DropdownMenuItem.displayName = "DropdownMenuItem";
|
||||||
|
|
||||||
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||||
|
|
||||||
type DropdownMenuCheckboxItemProps = ComponentProps<typeof DropdownMenuPrimitive["CheckboxItem"]>;
|
export const DropdownMenuCheckboxItem = forwardRef<
|
||||||
export const DropdownMenuCheckboxItem = forwardRef<HTMLDivElement, DropdownMenuCheckboxItemProps>(
|
HTMLDivElement,
|
||||||
({ children, ...props }, forwardedRef) => {
|
DropdownMenuPrimitive.DropdownMenuCheckboxItemProps & { ItemIndicator?: () => JSX.Element }
|
||||||
return (
|
>(
|
||||||
<DropdownMenuPrimitive.CheckboxItem {...props} ref={forwardedRef}>
|
(
|
||||||
{children}
|
{
|
||||||
|
children,
|
||||||
|
ItemIndicator = () => (
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
<CheckCircleIcon />
|
<CheckCircleIcon />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
),
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
forwardedRef
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem {...props} ref={forwardedRef}>
|
||||||
|
{children}
|
||||||
|
<ItemIndicator />
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
|
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
|
||||||
|
|
||||||
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user