Starting to take shape
This commit is contained in:
parent
a792d31939
commit
8d111cede5
|
@ -1,191 +0,0 @@
|
|||
/**
|
||||
* @deprecated modifications to this file should be v2 only
|
||||
* Use `/apps/web/pages/v2/availability/[schedule].tsx` instead
|
||||
*/
|
||||
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,16 @@
|
|||
import {
|
||||
DropdownMenuCheckboxItem as PrimitiveDropdownMenuCheckboxItem,
|
||||
DropdownMenuCheckboxItemProps,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from "@radix-ui/react-dropdown-menu";
|
||||
import classNames from "classnames";
|
||||
import { GetStaticPaths, GetStaticProps } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
|
@ -14,6 +24,7 @@ import type { Schedule as ScheduleType } from "@calcom/types/schedule";
|
|||
import { Icon } from "@calcom/ui";
|
||||
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
import Dropdown, { DropdownMenuTrigger, DropdownMenuContent } from "@calcom/ui/v2/core/Dropdown";
|
||||
import Shell from "@calcom/ui/v2/core/Shell";
|
||||
import Switch from "@calcom/ui/v2/core/Switch";
|
||||
import VerticalDivider from "@calcom/ui/v2/core/VerticalDivider";
|
||||
|
@ -36,6 +47,101 @@ type AvailabilityFormValues = {
|
|||
isDefault: boolean;
|
||||
};
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<HTMLDivElement, DropdownMenuCheckboxItemProps>(
|
||||
({ children }, ref) => (
|
||||
<PrimitiveDropdownMenuCheckboxItem ref={ref}>
|
||||
<label className="flex w-60 items-center justify-between">
|
||||
{children}
|
||||
<input
|
||||
type="checkbox"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => e.stopPropagation()}
|
||||
className="inline-block rounded-[4px] border-gray-300 text-neutral-900 focus:ring-neutral-500 disabled:text-neutral-400"
|
||||
/>
|
||||
</label>
|
||||
</PrimitiveDropdownMenuCheckboxItem>
|
||||
)
|
||||
);
|
||||
|
||||
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
|
||||
|
||||
const ActiveOnEventTypeSelect = () => {
|
||||
const { t } = useLocale();
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
|
||||
const { data } = trpc.useQuery(["viewer.eventTypes"]);
|
||||
|
||||
const eventTypeGroups = data?.eventTypeGroups.reduce((aggregate, eventTypeGroups) => {
|
||||
if (eventTypeGroups.eventTypes[0].team !== null) {
|
||||
aggregate.push({
|
||||
groupName: eventTypeGroups.eventTypes[0].team.name || "",
|
||||
eventTypeNames: [...eventTypeGroups.eventTypes.map((eventType) => eventType.title)],
|
||||
});
|
||||
} else {
|
||||
aggregate.push({
|
||||
groupName: eventTypeGroups.eventTypes[0].users[0].name || "",
|
||||
eventTypeNames: [...eventTypeGroups.eventTypes.map((eventType) => eventType.title)],
|
||||
});
|
||||
}
|
||||
return aggregate;
|
||||
}, [] as { groupName: string; eventTypeNames: string[] }[]);
|
||||
|
||||
return (
|
||||
<Dropdown onOpenChange={setOpen} open={isOpen}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="base"
|
||||
color="secondary"
|
||||
className="w-full px-3 !font-light"
|
||||
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: 3 })}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{(eventTypeGroups || []).map((eventTypeGroup) => (
|
||||
<DropdownMenuGroup key={eventTypeGroup.groupName} className="space-y-3 p-4 px-3">
|
||||
<DropdownMenuLabel className="h6 pb-3 pl-1 text-xs font-medium uppercase text-neutral-400">
|
||||
{eventTypeGroup.groupName}
|
||||
</DropdownMenuLabel>
|
||||
{eventTypeGroup.eventTypeNames.map((eventTypeTitle) => (
|
||||
<DropdownMenuCheckboxItem key={eventTypeTitle}>
|
||||
<span className="w-[200px] truncate">{eventTypeTitle}</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
))}
|
||||
<DropdownMenuSeparator asChild>
|
||||
<hr />
|
||||
</DropdownMenuSeparator>
|
||||
<DropdownMenuItem 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"
|
||||
onClick={() => {
|
||||
console.log("do nothing");
|
||||
}}>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Availability({ schedule }: { schedule: number }) {
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
|
@ -130,7 +236,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">
|
||||
<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">
|
||||
{t("change_start_end")}
|
||||
</h3>
|
||||
|
@ -148,25 +254,31 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||
{t("timezone")}
|
||||
</label>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
value ? (
|
||||
<TimezoneSelect
|
||||
value={value}
|
||||
className="focus:border-brand mt-1 block rounded-md border-gray-300 text-sm"
|
||||
onChange={(timezone) => onChange(timezone.value)}
|
||||
/>
|
||||
) : (
|
||||
<SkeletonText className="h-6 w-full" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="xl:max-w-80 w-full space-y-4 pr-4 sm:p-0">
|
||||
<div className="space-y-4">
|
||||
<div className="sm:w-full md:w-1/2 lg:w-full">
|
||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||
{t("timezone")}
|
||||
</label>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
value ? (
|
||||
<TimezoneSelect
|
||||
value={value}
|
||||
className="focus:border-brand mt-1 block rounded-md border-gray-300 text-sm"
|
||||
onChange={(timezone) => onChange(timezone.value)}
|
||||
/>
|
||||
) : (
|
||||
<SkeletonText className="h-6 w-full" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</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 />
|
||||
</Label>
|
||||
</div>
|
||||
<hr className="my-8" />
|
||||
<div className="rounded-md">
|
||||
|
|
|
@ -281,6 +281,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
successRedirectUrl: true,
|
||||
hashedLink: true,
|
||||
destinationCalendar: true,
|
||||
scheduleId: true,
|
||||
team: true,
|
||||
users: {
|
||||
select: {
|
||||
|
|
|
@ -87,7 +87,7 @@ export const Button = forwardRef<HTMLAnchorElement | HTMLButtonElement, ButtonPr
|
|||
ref: forwardedRef,
|
||||
className: classNames(
|
||||
// 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
|
||||
size === "base" && "h-9 px-4 py-2.5 ",
|
||||
size === "lg" && "h-[36px] px-4 py-2.5 ",
|
||||
|
|
|
@ -50,7 +50,7 @@ function Select<
|
|||
Option,
|
||||
IsMulti extends boolean = false,
|
||||
Group extends GroupBase<Option> = GroupBase<Option>
|
||||
>({ className, ...props }: SelectProps<Option, IsMulti, Group>) {
|
||||
>({ className, noInputStyle, ...props }: SelectProps<Option, IsMulti, Group> & { noInputStyle?: boolean }) {
|
||||
return (
|
||||
<ReactSelect
|
||||
className={classNames(
|
||||
|
@ -71,6 +71,12 @@ function Select<
|
|||
styles={{
|
||||
control: (base) => ({
|
||||
...base,
|
||||
...(noInputStyle
|
||||
? {
|
||||
border: 0,
|
||||
backgroundColor: "transparent !important",
|
||||
}
|
||||
: {}),
|
||||
// Brute force to remove focus outline of input
|
||||
"& .react-select__input": {
|
||||
borderWidth: 0,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useId } from "@radix-ui/react-id";
|
||||
import React, { forwardRef, ReactElement, ReactNode, Ref, useCallback, useState } from "react";
|
||||
import React, { forwardRef, ReactElement, ReactNode, Ref, useCallback, useEffect, useState } from "react";
|
||||
import { Check, Circle, Info, X, Eye, EyeOff } from "react-feather";
|
||||
import {
|
||||
FieldErrors,
|
||||
|
@ -13,8 +13,10 @@ import {
|
|||
import classNames from "@calcom/lib/classNames";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Skeleton, Tooltip } from "@calcom/ui/v2";
|
||||
import Select from "@calcom/ui/v2/core/form/Select";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
import { Skeleton } from "@calcom/ui/v2/core/skeleton";
|
||||
import { Tooltip } from "@calcom/ui/v2/core/tooltip";
|
||||
|
||||
import { Alert } from "../../../Alert";
|
||||
|
||||
|
@ -216,7 +218,7 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
|
|||
"h-9 border border-gray-300",
|
||||
addOnFilled && "bg-gray-100",
|
||||
addOnLeading && "rounded-l-md border-r-0 px-3",
|
||||
addOnSuffix && "rounded-r-md border-l-0 px-3"
|
||||
addOnSuffix && "rounded-r-md border-l-0"
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -446,6 +448,74 @@ export function InputGroupBox(props: JSX.IntrinsicElements["div"]) {
|
|||
);
|
||||
}
|
||||
|
||||
export const MinutesField = forwardRef<HTMLInputElement, InputFieldProps>(function MinutesField(props, ref) {
|
||||
return <InputField ref={ref} type="number" min={0} {...props} addOnSuffix="mins" />;
|
||||
export const MinutesField = (props: Omit<InputFieldProps, "name"> & { name: string }) => {
|
||||
const form = useFormContext();
|
||||
const [timeUnit, setTimeUnit] = useState<typeof TIME_UNIT_OPTIONS[number]>();
|
||||
return (
|
||||
<TimeUnitField
|
||||
onTimeUnitChange={setTimeUnit}
|
||||
{...form.register(props.name, {
|
||||
required: true,
|
||||
setValueAs: (value) => Number(value / (timeUnit?.asNumber || 1)),
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TIME_UNIT_OPTIONS = [
|
||||
{ value: "day", label: "days", asNumber: 1440 },
|
||||
{ value: "hour", label: "hours", asNumber: 60 },
|
||||
{ value: "minute", label: "mins", asNumber: 1 },
|
||||
];
|
||||
|
||||
export const TimeUnitField = forwardRef<
|
||||
HTMLInputElement,
|
||||
Omit<InputFieldProps, "name"> & {
|
||||
onTimeUnitChange: (timeUnit: typeof TIME_UNIT_OPTIONS[number]) => void;
|
||||
name: string;
|
||||
}
|
||||
>(function TimeUnitField({ onTimeUnitChange, ...props }, ref) {
|
||||
const { watch, setValue } = useFormContext();
|
||||
|
||||
const [displayValue, setDisplayValue] = useState<number>();
|
||||
const [selectedTimeUnit, setSelectedTimeUnit] = useState<typeof TIME_UNIT_OPTIONS[number]>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
TIME_UNIT_OPTIONS.find((option) => option.value === "minute")!
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(props.name, displayValue * selectedTimeUnit.asNumber);
|
||||
}, [displayValue, selectedTimeUnit, props.name, setValue]);
|
||||
|
||||
console.log(watch(props.name), displayValue);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Actual value (in minutes) of input field */}
|
||||
<input type="hidden" ref={ref} />
|
||||
<InputField
|
||||
type="number"
|
||||
onKeyUp={(e) => {
|
||||
setValue(props.name, Number(e.target.value) * selectedTimeUnit.asNumber);
|
||||
}}
|
||||
value={displayValue}
|
||||
onChange={(e) => setDisplayValue(Number(e.target.value))}
|
||||
min={0}
|
||||
addOnSuffix={
|
||||
<Select
|
||||
className="focus:outline-none"
|
||||
noInputStyle
|
||||
value={selectedTimeUnit}
|
||||
onChange={(newValue) => {
|
||||
if (!newValue) return;
|
||||
setSelectedTimeUnit(newValue);
|
||||
onTimeUnitChange(newValue);
|
||||
}}
|
||||
options={TIME_UNIT_OPTIONS}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user