fix: troubleshooter team events + improve race condition (#25704)

* fix query to bring back teams + fix hover state

* fix team issues with troubleshooter
This commit is contained in:
sean-brydon
2025-12-09 07:37:21 +00:00
committed by GitHub
parent 161ebdbbec
commit 75b93cebdb
5 changed files with 67 additions and 40 deletions
@@ -31,7 +31,7 @@ export function EventScheduleItem() {
suffixSlot={
schedule && (
<Link href={`/availability/${schedule.id}`} className="inline-flex">
<Badge color="orange" size="sm" className="hidden hover:cursor-pointer group-hover:inline-flex">
<Badge color="orange" size="sm" className="invisible hover:cursor-pointer group-hover:visible">
{t("edit")}
</Badge>
</Link>
@@ -1,4 +1,5 @@
import { useMemo, useEffect, startTransition } from "react";
import { shallow } from "zustand/shallow";
import { trpc } from "@calcom/trpc";
import { SelectField } from "@calcom/ui/components/form";
@@ -7,67 +8,80 @@ import { getQueryParam } from "../../bookings/Booker/utils/query-param";
import { useTroubleshooterStore } from "../store";
export function EventTypeSelect() {
const { data: eventTypes, isPending } = trpc.viewer.eventTypes.list.useQuery();
const selectedEventType = useTroubleshooterStore((state) => state.event);
const setSelectedEventType = useTroubleshooterStore((state) => state.setEvent);
const selectedEventQueryParam = getQueryParam("eventType");
const { data: eventTypes, isPending } = trpc.viewer.eventTypes.listWithTeam.useQuery();
const { event: selectedEventType, setEvent: setSelectedEventType } = useTroubleshooterStore(
(state) => ({
event: state.event,
setEvent: state.setEvent,
}),
shallow
);
const options = useMemo(() => {
if (!eventTypes) return [];
return eventTypes.map((e) => ({
label: e.title,
value: e.slug,
value: e.id.toString(),
id: e.id,
duration: e.length,
}));
}, [eventTypes]);
// Initialize event type from query param or default to first event
useEffect(() => {
if (!selectedEventType && eventTypes && eventTypes[0] && !selectedEventQueryParam) {
const { id, slug, length } = eventTypes[0];
setSelectedEventType({
id,
slug,
duration: length,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [eventTypes]);
if (!eventTypes || eventTypes.length === 0) return;
useEffect(() => {
if (selectedEventQueryParam) {
// ensure that the update is deferred until the Suspense boundary has finished hydrating
const selectedEventIdParam = getQueryParam("eventTypeId");
const eventTypeId = selectedEventIdParam ? parseInt(selectedEventIdParam, 10) : null;
// If we already have a selected event that matches the query param, don't do anything
if (selectedEventType?.id === eventTypeId) return;
// If there's a query param, try to find and set that event
if (eventTypeId && !isNaN(eventTypeId)) {
startTransition(() => {
const foundEventType = eventTypes?.find((et) => et.slug === selectedEventQueryParam);
const foundEventType = eventTypes.find((et) => et.id === eventTypeId);
if (foundEventType) {
const { id, slug, length } = foundEventType;
setSelectedEventType({ id, slug, duration: length });
} else if (eventTypes && eventTypes[0]) {
const { id, slug, length } = eventTypes[0];
setSelectedEventType({
id,
slug,
duration: length,
id: foundEventType.id,
slug: foundEventType.slug,
duration: foundEventType.length,
teamId: foundEventType.team?.id ?? null,
});
return;
}
});
}
}, [eventTypes, selectedEventQueryParam, setSelectedEventType]);
// If no event is selected and no valid query param, default to first event
if (!selectedEventType && !eventTypeId) {
const firstEvent = eventTypes[0];
setSelectedEventType({
id: firstEvent.id,
slug: firstEvent.slug,
duration: firstEvent.length,
teamId: firstEvent.team?.id ?? null,
});
}
}, [eventTypes, selectedEventType, setSelectedEventType]);
return (
<SelectField
label="Event Type"
options={options}
isDisabled={isPending || options.length === 0}
value={options.find((option) => option.value === selectedEventType?.slug) || options[0]}
value={options.find((option) => option.id === selectedEventType?.id) || options[0]}
onChange={(option) => {
if (!option) return;
setSelectedEventType({
slug: option.value,
id: option.id,
duration: option.duration,
});
const foundEventType = eventTypes?.find((et) => et.id === option.id);
if (foundEventType) {
setSelectedEventType({
id: foundEventType.id,
slug: foundEventType.slug,
duration: foundEventType.length,
teamId: foundEventType.team?.id ?? null,
});
}
}}
/>
);
@@ -28,6 +28,7 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
.add(extraDays - 1, "day")
.utc()
.format(),
eventTypeId: event?.id,
withSource: true,
},
{
@@ -35,13 +36,16 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
}
);
const isTeamEvent = !!event?.teamId;
const { data: schedule } = useSchedule({
username: session?.user.username || "",
eventSlug: event?.slug,
// For team events, don't pass eventSlug to avoid slug lookup issues - use eventId instead
eventSlug: isTeamEvent ? null : event?.slug,
eventId: event?.id,
timezone,
month: startDate.format("YYYY-MM"),
orgSlug: session?.user.org?.slug,
isTeamEvent,
});
const endDate = dayjs(startDate)
+2 -1
View File
@@ -17,6 +17,7 @@ type EventType = {
id: number;
slug: string;
duration: number;
teamId?: number | null;
};
export type TroubleshooterStore = {
@@ -76,7 +77,7 @@ export const useTroubleshooterStore = create<TroubleshooterStore>((set, get) =>
event: null,
setEvent: (event: EventType) => {
set({ event });
updateQueryParam("eventType", event.slug ?? "");
updateQueryParam("eventTypeId", event.id.toString());
},
month: getQueryParam("month") || getQueryParam("date") || dayjs().format("YYYY-MM"),
setMonth: (month: string | null) => {
@@ -11,19 +11,26 @@ type ListWithTeamOptions = {
export const listWithTeamHandler = async ({ ctx }: ListWithTeamOptions) => {
const userId = ctx.user.id;
const query = Prisma.sql`SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "j1"."name" as "teamName"
const query = Prisma.sql`SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "public"."EventType"."length", "j1"."name" as "teamName"
FROM "public"."EventType"
LEFT JOIN "public"."Team" AS "j1" ON ("j1"."id") = ("public"."EventType"."teamId")
WHERE "public"."EventType"."userId" = ${userId}
UNION
SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "j1"."name" as "teamName"
SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "public"."EventType"."length", "j1"."name" as "teamName"
FROM "public"."EventType"
INNER JOIN "public"."Team" AS "j1" ON ("j1"."id") = ("public"."EventType"."teamId")
INNER JOIN "public"."Membership" AS "t2" ON "t2"."teamId" = "j1"."id"
WHERE "t2"."userId" = ${userId} AND "t2"."accepted" = true`;
const result = await db.$queryRaw<
{ id: number; teamId: number | null; title: string; slug: string; teamName: string | null }[]
{
id: number;
teamId: number | null;
title: string;
slug: string;
length: number;
teamName: string | null;
}[]
>(query);
return result.map((row) => ({
@@ -31,5 +38,6 @@ export const listWithTeamHandler = async ({ ctx }: ListWithTeamOptions) => {
team: row.teamId ? { id: row.teamId, name: row.teamName || "" } : null,
title: row.title,
slug: row.slug,
length: row.length,
}));
};