Restore team event creation

This commit is contained in:
2026-06-07 12:13:21 -06:00
parent e7326095d1
commit ea213b8ccf
6 changed files with 609 additions and 54 deletions
@@ -5,12 +5,12 @@ import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import type { EventType } from "@calcom/prisma/client";
import type { MembershipRole } from "@calcom/prisma/enums";
import { SchedulingType } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { DialogClose, DialogContent, DialogFooter } from "@calcom/ui/components/dialog";
import { showToast } from "@calcom/ui/components/toast";
import { isValidPhoneNumber } from "libphonenumber-js/max";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { z } from "zod";
import { useCreateEventType } from "~/event-types/hooks/useCreateEventType";
@@ -69,7 +69,7 @@ export function CreateEventTypeDialog({ profileOptions }: { profileOptions: Prof
const orgBranding = null;
const {
data: { teamId, eventPage: pageSlug },
data: { teamId, eventPage: pageSlug, schedulingType: querySchedulingType },
} = useTypedQuery(querySchema);
const teamProfile = profileOptions.find((profile) => profile.teamId === teamId);
@@ -102,31 +102,85 @@ export function CreateEventTypeDialog({ profileOptions }: { profileOptions: Prof
};
const { form, createMutation, isManagedEventType } = useCreateEventType(onSuccessMutation, onErrorMutation);
const selectedSchedulingType = form.watch("schedulingType");
useEffect(() => {
if (!teamId) return;
form.setValue("teamId", teamId);
form.setValue("schedulingType", querySchedulingType ?? SchedulingType.ROUND_ROBIN);
}, [form, querySchedulingType, teamId]);
const urlPrefix = WEBSITE_URL;
const schedulingOptions = [
{
value: SchedulingType.ROUND_ROBIN,
label: t("round_robin"),
description: t("round_robin_description"),
},
{
value: SchedulingType.COLLECTIVE,
label: t("collective"),
description: t("collective_description"),
},
];
return (
<Dialog
name="new"
clearQueryParamsOnClose={["eventPage", "type", "description", "title", "length", "slug", "locations"]}>
clearQueryParamsOnClose={[
"eventPage",
"type",
"description",
"title",
"length",
"slug",
"locations",
"schedulingType",
]}>
<DialogContent
type="creation"
enableOverflow
title={teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
description={t("new_event_type_to_book_description")}>
{teamId ? null : (
<CreateEventTypeForm
urlPrefix={urlPrefix}
isPending={createMutation.isPending}
form={form}
isManagedEventType={isManagedEventType}
handleSubmit={(values) => {
createMutation.mutate(values);
}}
SubmitButton={SubmitButton}
pageSlug={pageSlug}
/>
)}
{teamId ? (
<div className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
{schedulingOptions.map((option) => {
const isSelected = selectedSchedulingType === option.value;
return (
<button
key={option.value}
type="button"
className={`rounded-md border p-3 text-left transition ${
isSelected ? "border-emphasis bg-emphasis text-emphasis" : "border-subtle hover:bg-muted"
}`}
onClick={() => {
form.setValue("teamId", teamId, { shouldDirty: true });
form.setValue("schedulingType", option.value, { shouldDirty: true });
}}>
<span className="block text-sm font-medium">{option.label}</span>
<span className="mt-1 block text-xs text-subtle">{option.description}</span>
</button>
);
})}
</div>
) : null}
<CreateEventTypeForm
urlPrefix={urlPrefix}
isPending={createMutation.isPending}
form={form}
isManagedEventType={isManagedEventType}
handleSubmit={(values) => {
createMutation.mutate({
...values,
...(teamId
? { teamId, schedulingType: values.schedulingType ?? SchedulingType.ROUND_ROBIN }
: {}),
});
}}
SubmitButton={SubmitButton}
pageSlug={teamProfile?.slug ?? pageSlug}
/>
</DialogContent>
</Dialog>
);
@@ -45,7 +45,7 @@ const EventAvailabilityTab = dynamic(() =>
import("./tabs/availability/EventAvailabilityTabWebWrapper").then((mod) => mod)
);
const EventTeamAssignmentTab = dynamic(() => Promise.resolve((_props: Record<string, unknown>) => null));
const EventTeamAssignmentTab = dynamic(() => import("./tabs/team/EventTeamAssignmentTab").then((mod) => mod));
const EventLimitsTab = dynamic(() => import("./tabs/limits/EventLimitsTabWebWrapper").then((mod) => mod));
@@ -61,7 +61,6 @@ const EventWebhooksTab = dynamic(() =>
import("./tabs/webhooks/EventWebhooksTab").then((mod) => mod.EventWebhooksTab)
);
export type EventTypeWebWrapperProps = {
id: number;
data: RouterOutputs["viewer"]["eventTypes"]["get"];
@@ -278,16 +277,7 @@ const EventTypeWeb = ({
const querySchema = z.object({
tabName: z
.enum([
"setup",
"availability",
"team",
"limits",
"advanced",
"recurring",
"apps",
"webhooks",
])
.enum(["setup", "availability", "team", "limits", "advanced", "recurring", "apps", "webhooks"])
.optional()
.default("setup"),
});
@@ -0,0 +1,261 @@
"use client";
import type { EventTypeSetupProps, FormValues, Host } from "@calcom/features/eventtypes/lib/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { Avatar } from "@calcom/ui/components/avatar";
import { Button } from "@calcom/ui/components/button";
import { useMemo, useState } from "react";
import { useFormContext } from "react-hook-form";
type EventTeamAssignmentTabProps = {
orgId: number | null;
teamMembers: EventTypeSetupProps["teamMembers"];
team: EventTypeSetupProps["team"];
eventType: EventTypeSetupProps["eventType"];
};
type SearchMember = {
userId: number;
name: string | null;
email: string;
avatarUrl: string | null;
defaultScheduleId: number | null;
};
const hostDefaults = (member: SearchMember, schedulingType: SchedulingType | null): Host => ({
userId: member.userId,
isFixed: schedulingType === SchedulingType.COLLECTIVE,
priority: 2,
weight: 100,
scheduleId: member.defaultScheduleId,
groupId: null,
});
export default function EventTeamAssignmentTab({
teamMembers,
team,
eventType,
}: EventTeamAssignmentTabProps) {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const [search, setSearch] = useState("");
const watchedHosts = formMethods.watch("hosts") ?? [];
const watchedGroups = formMethods.watch("hostGroups") ?? [];
const schedulingType = formMethods.watch("schedulingType") ?? eventType.schedulingType;
const isCollective = schedulingType === SchedulingType.COLLECTIVE;
const selectedUserIds = useMemo(() => watchedHosts.map((host) => host.userId), [watchedHosts]);
const fallbackMembers = useMemo<SearchMember[]>(
() =>
teamMembers.map((member) => ({
userId: member.id,
name: member.name,
email: member.email,
avatarUrl: member.avatarUrl,
defaultScheduleId: member.defaultScheduleId,
})),
[teamMembers]
);
const { data, isPending } = trpc.viewer.eventTypes.searchTeamMembers.useQuery(
{
teamId: team?.id ?? eventType.teamId ?? 0,
search: search || undefined,
limit: 20,
memberUserIds: selectedUserIds.length ? selectedUserIds : undefined,
},
{ enabled: Boolean(team?.id ?? eventType.teamId) }
);
const members = useMemo<SearchMember[]>(() => {
const memberMap = new Map<number, SearchMember>();
fallbackMembers.forEach((member) => memberMap.set(member.userId, member));
data?.members.forEach((member) => {
memberMap.set(member.userId, {
userId: member.userId,
name: member.name,
email: member.email,
avatarUrl: member.avatarUrl,
defaultScheduleId: member.defaultScheduleId,
});
});
return Array.from(memberMap.values());
}, [data?.members, fallbackMembers]);
const memberByUserId = useMemo(() => new Map(members.map((member) => [member.userId, member])), [members]);
const availableMembers = members.filter((member) => !selectedUserIds.includes(member.userId));
const setHosts = (hosts: Host[]) => {
formMethods.setValue("hosts", hosts, { shouldDirty: true });
};
const updateHost = (userId: number, data: Partial<Host>) => {
setHosts(watchedHosts.map((host) => (host.userId === userId ? { ...host, ...data } : host)));
};
return (
<div className="space-y-5">
<div className="rounded-md border border-subtle p-4">
<label className="mb-2 block text-sm font-medium text-emphasis" htmlFor="team-member-search">
{t("team_members")}
</label>
<input
id="team-member-search"
type="search"
value={search}
placeholder={t("search")}
className="border-default bg-default text-default placeholder:text-muted focus:border-emphasis mb-3 h-9 w-full rounded-md border px-3 text-sm outline-none"
onChange={(event) => setSearch(event.target.value)}
/>
<div className="max-h-64 overflow-y-auto rounded-md border border-subtle">
{availableMembers.length ? (
availableMembers.map((member) => (
<div
key={member.userId}
className="flex items-center justify-between gap-3 border-subtle border-b px-3 py-2 last:border-b-0">
<div className="flex min-w-0 items-center gap-3">
<Avatar size="sm" imageSrc={member.avatarUrl} alt={member.name ?? member.email} />
<div className="min-w-0">
<p className="truncate text-sm font-medium text-emphasis">
{member.name ?? member.email}
</p>
<p className="truncate text-xs text-subtle">{member.email}</p>
</div>
</div>
<Button
type="button"
color="secondary"
StartIcon="plus"
onClick={() => setHosts([...watchedHosts, hostDefaults(member, schedulingType)])}>
{t("add")}
</Button>
</div>
))
) : (
<p className="px-3 py-4 text-sm text-subtle">{isPending ? t("loading") : t("no_results")}</p>
)}
</div>
</div>
<div className="rounded-md border border-subtle">
<div className="border-subtle border-b px-4 py-3">
<h3 className="text-sm font-medium text-emphasis">{t("hosts")}</h3>
</div>
{watchedHosts.length ? (
<div className="divide-subtle divide-y">
{watchedHosts.map((host) => {
const member = memberByUserId.get(host.userId);
return (
<div key={host.userId} className="space-y-3 p-4">
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<Avatar
size="sm"
imageSrc={member?.avatarUrl}
alt={member?.name ?? member?.email ?? ""}
/>
<div className="min-w-0">
<p className="truncate text-sm font-medium text-emphasis">
{member?.name ?? member?.email ?? host.userId}
</p>
{member?.email ? (
<p className="truncate text-xs text-subtle">{member.email}</p>
) : null}
</div>
</div>
<Button
type="button"
color="secondary"
StartIcon="x"
onClick={() => setHosts(watchedHosts.filter((item) => item.userId !== host.userId))}>
{t("remove")}
</Button>
</div>
<div className="grid grid-cols-2 gap-3 md:grid-cols-5">
<label className="flex items-center gap-2 text-sm text-default">
<input
type="checkbox"
checked={isCollective || Boolean(host.isFixed)}
disabled={isCollective}
onChange={(event) => updateHost(host.userId, { isFixed: event.target.checked })}
/>
{t("fixed")}
</label>
<label className="text-xs font-medium text-subtle">
{t("priority")}
<input
type="number"
min={0}
max={4}
value={host.priority}
disabled={isCollective}
className="border-default bg-default text-default mt-1 h-9 w-full rounded-md border px-2 text-sm disabled:opacity-60"
onChange={(event) =>
updateHost(host.userId, { priority: Number(event.target.value || 0) })
}
/>
</label>
<label className="text-xs font-medium text-subtle">
{t("weight")}
<input
type="number"
min={0}
value={host.weight}
disabled={isCollective}
className="border-default bg-default text-default mt-1 h-9 w-full rounded-md border px-2 text-sm disabled:opacity-60"
onChange={(event) =>
updateHost(host.userId, { weight: Number(event.target.value || 0) })
}
/>
</label>
<label className="text-xs font-medium text-subtle">
{t("schedule")}
<input
type="number"
min={0}
value={host.scheduleId ?? ""}
className="border-default bg-default text-default mt-1 h-9 w-full rounded-md border px-2 text-sm"
onChange={(event) =>
updateHost(host.userId, {
scheduleId: event.target.value ? Number(event.target.value) : null,
})
}
/>
</label>
<label className="text-xs font-medium text-subtle">
{t("group")}
<select
value={host.groupId ?? ""}
disabled={isCollective}
className="border-default bg-default text-default mt-1 h-9 w-full rounded-md border px-2 text-sm disabled:opacity-60"
onChange={(event) =>
updateHost(host.userId, { groupId: event.target.value || null })
}>
<option value="">{t("none")}</option>
{watchedGroups.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
</label>
</div>
</div>
);
})}
</div>
) : (
<p className="px-4 py-5 text-sm text-subtle">{t("no_hosts_description")}</p>
)}
</div>
</div>
);
}
@@ -0,0 +1,228 @@
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import type { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createHandler } from "./create.handler";
import type { TCreateInputSchema } from "./create.schema";
const serviceMocks = vi.hoisted(() => ({
eventTypeRepositoryConstructor: vi.fn(),
membershipRepositoryConstructor: vi.fn(),
membershipServiceConstructor: vi.fn(),
createEventType: vi.fn(),
hasAnyRole: vi.fn(),
listAcceptedTeamMemberIds: vi.fn(),
}));
vi.mock("@calcom/app-store/_utils/getDefaultLocations", () => ({
getDefaultLocations: vi.fn().mockResolvedValue([]),
}));
vi.mock("@calcom/features/eventtypes/repositories/eventTypeRepository", () => ({
EventTypeRepository: vi.fn(function EventTypeRepository(prismaClient) {
serviceMocks.eventTypeRepositoryConstructor(prismaClient);
return {
create: serviceMocks.createEventType,
};
}),
}));
vi.mock("@calcom/features/membership/repositories/MembershipRepository", () => ({
MembershipRepository: vi.fn(function MembershipRepository(prismaClient) {
serviceMocks.membershipRepositoryConstructor(prismaClient);
return {
listAcceptedTeamMemberIds: serviceMocks.listAcceptedTeamMemberIds,
};
}),
}));
vi.mock("@calcom/features/membership/services/membershipService", () => ({
MembershipService: vi.fn(function MembershipService(repository) {
serviceMocks.membershipServiceConstructor(repository);
return {
hasAnyRole: serviceMocks.hasAnyRole,
};
}),
}));
const prisma = {
organizationSettings: {
findUnique: vi.fn(),
},
};
const user = {
id: 101,
role: "USER",
organizationId: null,
organization: {
isOrgAdmin: false,
},
profile: {
id: 501,
},
metadata: {},
email: "admin@example.com",
};
const baseTeamInput = {
title: "Team call",
slug: "team-call",
length: 30,
hidden: false,
teamId: 201,
schedulingType: SchedulingType.ROUND_ROBIN,
} satisfies TCreateInputSchema;
const buildCtx = () => ({
user,
prisma,
});
describe("createHandler", () => {
beforeEach(() => {
vi.clearAllMocks();
serviceMocks.createEventType.mockResolvedValue({ id: 301, title: "Team call" });
serviceMocks.hasAnyRole.mockResolvedValue(true);
serviceMocks.listAcceptedTeamMemberIds.mockResolvedValue([101, 102]);
prisma.organizationSettings.findUnique.mockResolvedValue(null);
});
it("creates a round robin team event with accepted member hosts as a team admin", async () => {
await expect(
createHandler({
ctx: buildCtx(),
input: {
...baseTeamInput,
hosts: [
{ userId: 101, isFixed: false, priority: 1, weight: 100, scheduleId: 1001, groupId: null },
{ userId: 102, isFixed: true, priority: 2, weight: 80, scheduleId: null, groupId: "group-a" },
],
} as TCreateInputSchema & {
hosts: Array<{
userId: number;
isFixed: boolean;
priority: number;
weight: number;
scheduleId: number | null;
groupId: string | null;
}>;
},
})
).resolves.toEqual({ eventType: { id: 301, title: "Team call" } });
expect(serviceMocks.membershipRepositoryConstructor).toHaveBeenCalledWith(prisma);
expect(serviceMocks.membershipServiceConstructor).toHaveBeenCalled();
expect(serviceMocks.hasAnyRole).toHaveBeenCalledWith(201, 101, [
MembershipRole.ADMIN,
MembershipRole.OWNER,
]);
expect(serviceMocks.listAcceptedTeamMemberIds).toHaveBeenCalledWith({ teamId: 201 });
expect(serviceMocks.createEventType).toHaveBeenCalledWith(
expect.objectContaining({
title: "Team call",
team: { connect: { id: 201 } },
schedulingType: SchedulingType.ROUND_ROBIN,
profileId: 501,
hosts: {
create: [
{
userId: 101,
isFixed: false,
priority: 1,
weight: 100,
scheduleId: 1001,
groupId: null,
},
{
userId: 102,
isFixed: true,
priority: 2,
weight: 80,
scheduleId: null,
groupId: "group-a",
},
],
},
})
);
});
it("creates a collective team event as a team owner", async () => {
serviceMocks.hasAnyRole.mockImplementation(async (_teamId, _userId, roles: MembershipRole[]) =>
roles.includes(MembershipRole.OWNER)
);
await createHandler({
ctx: buildCtx(),
input: {
...baseTeamInput,
schedulingType: SchedulingType.COLLECTIVE,
hosts: [{ userId: 102, isFixed: false, priority: 0, weight: 50, scheduleId: null, groupId: null }],
} as TCreateInputSchema & {
hosts: Array<{
userId: number;
isFixed: boolean;
priority: number;
weight: number;
scheduleId: number | null;
groupId: string | null;
}>;
},
});
expect(serviceMocks.createEventType).toHaveBeenCalledWith(
expect.objectContaining({
schedulingType: SchedulingType.COLLECTIVE,
hosts: {
create: [
{
userId: 102,
isFixed: true,
priority: 0,
weight: 50,
scheduleId: null,
groupId: null,
},
],
},
})
);
});
it("rejects team event creation by a regular team member", async () => {
serviceMocks.hasAnyRole.mockResolvedValue(false);
await expect(createHandler({ ctx: buildCtx(), input: baseTeamInput })).rejects.toMatchObject({
code: "UNAUTHORIZED",
} satisfies Partial<TRPCError>);
expect(serviceMocks.createEventType).not.toHaveBeenCalled();
});
it("rejects host user IDs that are not accepted members of the team", async () => {
serviceMocks.listAcceptedTeamMemberIds.mockResolvedValue([101]);
await expect(
createHandler({
ctx: buildCtx(),
input: {
...baseTeamInput,
hosts: [{ userId: 999, isFixed: false, priority: 2, weight: 100, scheduleId: null, groupId: null }],
} as TCreateInputSchema & {
hosts: Array<{
userId: number;
isFixed: boolean;
priority: number;
weight: number;
scheduleId: number | null;
groupId: string | null;
}>;
},
})
).rejects.toMatchObject({
code: "FORBIDDEN",
} satisfies Partial<TRPCError>);
expect(serviceMocks.createEventType).not.toHaveBeenCalled();
});
});
@@ -1,6 +1,8 @@
import { getDefaultLocations } from "@calcom/app-store/_utils/getDefaultLocations";
import { DailyLocationType } from "@calcom/app-store/constants";
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
import { MembershipService } from "@calcom/features/membership/services/membershipService";
import type { PrismaClient } from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
@@ -10,15 +12,17 @@ import type { z } from "zod";
import type { TrpcSessionUser } from "../../../../types";
import type { TCreateInputSchema } from "./create.schema";
class PermissionCheckService {
constructor(_prisma?: unknown) {}
async checkPermission(..._args: unknown[]) { return true; }
async hasPermission(..._args: unknown[]) { return true; }
async getTeamIdsWithPermission(..._args: unknown[]): Promise<number[]> { return []; }
}
type EventTypeLocation = z.infer<typeof eventTypeLocations>[number];
type CreateHostInput = {
userId: number;
isFixed?: boolean;
priority?: number | null;
weight?: number | null;
scheduleId?: number | null;
groupId?: string | null;
};
type SessionUser = NonNullable<TrpcSessionUser>;
type User = {
id: SessionUser["id"];
@@ -50,24 +54,23 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
locations: inputLocations,
scheduleId,
calVideoSettings,
hosts,
...rest
} = input;
} = input as TCreateInputSchema & { hosts?: CreateHostInput[] };
const userId = ctx.user.id;
const isManagedEventType = schedulingType === SchedulingType.MANAGED;
const isOrgAdmin = !!ctx.user?.organization?.isOrgAdmin;
const permissionService = new PermissionCheckService();
const membershipRepository = new MembershipRepository(ctx.prisma);
const membershipService = new MembershipService(membershipRepository);
// Check if user has organization-level eventType.create permission (equivalent to org admin for event types)
let hasOrgEventTypeCreatePermission = isOrgAdmin; // Default fallback
if (ctx.user.organizationId) {
hasOrgEventTypeCreatePermission = await permissionService.checkPermission({
userId,
teamId: ctx.user.organizationId,
permission: "eventType.create",
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
});
if (!hasOrgEventTypeCreatePermission && ctx.user.organizationId) {
hasOrgEventTypeCreatePermission = await membershipService.hasAnyRole(ctx.user.organizationId, userId, [
MembershipRole.ADMIN,
MembershipRole.OWNER,
]);
}
const locations: EventTypeLocation[] =
@@ -103,13 +106,10 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
if (teamId && schedulingType) {
const isSystemAdmin = ctx.user.role === "ADMIN";
// Only check for team-level permissions - this will also check for membership
const hasCreatePermission = await permissionService.checkPermission({
userId,
teamId,
permission: "eventType.create",
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
});
const hasCreatePermission = await membershipService.hasAnyRole(teamId, userId, [
MembershipRole.ADMIN,
MembershipRole.OWNER,
]);
if (!isSystemAdmin && !hasOrgEventTypeCreatePermission && !hasCreatePermission) {
// If none of the above conditions are met, the user is unauthorized.
@@ -124,6 +124,26 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
},
};
data.schedulingType = schedulingType;
if (hosts?.length) {
const acceptedTeamMemberIds = await membershipRepository.listAcceptedTeamMemberIds({ teamId });
const acceptedTeamMemberIdSet = new Set(acceptedTeamMemberIds);
if (!hosts.every((host) => acceptedTeamMemberIdSet.has(host.userId))) {
throw new TRPCError({ code: "FORBIDDEN" });
}
data.hosts = {
create: hosts.map((host) => ({
userId: host.userId,
isFixed: schedulingType === SchedulingType.COLLECTIVE || host.isFixed || false,
priority: host.priority ?? 2,
weight: host.weight ?? 100,
scheduleId: host.scheduleId ?? null,
groupId: host.groupId ?? null,
})),
};
}
}
// If we are in an organization & they don't have org-level eventType.create permission & they are not creating an event on a teamID
@@ -99,6 +99,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
where: { id },
select: {
title: true,
schedulingType: true,
locations: true,
description: true,
seatsPerTimeSlot: true,
@@ -454,6 +455,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
let hostLocationDeletions: { userId: number; eventTypeId: number }[] = [];
if (teamId && hosts) {
const effectiveSchedulingType = data.schedulingType ?? eventType.schedulingType;
// check if all hosts can be assigned (memberships that have accepted invite)
const teamMemberIds = await membershipRepo.listAcceptedTeamMemberIds({ teamId });
const teamMemberIdSet = new Set(teamMemberIds);
@@ -499,7 +501,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
};
} = {
userId: host.userId,
isFixed: data.schedulingType === SchedulingType.COLLECTIVE || host.isFixed || false,
isFixed: effectiveSchedulingType === SchedulingType.COLLECTIVE || host.isFixed || false,
priority: host.priority ?? 2,
weight: host.weight ?? 100,
groupId: host.groupId,
@@ -544,7 +546,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
};
};
} = {
isFixed: data.schedulingType === SchedulingType.COLLECTIVE || host.isFixed,
isFixed: effectiveSchedulingType === SchedulingType.COLLECTIVE || host.isFixed,
priority: host.priority ?? 2,
weight: host.weight ?? 100,
scheduleId: host.scheduleId === undefined ? undefined : host.scheduleId,