Bugfix/improved assignment page (#7165)

* Refactor Assignment hosts components

* Addressed NIT

* Remove switch in favour of render object

---------

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Alex van Andel 2023-03-08 22:04:33 +00:00 committed by GitHub
parent 2e5c0c6332
commit d40b934866
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 173 additions and 145 deletions

View File

@ -1,7 +1,7 @@
import { SchedulingType } from "@prisma/client";
import type { SchedulingType } from "@prisma/client";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useRef } from "react";
import type { ComponentProps } from "react";
import type { Control } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import type { Options } from "react-select";
@ -35,65 +35,169 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
return 0;
};
const FixedHosts = ({
control,
const CheckedHostField = ({
labelText,
placeholder,
options = [],
isFixed,
value,
onChange,
...rest
}: {
control: Control<FormValues>;
labelText: string;
placeholder: string;
isFixed: boolean;
value: { isFixed: boolean; userId: number }[];
onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
options?: Options<CheckedSelectOption>;
} & Partial<ComponentProps<typeof CheckedTeamSelect>>) => {
} & Omit<Partial<ComponentProps<typeof CheckedTeamSelect>>, "onChange" | "value">) => {
return (
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div>
<Label>{labelText}</Label>
<Controller
name="hostsFixed"
control={control}
render={({ field: { onChange, value } }) => {
return (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) => {
onChange(
options.map((option) => ({
isFixed: true,
userId: parseInt(option.value, 10),
}))
);
}}
value={value
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={options}
placeholder={placeholder}
{...rest}
/>
);
<CheckedTeamSelect
isOptionDisabled={(option) => !!value.find((host) => host.userId.toString() === option.value)}
onChange={(options) => {
onChange &&
onChange(
options.map((option) => ({
isFixed,
userId: parseInt(option.value, 10),
}))
);
}}
value={(value || [])
.filter(({ isFixed: _isFixed }) => isFixed === _isFixed)
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={options}
placeholder={placeholder}
{...rest}
/>
</div>
</div>
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
const formMethods = useFormContext<FormValues>();
const RoundRobinHosts = ({
teamMembers,
value,
onChange,
}: {
value: { isFixed: boolean; userId: number }[];
onChange: (hosts: { isFixed: boolean; userId: number }[]) => void;
teamMembers: {
value: string;
label: string;
avatar: string;
email: string;
}[];
}) => {
const { t } = useLocale();
return (
<>
<CheckedHostField
options={teamMembers.sort(sortByLabel)}
isFixed={true}
onChange={(changeValue) => {
onChange([...value.filter(({ isFixed }) => !isFixed), ...changeValue]);
}}
value={value}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
/>
<CheckedHostField
options={teamMembers.sort(sortByLabel)}
onChange={(changeValue) => onChange([...value.filter(({ isFixed }) => isFixed), ...changeValue])}
value={value}
isFixed={false}
placeholder={t("add_attendees")}
labelText={t("round_robin_hosts")}
/>
</>
);
};
const Hosts = ({
teamMembers,
}: {
teamMembers: {
value: string;
label: string;
avatar: string;
email: string;
}[];
}) => {
const { t } = useLocale();
const {
control,
resetField,
getValues,
formState: { submitCount },
} = useFormContext<FormValues>();
const schedulingType = useWatch({
control: formMethods.control,
control,
name: "schedulingType",
});
const initialValue = useRef<{
hosts: FormValues["hosts"];
schedulingType: SchedulingType | null;
submitCount: number;
} | null>(null);
useEffect(() => {
// Handles init & out of date initial value after submission.
if (!initialValue.current || initialValue.current?.submitCount !== submitCount) {
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
return;
}
resetField("hosts", {
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
});
}, [schedulingType, resetField, getValues, submitCount]);
return (
<Controller<FormValues>
name="hosts"
render={({ field: { onChange, value } }) => {
const schedulingTypeRender = {
COLLECTIVE: (
<CheckedHostField
value={value}
onChange={onChange}
isFixed={true}
options={teamMembers.sort(sortByLabel)}
placeholder={t("add_attendees")}
labelText={t("team")}
/>
),
ROUND_ROBIN: (
<>
<RoundRobinHosts teamMembers={teamMembers} onChange={onChange} value={value} />
{/*<TextField
required
type="number"
label={t("minimum_round_robin_hosts_count")}
defaultValue={1}
{...formMethods.register("minimumHostCount")}
addOnSuffix={<>{t("hosts")}</>}
/>*/}
</>
),
};
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
}}
/>
);
};
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
const { t } = useLocale();
const schedulingTypeOptions: {
value: SchedulingType;
@ -101,17 +205,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
// description: string;
}[] = [
{
value: SchedulingType.COLLECTIVE,
value: "COLLECTIVE",
label: t("collective"),
// description: t("collective_description"),
},
{
value: SchedulingType.ROUND_ROBIN,
value: "ROUND_ROBIN",
label: t("round_robin"),
// description: t("round_robin_description"),
},
];
const teamMembersOptions = teamMembers.map(mapUserToValue);
return (
<div>
@ -119,9 +222,8 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
<div className="space-y-5">
<div className="flex flex-col">
<Label>{t("scheduling_type")}</Label>
<Controller
<Controller<FormValues>
name="schedulingType"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<Select
options={schedulingTypeOptions}
@ -134,75 +236,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
)}
/>
</div>
{schedulingType === SchedulingType.COLLECTIVE && (
<FixedHosts
options={teamMembersOptions.sort(sortByLabel)}
placeholder={t("add_attendees")}
labelText={t("team")}
control={formMethods.control}
/>
)}
{schedulingType === SchedulingType.ROUND_ROBIN && (
<>
<FixedHosts
options={teamMembersOptions.sort(sortByLabel)}
isOptionDisabled={(option) =>
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
}
placeholder={t("add_fixed_hosts")}
labelText={t("fixed_hosts")}
control={formMethods.control}
/>
<div className="flex flex-col space-y-5 bg-gray-50 p-4">
<div>
<Label>{t("round_robin_hosts")}</Label>
<Controller
name="hosts"
control={formMethods.control}
render={({ field: { onChange, value } }) => (
<CheckedTeamSelect
isDisabled={false}
onChange={(options) =>
onChange(
options.map((option) => ({
isFixed: false,
userId: parseInt(option.value, 10),
}))
)
}
value={value
.map(
(host) =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
teamMembers
.map(mapUserToValue)
.find((member) => member.value === host.userId.toString())!
)
.filter(Boolean)}
controlShouldRenderValue={false}
options={teamMembersOptions.sort(sortByLabel)}
isOptionDisabled={(option) =>
!!formMethods
.getValues("hostsFixed")
.find((host) => host.userId.toString() === option.value)
}
placeholder={t("add_attendees")}
/>
)}
/>
</div>
{/*<TextField
required
type="number"
label={t("minimum_round_robin_hosts_count")}
defaultValue={1}
{...formMethods.register("minimumHostCount")}
addOnSuffix={<>{t("hosts")}</>}
/>*/}
</div>
</>
)}
<Hosts teamMembers={teamMembersOptions} />
</div>
)}
</div>

View File

@ -2,7 +2,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod";
import type { PeriodType } from "@prisma/client";
import { SchedulingType } from "@prisma/client";
import type { SchedulingType } from "@prisma/client";
import type { GetServerSidePropsContext } from "next";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@ -86,8 +86,7 @@ export type FormValues = {
};
successRedirectUrl: string;
bookingLimits?: BookingLimit;
hosts: { userId: number }[];
hostsFixed: { userId: number }[];
hosts: { userId: number; isFixed: boolean }[];
bookingFields: z.infer<typeof eventTypeBookingFields>;
};
@ -200,16 +199,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
schedulingType: eventType.schedulingType,
minimumBookingNotice: eventType.minimumBookingNotice,
metadata,
hosts: !!eventType.hosts?.length
? eventType.hosts.filter((host) => !host.isFixed)
: eventType.users
.filter(() => eventType.schedulingType === SchedulingType.ROUND_ROBIN)
.map((user) => ({ userId: user.id })),
hostsFixed: !!eventType.hosts?.length
? eventType.hosts.filter((host) => host.isFixed)
: eventType.users
.filter(() => eventType.schedulingType === SchedulingType.COLLECTIVE)
.map((user) => ({ userId: user.id })),
hosts: eventType.hosts,
} as const;
const formMethods = useForm<FormValues>({
@ -306,8 +296,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
locations,
metadata,
customInputs,
hosts: hostsInput,
hostsFixed,
// We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled,
@ -316,11 +304,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
...input
} = values;
const hosts: ((typeof hostsInput)[number] & { isFixed?: boolean })[] = [];
if (hostsInput || hostsFixed) {
hosts.push(...hostsInput.concat(hostsFixed.map((host) => ({ isFixed: true, ...host }))));
}
if (bookingLimits) {
const isValid = validateBookingLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@ -338,7 +321,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
updateMutation.mutate({
...input,
hosts,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,

View File

@ -338,7 +338,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<div className="mt-4 hidden sm:mt-0 sm:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
{type.users?.length > 1 && (
{type.team && (
<AvatarGroup
className="relative top-1 right-3"
size="sm"

View File

@ -10,15 +10,16 @@ import type {
UseFieldArrayRemove,
} from "react-hook-form";
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { GroupBase, Props } from "react-select";
import type { GroupBase, Props } from "react-select";
import dayjs, { ConfigType } from "@calcom/dayjs";
import type { ConfigType } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { defaultDayRange as DEFAULT_DAY_RANGE } from "@calcom/lib/availability";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { TimeRange } from "@calcom/types/schedule";
import type { TimeRange } from "@calcom/types/schedule";
import {
Button,
Dropdown,

View File

@ -0,0 +1,11 @@
/*
Warnings:
- The primary key for the `Host` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `Host` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Host" DROP CONSTRAINT "Host_pkey",
DROP COLUMN "id",
ADD CONSTRAINT "Host_pkey" PRIMARY KEY ("userId", "eventTypeId");

View File

@ -30,12 +30,13 @@ enum PeriodType {
}
model Host {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int
isFixed Boolean @default(false)
@@id([userId, eventTypeId])
}
model EventType {

View File

@ -15,8 +15,8 @@ import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler"
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { DailyLocationType } from "@calcom/core/location";
import {
getRecordingsOfCalVideoByRoomName,
getDownloadLinkOfCalVideoByRecordingId,
getRecordingsOfCalVideoByRoomName,
} from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmails, sendFeedbackEmail } from "@calcom/emails";

View File

@ -617,15 +617,14 @@ export const eventTypesRouter = router({
connect: users.map((userId: number) => ({ id: userId })),
};
}
if (hosts) {
data.hosts = {
deleteMany: {
eventTypeId: id,
},
createMany: {
// when schedulingType is COLLECTIVE, remove unFixed hosts.
data: hosts.filter((host) => !(data.schedulingType === SchedulingType.COLLECTIVE && !host.isFixed)),
},
deleteMany: {},
create: hosts.map((host) => ({
...host,
isFixed: data.schedulingType === SchedulingType.COLLECTIVE || host.isFixed,
})),
};
}