Skip to content

Commit 8a17ebc

Browse files
authored
feat: Troubleshooter atom (calcom#27497)
1 parent a71d62d commit 8a17ebc

31 files changed

Lines changed: 816 additions & 300 deletions

apps/api/v2/src/modules/atoms/controllers/atoms.event-types.controller.ts

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
2+
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries/event-types";
3+
import { listWithTeamHandler, PublicEventType } from "@calcom/platform-libraries/event-types";
4+
import { ApiResponse } from "@calcom/platform-types";
5+
import {
6+
Body,
7+
Controller,
8+
Get,
9+
Param,
10+
ParseIntPipe,
11+
Patch,
12+
Query,
13+
UseGuards,
14+
VERSION_NEUTRAL,
15+
Version,
16+
} from "@nestjs/common";
17+
import { ApiExcludeController as DocsExcludeController, ApiTags as DocsTags } from "@nestjs/swagger";
118
import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output";
219
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
320
import {
@@ -16,24 +33,6 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
1633
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
1734
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
1835
import { UserWithProfile } from "@/modules/users/users.repository";
19-
import {
20-
Controller,
21-
Get,
22-
Param,
23-
ParseIntPipe,
24-
UseGuards,
25-
Version,
26-
VERSION_NEUTRAL,
27-
Patch,
28-
Body,
29-
Query,
30-
} from "@nestjs/common";
31-
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
32-
33-
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
34-
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries/event-types";
35-
import { PublicEventType } from "@calcom/platform-libraries/event-types";
36-
import { ApiResponse } from "@calcom/platform-types";
3736

3837
/*
3938
Event-types endpoints for atoms, split from AtomsController for clarity and maintainability.
@@ -49,6 +48,17 @@ These endpoints should not be recommended for use by third party and are exclude
4948
export class AtomsEventTypesController {
5049
constructor(private readonly eventTypesService: EventTypesAtomService) {}
5150

51+
@Get("/event-types/list-with-team")
52+
@Version(VERSION_NEUTRAL)
53+
@UseGuards(ApiAuthGuard)
54+
async listEventTypesWithTeam(@GetUser() user: UserWithProfile): Promise<ApiResponse<unknown>> {
55+
const eventTypes = await listWithTeamHandler({ ctx: { user } });
56+
return {
57+
status: SUCCESS_STATUS,
58+
data: eventTypes,
59+
};
60+
}
61+
5262
@Get("/event-types/:eventSlug/public")
5363
async getPublicEventType(
5464
@Param("eventSlug") eventSlug: string,

apps/api/v2/src/modules/atoms/controllers/atoms.schedules.controller.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ export class AtomsSchedulesController {
7373
};
7474
}
7575

76+
@Get("/schedules/event-type/:eventSlug")
77+
@Version(VERSION_NEUTRAL)
78+
@UseGuards(ApiAuthGuard)
79+
@Permissions([SCHEDULE_READ])
80+
async getScheduleByEventSlug(
81+
@GetUser() user: UserWithProfile,
82+
@Param("eventSlug") eventSlug: string
83+
): Promise<ApiResponse<unknown>> {
84+
const schedule = await this.schedulesService.getScheduleByEventSlug(user, eventSlug);
85+
return {
86+
status: SUCCESS_STATUS,
87+
data: schedule,
88+
};
89+
}
90+
7691
@Get("/schedules/all")
7792
@Version(VERSION_NEUTRAL)
7893
@UseGuards(ApiAuthGuard)

apps/api/v2/src/modules/atoms/services/schedules-atom.service.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
12
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
23
import { UsersRepository } from "@/modules/users/users.repository";
34
import { UserWithProfile } from "@/modules/users/users.repository";
45
import { Logger } from "@nestjs/common";
56
import { Injectable } from "@nestjs/common";
67

78
import { ScheduleRepository, UpdateScheduleResponse } from "@calcom/platform-libraries/schedules";
9+
import { getScheduleByEventSlugHandler } from "@calcom/platform-libraries/schedules";
810
import { updateSchedule } from "@calcom/platform-libraries/schedules";
911
import { UpdateAtomScheduleDto } from "@calcom/platform-types";
1012
import type { PrismaClient } from "@calcom/prisma";
@@ -15,6 +17,7 @@ export class SchedulesAtomsService {
1517

1618
constructor(
1719
private readonly usersRepository: UsersRepository,
20+
private readonly dbRead: PrismaReadService,
1821
private readonly dbWrite: PrismaWriteService
1922
) {}
2023

@@ -42,6 +45,13 @@ export class SchedulesAtomsService {
4245
});
4346
}
4447

48+
async getScheduleByEventSlug(user: UserWithProfile, eventSlug: string) {
49+
return getScheduleByEventSlugHandler({
50+
ctx: { user, prisma: this.dbRead.prisma as unknown as PrismaClient },
51+
input: { eventSlug },
52+
});
53+
}
54+
4555
async updateUserSchedule({
4656
input,
4757
user,

apps/web/modules/troubleshooter/components/AvailabilitySchedulesContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Badge } from "@calcom/ui/components/badge";
33
import { Button } from "@calcom/ui/components/button";
44
import { Switch } from "@calcom/ui/components/form";
55

6-
import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer";
6+
import { TroubleshooterListItemContainer } from "@calcom/features/troubleshooter/components/TroubleshooterListItemContainer";
77

88
function AvailabiltyItem() {
99
const { t } = useLocale();
Lines changed: 9 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,16 @@
1-
import { useLocale } from "@calcom/lib/hooks/useLocale";
1+
import { CalendarToggleContainerComponent } from "@calcom/features/troubleshooter/components/CalendarToggleContainerComponent";
22
import { trpc } from "@calcom/trpc/react";
3-
import { Badge } from "@calcom/ui/components/badge";
4-
import { Button } from "@calcom/ui/components/button";
5-
import { Switch } from "@calcom/ui/components/form";
6-
7-
import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer";
8-
9-
const SELECTION_COLORS = ["#f97316", "#84cc16", "#06b6d4", "#8b5cf6", "#ec4899", "#f43f5e"];
10-
11-
interface CalendarToggleItemProps {
12-
title: string;
13-
subtitle: string;
14-
colorDot?: string;
15-
status: "connected" | "not_found";
16-
calendars?: {
17-
active?: boolean;
18-
name?: string;
19-
}[];
20-
}
21-
function CalendarToggleItem(props: CalendarToggleItemProps) {
22-
const badgeStatus = props.status === "connected" ? "green" : "orange";
23-
const badgeText = props.status === "connected" ? "Connected" : "Not found";
24-
return (
25-
<TroubleshooterListItemContainer
26-
title={props.title}
27-
subtitle={props.subtitle}
28-
prefixSlot={
29-
<>
30-
<div
31-
className="h-4 w-4 self-center rounded-[4px]"
32-
style={{
33-
backgroundColor: props.colorDot,
34-
}}
35-
/>
36-
</>
37-
}
38-
suffixSlot={
39-
<div>
40-
<Badge variant={badgeStatus} withDot size="sm">
41-
{badgeText}
42-
</Badge>
43-
</div>
44-
}>
45-
<div className="[&>*]:text-emphasis flex flex-col gap-3">
46-
{props.calendars?.map((calendar) => {
47-
return <Switch key={calendar.name} checked={calendar.active} label={calendar.name} disabled />;
48-
})}
49-
</div>
50-
</TroubleshooterListItemContainer>
51-
);
52-
}
53-
54-
function EmptyCalendarToggleItem() {
55-
const { t } = useLocale();
56-
57-
return (
58-
<TroubleshooterListItemContainer
59-
title={t("installed", { count: 0 })}
60-
subtitle={t("please_install_a_calendar")}
61-
prefixSlot={
62-
<>
63-
<div className="h-4 w-4 self-center rounded-[4px] bg-blue-500" />
64-
</>
65-
}
66-
suffixSlot={
67-
<div>
68-
<Badge variant="orange" withDot size="sm">
69-
{t("unavailable")}
70-
</Badge>
71-
</div>
72-
}>
73-
<div className="flex flex-col gap-3">
74-
<Button color="secondary" className="justify-center gap-2" href="/apps/categories/calendar">
75-
{t("install_calendar")}
76-
</Button>
77-
</div>
78-
</TroubleshooterListItemContainer>
79-
);
80-
}
813

824
export function CalendarToggleContainer() {
83-
const { t } = useLocale();
84-
const { data, isLoading } = trpc.viewer.calendars.connectedCalendars.useQuery();
85-
86-
const hasConnectedCalendars = data && data?.connectedCalendars.length > 0;
5+
const { data, isLoading } =
6+
trpc.viewer.calendars.connectedCalendars.useQuery();
877

888
return (
89-
<div className="flex flex-col stack-y-3">
90-
<p className="text-sm font-medium leading-none">{t("calendars_were_checking_for_conflicts")}</p>
91-
{hasConnectedCalendars && !isLoading ? (
92-
<>
93-
{data.connectedCalendars.map((calendar) => {
94-
const foundPrimary = calendar.calendars?.find((item) => item.primary);
95-
// Will be used when getAvailbility is modified to use externalId instead of appId for source.
96-
// const color = SELECTION_COLORS[idx] || "#000000";
97-
// // Add calendar to color map using externalId (what we use on the backend to determine source)
98-
// addToColorMap(foundPrimary?.externalId, color);
99-
return (
100-
<CalendarToggleItem
101-
key={calendar.credentialId}
102-
title={calendar.integration.name}
103-
colorDot="#000000"
104-
subtitle={foundPrimary?.name ?? "Nameless Calendar"}
105-
status={calendar.error ? "not_found" : "connected"}
106-
calendars={calendar.calendars?.map((item) => {
107-
return {
108-
active: item.isSelected,
109-
name: item.name,
110-
};
111-
})}
112-
/>
113-
);
114-
})}
115-
<Button color="secondary" className="justify-center gap-2" href="/settings/my-account/calendars">
116-
{t("manage_calendars")}
117-
</Button>
118-
</>
119-
) : (
120-
<EmptyCalendarToggleItem />
121-
)}
122-
</div>
9+
<CalendarToggleContainerComponent
10+
connectedCalendars={data?.connectedCalendars ?? []}
11+
isLoading={isLoading}
12+
manageCalendarsAction={{ href: "/settings/my-account/calendars" }}
13+
installCalendarAction={{ href: "/apps/categories/calendar" }}
14+
/>
12315
);
12416
}

apps/web/modules/troubleshooter/components/ConnectedAppsContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useLocale } from "@calcom/lib/hooks/useLocale";
22
import { Badge } from "@calcom/ui/components/badge";
33

4-
import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer";
4+
import { TroubleshooterListItemHeader } from "@calcom/features/troubleshooter/components/TroubleshooterListItemContainer";
55

66
function ConnectedAppsItem() {
77
return (
Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,42 @@
1-
import Link from "next/link";
2-
1+
import { EventScheduleItemComponent } from "@calcom/features/troubleshooter/components/EventScheduleItemComponent";
2+
import { useTroubleshooterStore } from "@calcom/features/troubleshooter/store";
33
import { useLocale } from "@calcom/lib/hooks/useLocale";
44
import { trpc } from "@calcom/trpc/react";
55
import { Badge } from "@calcom/ui/components/badge";
6-
import { Label } from "@calcom/ui/components/form";
6+
import Link from "next/link";
77

8-
import { useTroubleshooterStore } from "@calcom/features/troubleshooter/store";
9-
import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer";
8+
export { EventScheduleItemComponent };
109

11-
export function EventScheduleItem() {
10+
export function EventScheduleItem(): JSX.Element {
1211
const { t } = useLocale();
1312
const selectedEventType = useTroubleshooterStore((state) => state.event);
1413

15-
const { data: schedule } = trpc.viewer.availability.schedule.getScheduleByEventSlug.useQuery(
16-
{
17-
eventSlug: selectedEventType?.slug as string,
18-
},
19-
{
20-
enabled: !!selectedEventType?.slug,
21-
}
22-
);
14+
const { data: schedule } =
15+
trpc.viewer.availability.schedule.getScheduleByEventSlug.useQuery(
16+
{
17+
eventSlug: selectedEventType?.slug as string,
18+
},
19+
{
20+
enabled: !!selectedEventType?.slug,
21+
}
22+
);
2323

2424
return (
25-
<div>
26-
<Label>{t("availability_schedule")}</Label>
27-
<TroubleshooterListItemHeader
28-
className="group rounded-md border-b"
29-
prefixSlot={<div className="w-4 rounded-[4px] bg-black" />}
30-
title={schedule?.name ?? "Loading"}
31-
suffixSlot={
32-
schedule && (
33-
<Link href={`/availability/${schedule.id}`} className="inline-flex">
34-
<Badge color="orange" size="sm" className="invisible hover:cursor-pointer group-hover:visible">
35-
{t("edit")}
36-
</Badge>
37-
</Link>
38-
)
39-
}
40-
/>
41-
</div>
25+
<EventScheduleItemComponent
26+
schedule={schedule ?? null}
27+
suffixSlot={
28+
schedule && (
29+
<Link href={`/availability/${schedule.id}`} className="inline-flex">
30+
<Badge
31+
color="orange"
32+
size="sm"
33+
className="invisible hover:cursor-pointer group-hover:visible"
34+
>
35+
{t("edit")}
36+
</Badge>
37+
</Link>
38+
)
39+
}
40+
/>
4241
);
4342
}

0 commit comments

Comments
 (0)