feat: Troubleshooter atom (#27497)
This commit is contained in:
@@ -1,3 +1,20 @@
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries/event-types";
|
||||
import { listWithTeamHandler, PublicEventType } from "@calcom/platform-libraries/event-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Query,
|
||||
UseGuards,
|
||||
VERSION_NEUTRAL,
|
||||
Version,
|
||||
} from "@nestjs/common";
|
||||
import { ApiExcludeController as DocsExcludeController, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { GetEventTypePublicOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import {
|
||||
@@ -16,24 +33,6 @@ import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
UseGuards,
|
||||
Version,
|
||||
VERSION_NEUTRAL,
|
||||
Patch,
|
||||
Body,
|
||||
Query,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries/event-types";
|
||||
import { PublicEventType } from "@calcom/platform-libraries/event-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
/*
|
||||
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
|
||||
export class AtomsEventTypesController {
|
||||
constructor(private readonly eventTypesService: EventTypesAtomService) {}
|
||||
|
||||
@Get("/event-types/list-with-team")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async listEventTypesWithTeam(@GetUser() user: UserWithProfile): Promise<ApiResponse<unknown>> {
|
||||
const eventTypes = await listWithTeamHandler({ ctx: { user } });
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: eventTypes,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/event-types/:eventSlug/public")
|
||||
async getPublicEventType(
|
||||
@Param("eventSlug") eventSlug: string,
|
||||
|
||||
@@ -73,6 +73,21 @@ export class AtomsSchedulesController {
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/schedules/event-type/:eventSlug")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@Permissions([SCHEDULE_READ])
|
||||
async getScheduleByEventSlug(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("eventSlug") eventSlug: string
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
const schedule = await this.schedulesService.getScheduleByEventSlug(user, eventSlug);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: schedule,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/schedules/all")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
@@ -5,6 +6,7 @@ import { Logger } from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { ScheduleRepository, UpdateScheduleResponse } from "@calcom/platform-libraries/schedules";
|
||||
import { getScheduleByEventSlugHandler } from "@calcom/platform-libraries/schedules";
|
||||
import { updateSchedule } from "@calcom/platform-libraries/schedules";
|
||||
import { UpdateAtomScheduleDto } from "@calcom/platform-types";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
@@ -15,6 +17,7 @@ export class SchedulesAtomsService {
|
||||
|
||||
constructor(
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService
|
||||
) {}
|
||||
|
||||
@@ -42,6 +45,13 @@ export class SchedulesAtomsService {
|
||||
});
|
||||
}
|
||||
|
||||
async getScheduleByEventSlug(user: UserWithProfile, eventSlug: string) {
|
||||
return getScheduleByEventSlugHandler({
|
||||
ctx: { user, prisma: this.dbRead.prisma as unknown as PrismaClient },
|
||||
input: { eventSlug },
|
||||
});
|
||||
}
|
||||
|
||||
async updateUserSchedule({
|
||||
input,
|
||||
user,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
|
||||
import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer";
|
||||
import { TroubleshooterListItemContainer } from "@calcom/features/troubleshooter/components/TroubleshooterListItemContainer";
|
||||
|
||||
function AvailabiltyItem() {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -1,124 +1,16 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { CalendarToggleContainerComponent } from "@calcom/features/troubleshooter/components/CalendarToggleContainerComponent";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
|
||||
import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer";
|
||||
|
||||
const SELECTION_COLORS = ["#f97316", "#84cc16", "#06b6d4", "#8b5cf6", "#ec4899", "#f43f5e"];
|
||||
|
||||
interface CalendarToggleItemProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
colorDot?: string;
|
||||
status: "connected" | "not_found";
|
||||
calendars?: {
|
||||
active?: boolean;
|
||||
name?: string;
|
||||
}[];
|
||||
}
|
||||
function CalendarToggleItem(props: CalendarToggleItemProps) {
|
||||
const badgeStatus = props.status === "connected" ? "green" : "orange";
|
||||
const badgeText = props.status === "connected" ? "Connected" : "Not found";
|
||||
return (
|
||||
<TroubleshooterListItemContainer
|
||||
title={props.title}
|
||||
subtitle={props.subtitle}
|
||||
prefixSlot={
|
||||
<>
|
||||
<div
|
||||
className="h-4 w-4 self-center rounded-[4px]"
|
||||
style={{
|
||||
backgroundColor: props.colorDot,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
suffixSlot={
|
||||
<div>
|
||||
<Badge variant={badgeStatus} withDot size="sm">
|
||||
{badgeText}
|
||||
</Badge>
|
||||
</div>
|
||||
}>
|
||||
<div className="[&>*]:text-emphasis flex flex-col gap-3">
|
||||
{props.calendars?.map((calendar) => {
|
||||
return <Switch key={calendar.name} checked={calendar.active} label={calendar.name} disabled />;
|
||||
})}
|
||||
</div>
|
||||
</TroubleshooterListItemContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyCalendarToggleItem() {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<TroubleshooterListItemContainer
|
||||
title={t("installed", { count: 0 })}
|
||||
subtitle={t("please_install_a_calendar")}
|
||||
prefixSlot={
|
||||
<>
|
||||
<div className="h-4 w-4 self-center rounded-[4px] bg-blue-500" />
|
||||
</>
|
||||
}
|
||||
suffixSlot={
|
||||
<div>
|
||||
<Badge variant="orange" withDot size="sm">
|
||||
{t("unavailable")}
|
||||
</Badge>
|
||||
</div>
|
||||
}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button color="secondary" className="justify-center gap-2" href="/apps/categories/calendar">
|
||||
{t("install_calendar")}
|
||||
</Button>
|
||||
</div>
|
||||
</TroubleshooterListItemContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export function CalendarToggleContainer() {
|
||||
const { t } = useLocale();
|
||||
const { data, isLoading } = trpc.viewer.calendars.connectedCalendars.useQuery();
|
||||
|
||||
const hasConnectedCalendars = data && data?.connectedCalendars.length > 0;
|
||||
const { data, isLoading } =
|
||||
trpc.viewer.calendars.connectedCalendars.useQuery();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col stack-y-3">
|
||||
<p className="text-sm font-medium leading-none">{t("calendars_were_checking_for_conflicts")}</p>
|
||||
{hasConnectedCalendars && !isLoading ? (
|
||||
<>
|
||||
{data.connectedCalendars.map((calendar) => {
|
||||
const foundPrimary = calendar.calendars?.find((item) => item.primary);
|
||||
// Will be used when getAvailbility is modified to use externalId instead of appId for source.
|
||||
// const color = SELECTION_COLORS[idx] || "#000000";
|
||||
// // Add calendar to color map using externalId (what we use on the backend to determine source)
|
||||
// addToColorMap(foundPrimary?.externalId, color);
|
||||
return (
|
||||
<CalendarToggleItem
|
||||
key={calendar.credentialId}
|
||||
title={calendar.integration.name}
|
||||
colorDot="#000000"
|
||||
subtitle={foundPrimary?.name ?? "Nameless Calendar"}
|
||||
status={calendar.error ? "not_found" : "connected"}
|
||||
calendars={calendar.calendars?.map((item) => {
|
||||
return {
|
||||
active: item.isSelected,
|
||||
name: item.name,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Button color="secondary" className="justify-center gap-2" href="/settings/my-account/calendars">
|
||||
{t("manage_calendars")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<EmptyCalendarToggleItem />
|
||||
)}
|
||||
</div>
|
||||
<CalendarToggleContainerComponent
|
||||
connectedCalendars={data?.connectedCalendars ?? []}
|
||||
isLoading={isLoading}
|
||||
manageCalendarsAction={{ href: "/settings/my-account/calendars" }}
|
||||
installCalendarAction={{ href: "/apps/categories/calendar" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
|
||||
import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer";
|
||||
import { TroubleshooterListItemHeader } from "@calcom/features/troubleshooter/components/TroubleshooterListItemContainer";
|
||||
|
||||
function ConnectedAppsItem() {
|
||||
return (
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { EventScheduleItemComponent } from "@calcom/features/troubleshooter/components/EventScheduleItemComponent";
|
||||
import { useTroubleshooterStore } from "@calcom/features/troubleshooter/store";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Label } from "@calcom/ui/components/form";
|
||||
import Link from "next/link";
|
||||
|
||||
import { useTroubleshooterStore } from "@calcom/features/troubleshooter/store";
|
||||
import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer";
|
||||
export { EventScheduleItemComponent };
|
||||
|
||||
export function EventScheduleItem() {
|
||||
export function EventScheduleItem(): JSX.Element {
|
||||
const { t } = useLocale();
|
||||
const selectedEventType = useTroubleshooterStore((state) => state.event);
|
||||
|
||||
const { data: schedule } = trpc.viewer.availability.schedule.getScheduleByEventSlug.useQuery(
|
||||
{
|
||||
eventSlug: selectedEventType?.slug as string,
|
||||
},
|
||||
{
|
||||
enabled: !!selectedEventType?.slug,
|
||||
}
|
||||
);
|
||||
const { data: schedule } =
|
||||
trpc.viewer.availability.schedule.getScheduleByEventSlug.useQuery(
|
||||
{
|
||||
eventSlug: selectedEventType?.slug as string,
|
||||
},
|
||||
{
|
||||
enabled: !!selectedEventType?.slug,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label>{t("availability_schedule")}</Label>
|
||||
<TroubleshooterListItemHeader
|
||||
className="group rounded-md border-b"
|
||||
prefixSlot={<div className="w-4 rounded-[4px] bg-black" />}
|
||||
title={schedule?.name ?? "Loading"}
|
||||
suffixSlot={
|
||||
schedule && (
|
||||
<Link href={`/availability/${schedule.id}`} className="inline-flex">
|
||||
<Badge color="orange" size="sm" className="invisible hover:cursor-pointer group-hover:visible">
|
||||
{t("edit")}
|
||||
</Badge>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<EventScheduleItemComponent
|
||||
schedule={schedule ?? null}
|
||||
suffixSlot={
|
||||
schedule && (
|
||||
<Link href={`/availability/${schedule.id}`} className="inline-flex">
|
||||
<Badge
|
||||
color="orange"
|
||||
size="sm"
|
||||
className="invisible hover:cursor-pointer group-hover:visible"
|
||||
>
|
||||
{t("edit")}
|
||||
</Badge>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,88 +1,16 @@
|
||||
import { useMemo, useEffect, startTransition } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import { EventTypeSelectComponent } from "@calcom/features/troubleshooter/components/EventTypeSelectComponent";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { SelectField } from "@calcom/ui/components/form";
|
||||
|
||||
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
||||
import { useTroubleshooterStore } from "@calcom/features/troubleshooter/store";
|
||||
export { EventTypeSelectComponent };
|
||||
|
||||
export function EventTypeSelect() {
|
||||
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.id.toString(),
|
||||
id: e.id,
|
||||
duration: e.length,
|
||||
}));
|
||||
}, [eventTypes]);
|
||||
|
||||
// Initialize event type from query param or default to first event
|
||||
useEffect(() => {
|
||||
if (!eventTypes || eventTypes.length === 0) return;
|
||||
|
||||
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.id === eventTypeId);
|
||||
if (foundEventType) {
|
||||
setSelectedEventType({
|
||||
id: foundEventType.id,
|
||||
slug: foundEventType.slug,
|
||||
duration: foundEventType.length,
|
||||
teamId: foundEventType.team?.id ?? null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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]);
|
||||
export function EventTypeSelect(): JSX.Element {
|
||||
const { data: eventTypes, isPending } =
|
||||
trpc.viewer.eventTypes.listWithTeam.useQuery();
|
||||
|
||||
return (
|
||||
<SelectField
|
||||
label="Event Type"
|
||||
options={options}
|
||||
isDisabled={isPending || options.length === 0}
|
||||
value={options.find((option) => option.id === selectedEventType?.id) || options[0]}
|
||||
onChange={(option) => {
|
||||
if (!option) return;
|
||||
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,
|
||||
});
|
||||
}
|
||||
}}
|
||||
<EventTypeSelectComponent
|
||||
eventTypes={eventTypes ?? []}
|
||||
isPending={isPending}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1247,6 +1247,7 @@
|
||||
"invitees_can_schedule": "Invitees can schedule",
|
||||
"date_range": "Date range",
|
||||
"calendar_days": "calendar days",
|
||||
"nameless_calendar": "Nameless Calendar",
|
||||
"business_days": "business days",
|
||||
"set_address_place": "Set an address or place",
|
||||
"set_link_meeting": "Set a link to the meeting",
|
||||
@@ -2925,6 +2926,7 @@
|
||||
"directory_sync_delete_confirmation": "This action cannot be undone. This will permanently delete the directory sync connection.",
|
||||
"event_setup_length_error": "Event Setup: The duration must be at least 1 minute.",
|
||||
"availability_schedules": "Availability schedules",
|
||||
"availability_schedule": "Availability Schedule",
|
||||
"unauthorized": "Unauthorized",
|
||||
"access_cal_account": "{{clientName}} wants to access your {{appName}} account",
|
||||
"select_account_team": "Select account or team",
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import { TroubleshooterListItemContainer } from "./TroubleshooterListItemContainer";
|
||||
|
||||
interface CalendarToggleItemProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
colorDot?: string;
|
||||
status: "connected" | "not_found";
|
||||
calendars?: {
|
||||
active?: boolean;
|
||||
name?: string;
|
||||
}[];
|
||||
}
|
||||
function CalendarToggleItem(props: CalendarToggleItemProps) {
|
||||
const badgeStatus = props.status === "connected" ? "green" : "orange";
|
||||
const badgeText = props.status === "connected" ? "Connected" : "Not found";
|
||||
return (
|
||||
<TroubleshooterListItemContainer
|
||||
title={props.title}
|
||||
subtitle={props.subtitle}
|
||||
prefixSlot={
|
||||
<>
|
||||
<div
|
||||
className="h-4 w-4 self-center rounded-[4px]"
|
||||
style={{
|
||||
backgroundColor: props.colorDot,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
suffixSlot={
|
||||
<div>
|
||||
<Badge variant={badgeStatus} withDot size="sm">
|
||||
{badgeText}
|
||||
</Badge>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="[&>*]:text-emphasis flex flex-col gap-3">
|
||||
{props.calendars?.map((calendar) => {
|
||||
return (
|
||||
<Switch
|
||||
key={calendar.name}
|
||||
checked={calendar.active}
|
||||
label={calendar.name}
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TroubleshooterListItemContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyCalendarToggleItem({
|
||||
installCalendarAction,
|
||||
}: {
|
||||
installCalendarAction?: { href: string } | { onClick: () => void };
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<TroubleshooterListItemContainer
|
||||
title={t("installed", { count: 0 })}
|
||||
subtitle={t("please_install_a_calendar")}
|
||||
prefixSlot={
|
||||
<>
|
||||
<div className="h-4 w-4 self-center rounded-[4px] bg-blue-500" />
|
||||
</>
|
||||
}
|
||||
suffixSlot={
|
||||
<div>
|
||||
<Badge variant="orange" withDot size="sm">
|
||||
{t("unavailable")}
|
||||
</Badge>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{installCalendarAction && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
color="secondary"
|
||||
className="justify-center gap-2"
|
||||
{...installCalendarAction}
|
||||
>
|
||||
{t("install_calendar")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TroubleshooterListItemContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConnectedCalendarItem {
|
||||
credentialId: number;
|
||||
integration: {
|
||||
name: string;
|
||||
};
|
||||
error?: {
|
||||
message: string;
|
||||
};
|
||||
calendars?: {
|
||||
primary: boolean | null;
|
||||
isSelected: boolean;
|
||||
name?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface CalendarToggleContainerComponentProps {
|
||||
connectedCalendars: ConnectedCalendarItem[];
|
||||
isLoading: boolean;
|
||||
manageCalendarsAction?: { href: string } | { onClick: () => void };
|
||||
installCalendarAction?: { href: string } | { onClick: () => void };
|
||||
}
|
||||
|
||||
export function CalendarToggleContainerComponent({
|
||||
connectedCalendars,
|
||||
isLoading,
|
||||
manageCalendarsAction,
|
||||
installCalendarAction,
|
||||
}: CalendarToggleContainerComponentProps): JSX.Element {
|
||||
const { t } = useLocale();
|
||||
const hasConnectedCalendars = connectedCalendars.length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col stack-y-3">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{t("calendars_were_checking_for_conflicts")}
|
||||
</p>
|
||||
{hasConnectedCalendars && !isLoading ? (
|
||||
<>
|
||||
{connectedCalendars.map((calendar) => {
|
||||
const foundPrimary = calendar.calendars?.find(
|
||||
(item) => item.primary
|
||||
);
|
||||
// Will be used when getAvailbility is modified to use externalId instead of appId for source.
|
||||
// const color = SELECTION_COLORS[idx] || "#000000";
|
||||
// // Add calendar to color map using externalId (what we use on the backend to determine source)
|
||||
// addToColorMap(foundPrimary?.externalId, color);
|
||||
return (
|
||||
<CalendarToggleItem
|
||||
key={calendar.credentialId}
|
||||
title={calendar.integration.name}
|
||||
colorDot="#000000"
|
||||
subtitle={foundPrimary?.name ?? t("nameless_calendar")}
|
||||
status={calendar.error ? "not_found" : "connected"}
|
||||
calendars={calendar.calendars?.map((item) => {
|
||||
return {
|
||||
active: item.isSelected,
|
||||
name: item.name,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{manageCalendarsAction && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className="justify-center gap-2"
|
||||
{...manageCalendarsAction}
|
||||
>
|
||||
{t("manage_calendars")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<EmptyCalendarToggleItem installCalendarAction={installCalendarAction} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Label } from "@calcom/ui/components/form";
|
||||
import type React from "react";
|
||||
import { TroubleshooterListItemHeader } from "./TroubleshooterListItemContainer";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
interface ScheduleItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface EventScheduleItemComponentProps {
|
||||
schedule: ScheduleItem | null;
|
||||
suffixSlot?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function EventScheduleItemComponent({
|
||||
schedule,
|
||||
suffixSlot,
|
||||
}: EventScheduleItemComponentProps): JSX.Element {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label> {t("availability_schedule")}</Label>
|
||||
<TroubleshooterListItemHeader
|
||||
className="group rounded-md border-b"
|
||||
prefixSlot={<div className="w-4 rounded-[4px] bg-black" />}
|
||||
title={schedule?.name ?? t("loading")}
|
||||
suffixSlot={suffixSlot}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
|
||||
import { useTroubleshooterStore } from "@calcom/features/troubleshooter/store";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SelectField } from "@calcom/ui/components/form";
|
||||
import { startTransition, useEffect, useMemo } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
interface EventTypeItem {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
length: number;
|
||||
username?: string | null;
|
||||
team?: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface EventTypeSelectComponentProps {
|
||||
eventTypes: EventTypeItem[];
|
||||
isPending?: boolean;
|
||||
}
|
||||
|
||||
export function EventTypeSelectComponent({
|
||||
eventTypes,
|
||||
isPending,
|
||||
}: EventTypeSelectComponentProps): JSX.Element {
|
||||
const { t } = useLocale();
|
||||
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.id.toString(),
|
||||
id: e.id,
|
||||
duration: e.length,
|
||||
}));
|
||||
}, [eventTypes]);
|
||||
|
||||
// Initialize event type from query param or default to first event
|
||||
useEffect(() => {
|
||||
if (!eventTypes || eventTypes.length === 0) return;
|
||||
|
||||
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.id === eventTypeId);
|
||||
if (foundEventType) {
|
||||
setSelectedEventType({
|
||||
id: foundEventType.id,
|
||||
slug: foundEventType.slug,
|
||||
duration: foundEventType.length,
|
||||
teamId: foundEventType.team?.id ?? null,
|
||||
username: foundEventType.username ?? null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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,
|
||||
username: firstEvent.username ?? null,
|
||||
});
|
||||
}
|
||||
}, [eventTypes, selectedEventType, setSelectedEventType]);
|
||||
|
||||
return (
|
||||
<SelectField
|
||||
label={t("event_type")}
|
||||
options={options}
|
||||
isDisabled={isPending || options.length === 0}
|
||||
value={
|
||||
options.find((option) => option.id === selectedEventType?.id) ||
|
||||
options[0]
|
||||
}
|
||||
onChange={(option) => {
|
||||
if (!option) return;
|
||||
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,
|
||||
username: foundEventType.username ?? null,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { updateQueryParam, getQueryParam, removeQueryParam } from "../bookings/B
|
||||
*/
|
||||
type StoreInitializeType = {
|
||||
month: string | null;
|
||||
isPlatform?: boolean;
|
||||
};
|
||||
|
||||
type EventType = {
|
||||
@@ -18,9 +19,11 @@ type EventType = {
|
||||
slug: string;
|
||||
duration: number;
|
||||
teamId?: number | null;
|
||||
username?: string | null;
|
||||
};
|
||||
|
||||
export type TroubleshooterStore = {
|
||||
isPlatform: boolean;
|
||||
event: EventType | null;
|
||||
setEvent: (eventSlug: EventType) => void;
|
||||
month: string | null;
|
||||
@@ -41,6 +44,7 @@ export type TroubleshooterStore = {
|
||||
* See comments in interface above for more information on it's specific values.
|
||||
*/
|
||||
export const useTroubleshooterStore = create<TroubleshooterStore>((set, get) => ({
|
||||
isPlatform: false,
|
||||
selectedDate: getQueryParam("date") || null,
|
||||
setSelectedDate: (selectedDate: string | null) => {
|
||||
// unset selected date
|
||||
@@ -77,7 +81,9 @@ export const useTroubleshooterStore = create<TroubleshooterStore>((set, get) =>
|
||||
event: null,
|
||||
setEvent: (event: EventType) => {
|
||||
set({ event });
|
||||
updateQueryParam("eventTypeId", event.id.toString());
|
||||
if (!get().isPlatform) {
|
||||
updateQueryParam("eventTypeId", event.id.toString());
|
||||
}
|
||||
},
|
||||
month: getQueryParam("month") || getQueryParam("date") || dayjs().format("YYYY-MM"),
|
||||
setMonth: (month: string | null) => {
|
||||
@@ -85,7 +91,10 @@ export const useTroubleshooterStore = create<TroubleshooterStore>((set, get) =>
|
||||
updateQueryParam("month", month ?? "");
|
||||
get().setSelectedDate(null);
|
||||
},
|
||||
initialize: ({ month }: StoreInitializeType) => {
|
||||
initialize: ({ month, isPlatform }: StoreInitializeType) => {
|
||||
if (isPlatform) {
|
||||
set({ isPlatform: true });
|
||||
}
|
||||
if (month) {
|
||||
set({ month });
|
||||
updateQueryParam("month", month);
|
||||
@@ -101,11 +110,12 @@ export const useTroubleshooterStore = create<TroubleshooterStore>((set, get) =>
|
||||
},
|
||||
}));
|
||||
|
||||
export const useInitalizeTroubleshooterStore = ({ month }: StoreInitializeType) => {
|
||||
export const useInitalizeTroubleshooterStore = ({ month, isPlatform }: StoreInitializeType) => {
|
||||
const initializeStore = useTroubleshooterStore((state) => state.initialize);
|
||||
useEffect(() => {
|
||||
initializeStore({
|
||||
month,
|
||||
isPlatform,
|
||||
});
|
||||
}, [initializeStore, month]);
|
||||
}, [initializeStore, month, isPlatform]);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
import http from "../lib/http";
|
||||
import { useAtomsContext } from "./useAtomsContext";
|
||||
|
||||
type EventTypeWithTeam = {
|
||||
id: number;
|
||||
team: { id: number; name: string } | null;
|
||||
title: string;
|
||||
slug: string;
|
||||
length: number;
|
||||
username: string | null;
|
||||
};
|
||||
|
||||
type ListWithTeamData = EventTypeWithTeam[];
|
||||
|
||||
export const QUERY_KEY = "get-event-types-list-with-team";
|
||||
export const useEventTypesList = (props: { enabled?: boolean }) => {
|
||||
const { isInit } = useAtomsContext();
|
||||
|
||||
const eventTypes = useQuery({
|
||||
queryKey: [QUERY_KEY],
|
||||
queryFn: () => {
|
||||
return http.get<ApiResponse<ListWithTeamData>>("/atoms/event-types/list-with-team").then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<ListWithTeamData>)?.data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: props?.enabled !== undefined ? props.enabled && isInit : isInit,
|
||||
staleTime: 5000,
|
||||
});
|
||||
|
||||
return eventTypes;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
import http from "../lib/http";
|
||||
import { useAtomsContext } from "./useAtomsContext";
|
||||
|
||||
type ScheduleByEventSlugData = {
|
||||
id: number;
|
||||
name: string;
|
||||
availability: unknown[][];
|
||||
dateOverrides: unknown[];
|
||||
timeZone: string;
|
||||
workingHours: unknown[];
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
export const QUERY_KEY = "get-schedule-by-event-slug";
|
||||
export const useScheduleByEventSlug = (props: { eventSlug?: string; enabled?: boolean }) => {
|
||||
const { isInit } = useAtomsContext();
|
||||
|
||||
const schedule = useQuery({
|
||||
queryKey: [QUERY_KEY, props.eventSlug],
|
||||
queryFn: () => {
|
||||
return http
|
||||
.get<ApiResponse<ScheduleByEventSlugData>>(`/atoms/schedules/event-type/${props.eventSlug}`)
|
||||
.then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<ScheduleByEventSlugData>)?.data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: !!props.eventSlug && (props?.enabled !== undefined ? props.enabled && isInit : isInit),
|
||||
staleTime: 5000,
|
||||
});
|
||||
|
||||
return schedule;
|
||||
};
|
||||
@@ -1,55 +1,48 @@
|
||||
export { CalProvider, CalOAuthProvider } from "./cal-provider";
|
||||
export { GcalConnect } from "./connect/google/GcalConnect";
|
||||
export { AvailabilitySettingsPlatformWrapper as AvailabilitySettings } from "./availability";
|
||||
export type { AvailabilitySettingsPlatformWrapperProps as AvailabilitySettingsProps } from "./availability/wrappers/AvailabilitySettingsPlatformWrapper";
|
||||
export type { AvailabilitySettingsScheduleType } from "./availability/AvailabilitySettings";
|
||||
export { BookerPlatformWrapper as Booker } from "./booker/BookerPlatformWrapper";
|
||||
export { useIsPlatform } from "./hooks/useIsPlatform";
|
||||
export { useAtomsContext } from "./hooks/useAtomsContext";
|
||||
export { useConnectedCalendars } from "./hooks/useConnectedCalendars";
|
||||
export { useEventTypes } from "./hooks/event-types/public/useEventTypes";
|
||||
export { useTeamEventTypes } from "./hooks/event-types/public/useTeamEventTypes";
|
||||
export { useEventType as useEvent } from "./hooks/event-types/public/useEventType";
|
||||
export { useEventTypeById } from "./hooks/event-types/private/useEventTypeById";
|
||||
export { useCancelBooking } from "./hooks/bookings/useCancelBooking";
|
||||
export { useBooking } from "./hooks/bookings/useBooking";
|
||||
export { useBookings } from "./hooks/bookings/useBookings";
|
||||
export { useMe } from "./hooks/useMe";
|
||||
export { OutlookConnect } from "./connect/outlook/OutlookConnect";
|
||||
export * as Connect from "./connect";
|
||||
export { BookerEmbed } from "./booker-embed";
|
||||
export { Router } from "./router";
|
||||
|
||||
export { useDeleteCalendarCredentials } from "./hooks/calendars/useDeleteCalendarCredentials";
|
||||
export { useAddSelectedCalendar } from "./hooks/calendars/useAddSelectedCalendar";
|
||||
export { useRemoveSelectedCalendar } from "./hooks/calendars/useRemoveSelectedCalendar";
|
||||
export { useTeams } from "./hooks/teams/useTeams";
|
||||
export { SelectedCalendarsSettingsPlatformWrapper as SelectedCalendarsSettings } from "./selected-calendars/index";
|
||||
export { DestinationCalendarSettingsPlatformWrapper as DestinationCalendarSettings } from "./destination-calendar/index";
|
||||
export { CalendarSettingsPlatformWrapper as CalendarSettings } from "./calendar-settings/index";
|
||||
export type { UpdateScheduleInput_2024_06_11 as UpdateScheduleBody } from "@calcom/platform-types";
|
||||
export { AvailabilitySettingsPlatformWrapper as AvailabilitySettings } from "./availability";
|
||||
export type { AvailabilitySettingsScheduleType } from "./availability/AvailabilitySettings";
|
||||
export type { AvailabilitySettingsFormRef } from "./availability/types";
|
||||
export type { AvailabilitySettingsPlatformWrapperProps as AvailabilitySettingsProps } from "./availability/wrappers/AvailabilitySettingsPlatformWrapper";
|
||||
export { BookerPlatformWrapper as Booker } from "./booker/BookerPlatformWrapper";
|
||||
export { BookerEmbed } from "./booker-embed";
|
||||
export { CalOAuthProvider, CalProvider } from "./cal-provider";
|
||||
export { CalendarSettingsPlatformWrapper as CalendarSettings } from "./calendar-settings/index";
|
||||
export { CalendarViewPlatformWrapper as CalendarView } from "./calendar-view/index";
|
||||
export * as Connect from "./connect";
|
||||
export type { ConferencingAppsCustomClassNames } from "./connect/conferencing-apps/ConferencingAppsViewPlatformWrapper";
|
||||
export { ConferencingAppsViewPlatformWrapper as ConferencingAppsSettings } from "./connect/conferencing-apps/ConferencingAppsViewPlatformWrapper";
|
||||
export { GcalConnect } from "./connect/google/GcalConnect";
|
||||
export { OutlookConnect } from "./connect/outlook/OutlookConnect";
|
||||
export { StripeConnect } from "./connect/stripe/StripeConnect";
|
||||
export { CreateScheduleForm } from "./create-schedule/CreateScheduleForm";
|
||||
export { CreateSchedulePlatformWrapper as CreateSchedule } from "./create-schedule/index";
|
||||
export { DestinationCalendarSettingsPlatformWrapper as DestinationCalendarSettings } from "./destination-calendar/index";
|
||||
export { ListEventTypesPlatformWrapper as ListEventTypes } from "./event-types/index";
|
||||
export { PaymentForm } from "./event-types/payments/PaymentForm";
|
||||
export { CreateEventTypePlatformWrapper as CreateEventType } from "./event-types/wrappers/CreateEventTypePlatformWrapper";
|
||||
export { EventTypePlatformWrapper as EventTypeSettings } from "./event-types/wrappers/EventTypePlatformWrapper";
|
||||
export type { EventSettingsFromRef } from "./event-types/wrappers/types";
|
||||
export type { AvailabilitySettingsFormRef } from "./availability/types";
|
||||
export { ConferencingAppsViewPlatformWrapper as ConferencingAppsSettings } from "./connect/conferencing-apps/ConferencingAppsViewPlatformWrapper";
|
||||
export type { ConferencingAppsCustomClassNames } from "./connect/conferencing-apps/ConferencingAppsViewPlatformWrapper";
|
||||
export { StripeConnect } from "./connect/stripe/StripeConnect";
|
||||
export { CreateEventTypePlatformWrapper as CreateEventType } from "./event-types/wrappers/CreateEventTypePlatformWrapper";
|
||||
export { PaymentForm } from "./event-types/payments/PaymentForm";
|
||||
|
||||
export { useBooking } from "./hooks/bookings/useBooking";
|
||||
export { useBookings } from "./hooks/bookings/useBookings";
|
||||
export { useCancelBooking } from "./hooks/bookings/useCancelBooking";
|
||||
export { useAddSelectedCalendar } from "./hooks/calendars/useAddSelectedCalendar";
|
||||
export { useDeleteCalendarCredentials } from "./hooks/calendars/useDeleteCalendarCredentials";
|
||||
export { useRemoveSelectedCalendar } from "./hooks/calendars/useRemoveSelectedCalendar";
|
||||
export { useCreateEventType } from "./hooks/event-types/private/useCreateEventType";
|
||||
export { useCreateTeamEventType } from "./hooks/event-types/private/useCreateTeamEventType";
|
||||
|
||||
export { useEventTypeById } from "./hooks/event-types/private/useEventTypeById";
|
||||
export { useEventType as useEvent } from "./hooks/event-types/public/useEventType";
|
||||
export { useEventTypes } from "./hooks/event-types/public/useEventTypes";
|
||||
export { useTeamEventTypes } from "./hooks/event-types/public/useTeamEventTypes";
|
||||
export { useOrganizationBookings } from "./hooks/organizations/bookings/useOrganizationBookings";
|
||||
export { useOrganizationUserBookings } from "./hooks/organizations/bookings/useOrganizationUserBookings";
|
||||
|
||||
export { CalendarViewPlatformWrapper as CalendarView } from "./calendar-view/index";
|
||||
|
||||
export { CreateSchedulePlatformWrapper as CreateSchedule } from "./create-schedule/index";
|
||||
export { CreateScheduleForm } from "./create-schedule/CreateScheduleForm";
|
||||
|
||||
export { ListSchedulesPlatformWrapper as ListSchedules } from "./list-schedules/index";
|
||||
|
||||
export { ListEventTypesPlatformWrapper as ListEventTypes } from "./event-types/index";
|
||||
|
||||
export { useTeams } from "./hooks/teams/useTeams";
|
||||
export { useAtomsContext } from "./hooks/useAtomsContext";
|
||||
export { useAvailableSlots } from "./hooks/useAvailableSlots";
|
||||
export { useConnectedCalendars } from "./hooks/useConnectedCalendars";
|
||||
export { useIsPlatform } from "./hooks/useIsPlatform";
|
||||
export { useMe } from "./hooks/useMe";
|
||||
export { ListSchedulesPlatformWrapper as ListSchedules } from "./list-schedules/index";
|
||||
export { Router } from "./router";
|
||||
export { SelectedCalendarsSettingsPlatformWrapper as SelectedCalendarsSettings } from "./selected-calendars/index";
|
||||
export { TroubleshooterPlatformWrapper as TroubleShooter } from "./troubleshooter/index";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { TroubleshooterPlatformWrapper } from "./wrappers/TroubleshooterPlatformWrapper";
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTroubleshooterStore } from "@calcom/features/troubleshooter/store";
|
||||
|
||||
import { CalendarViewPlatformWrapper } from "../../calendar-view/wrappers/CalendarViewPlatformWrapper";
|
||||
|
||||
export const LargeCalendar = (): JSX.Element | null => {
|
||||
const event = useTroubleshooterStore((state) => state.event);
|
||||
|
||||
if (!event?.slug) return null;
|
||||
|
||||
if (event.teamId) {
|
||||
return (
|
||||
<CalendarViewPlatformWrapper
|
||||
isEventTypeView={true}
|
||||
teamId={event.teamId}
|
||||
eventSlug={event.slug}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (event.username) {
|
||||
return (
|
||||
<CalendarViewPlatformWrapper
|
||||
isEventTypeView={true}
|
||||
username={event.username}
|
||||
eventSlug={event.slug}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { CalendarToggleContainerComponent } from "@calcom/features/troubleshooter/components/CalendarToggleContainerComponent";
|
||||
import { useConnectedCalendars } from "../../hooks/useConnectedCalendars";
|
||||
|
||||
interface CalendarToggleContainerProps {
|
||||
onManageCalendarsClick?: () => void;
|
||||
onInstallCalendarClick?: () => void;
|
||||
}
|
||||
|
||||
export function CalendarToggleContainer({
|
||||
onManageCalendarsClick,
|
||||
onInstallCalendarClick,
|
||||
}: CalendarToggleContainerProps): JSX.Element {
|
||||
const calendars = useConnectedCalendars({});
|
||||
|
||||
return (
|
||||
<CalendarToggleContainerComponent
|
||||
connectedCalendars={calendars.data?.connectedCalendars ?? []}
|
||||
isLoading={calendars.isLoading}
|
||||
{...(onManageCalendarsClick
|
||||
? { manageCalendarsAction: { onClick: onManageCalendarsClick } }
|
||||
: {})}
|
||||
{...(onInstallCalendarClick
|
||||
? { installCalendarAction: { onClick: onInstallCalendarClick } }
|
||||
: {})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useTroubleshooterStore } from "@calcom/features/troubleshooter/store";
|
||||
import { EventScheduleItemComponent } from "@calcom/features/troubleshooter/components/EventScheduleItemComponent";
|
||||
import { useScheduleByEventSlug } from "../../hooks/useScheduleByEventSlug";
|
||||
|
||||
export function EventScheduleItem(): JSX.Element {
|
||||
const selectedEventType = useTroubleshooterStore((state) => state.event);
|
||||
|
||||
const { data: schedule } = useScheduleByEventSlug({
|
||||
eventSlug: selectedEventType?.slug,
|
||||
});
|
||||
|
||||
return <EventScheduleItemComponent schedule={schedule ?? null} />;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { EventTypeSelectComponent } from "@calcom/features/troubleshooter/components/EventTypeSelectComponent";
|
||||
import { useEventTypesList } from "../../hooks/useEventTypesList";
|
||||
|
||||
export function EventTypeSelect(): JSX.Element {
|
||||
const { data: eventTypes, isPending } = useEventTypesList({});
|
||||
|
||||
return (
|
||||
<EventTypeSelectComponent
|
||||
eventTypes={eventTypes ?? []}
|
||||
isPending={isPending}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Skeleton } from "@calcom/ui/components/skeleton";
|
||||
import { CalendarToggleContainer } from "./CalendarToggleContainer";
|
||||
import { EventScheduleItem } from "./EventScheduleItem";
|
||||
import { EventTypeSelect } from "./EventTypeSelect";
|
||||
|
||||
const SidebarHeading = ({ name }: { name: string }): JSX.Element => {
|
||||
return (
|
||||
<Skeleton
|
||||
title={name}
|
||||
as="p"
|
||||
className="max-w-36 min-h-4 truncate font-semibold"
|
||||
loadingClassName="ms-3"
|
||||
>
|
||||
{name}
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
interface TroubleshooterSidebarProps {
|
||||
onManageCalendarsClick?: () => void;
|
||||
onInstallCalendarClick?: () => void;
|
||||
}
|
||||
|
||||
export const TroubleshooterSidebar = ({
|
||||
onManageCalendarsClick,
|
||||
onInstallCalendarClick,
|
||||
}: TroubleshooterSidebarProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="relative z-10 hidden h-screen w-full flex-col gap-6 overflow-y-auto py-6 pl-4 pr-6 sm:flex md:pl-0">
|
||||
<SidebarHeading name={t("troubleshooter")} />
|
||||
<EventTypeSelect />
|
||||
<EventScheduleItem />
|
||||
<CalendarToggleContainer
|
||||
onManageCalendarsClick={onManageCalendarsClick}
|
||||
onInstallCalendarClick={onInstallCalendarClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useInitalizeTroubleshooterStore } from "@calcom/features/troubleshooter/store";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
|
||||
import { TroubleshooterSidebar } from "../sidebar/TroubleshooterSidebar";
|
||||
import { LargeCalendar } from "../large-calendar/LargeCalendar";
|
||||
import { BookerStoreProvider } from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
|
||||
interface TroubleshooterComponentProps {
|
||||
month?: string | null;
|
||||
onManageCalendarsClick?: () => void;
|
||||
onInstallCalendarClick?: () => void;
|
||||
}
|
||||
|
||||
export const TroubleshooterComponent = ({
|
||||
month = null,
|
||||
onManageCalendarsClick,
|
||||
onInstallCalendarClick,
|
||||
}: TroubleshooterComponentProps): JSX.Element => {
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const isTablet = useMediaQuery("(max-width: 1024px)");
|
||||
|
||||
useInitalizeTroubleshooterStore({
|
||||
month: month,
|
||||
isPlatform: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<AtomsWrapper>
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
"text-default flex min-h-full w-full flex-col items-center overflow-clip",
|
||||
isMobile
|
||||
? "[--troubleshooter-meta-width:0px]"
|
||||
: "[--troubleshooter-meta-width:250px] lg:[--troubleshooter-meta-width:430px]"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
gridTemplateAreas: `"meta main"`,
|
||||
gridTemplateColumns: "var(--troubleshooter-meta-width) 1fr",
|
||||
gridTemplateRows: "auto",
|
||||
}}
|
||||
className={classNames(
|
||||
"bg-default dark:bg-cal-muted text-default min-h-full w-full sm:transition-[width] sm:duration-300"
|
||||
)}
|
||||
>
|
||||
<div className="sticky top-0 z-10 self-start ps-6 [grid-area:meta]">
|
||||
<TroubleshooterSidebar
|
||||
onManageCalendarsClick={onManageCalendarsClick}
|
||||
onInstallCalendarClick={onInstallCalendarClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-[-1px] border-subtle [grid-area:main]">
|
||||
<LargeCalendar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</AtomsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
interface TroubleshooterPlatformWrapperProps {
|
||||
month?: string | null;
|
||||
onManageCalendarsClick?: () => void;
|
||||
onInstallCalendarClick?: () => void;
|
||||
}
|
||||
|
||||
export const TroubleshooterPlatformWrapper = ({
|
||||
month,
|
||||
onManageCalendarsClick,
|
||||
onInstallCalendarClick,
|
||||
}: TroubleshooterPlatformWrapperProps): JSX.Element => {
|
||||
return (
|
||||
<BookerStoreProvider>
|
||||
<TroubleshooterComponent
|
||||
month={month}
|
||||
onManageCalendarsClick={onManageCalendarsClick}
|
||||
onInstallCalendarClick={onInstallCalendarClick}
|
||||
/>
|
||||
</BookerStoreProvider>
|
||||
);
|
||||
};
|
||||
@@ -25,6 +25,9 @@ export function Navbar({ username }: { username?: string }) {
|
||||
<li>
|
||||
<Link href="/availability">Availability</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/troubleshooter">Troubleshooter</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/event-types">EventTypes</Link>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { TroubleShooter } from "@calcom/atoms";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export default function Troubleshooter(props: {
|
||||
calUsername: string;
|
||||
calEmail: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<main className={`flex min-h-screen flex-col ${inter.className}`}>
|
||||
<Navbar username={props.calUsername} />
|
||||
<div data-testid="troubleshooter-atom">
|
||||
<TroubleShooter
|
||||
onManageCalendarsClick={() => {
|
||||
router.push("/calendars");
|
||||
}}
|
||||
onInstallCalendarClick={() => {
|
||||
router.push("/calendars");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,8 @@ export { getBulkUserEventTypes, getBulkTeamEventTypes } from "@calcom/app-store/
|
||||
export { createHandler as createEventType } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/create.handler";
|
||||
export { updateHandler as updateEventType } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/update.handler";
|
||||
|
||||
export { listWithTeamHandler } from "@calcom/trpc/server/routers/viewer/eventTypes/listWithTeam.handler";
|
||||
|
||||
export type { TUpdateInputSchema as TUpdateEventTypeInputSchema } from "@calcom/trpc/server/routers/viewer/eventTypes/heavy/update.schema";
|
||||
export type { EventTypesPublic } from "@calcom/features/eventtypes/lib/getEventTypesPublic";
|
||||
export { getEventTypesPublic } from "@calcom/features/eventtypes/lib/getEventTypesPublic";
|
||||
|
||||
@@ -23,3 +23,5 @@ export {
|
||||
duplicateHandler as duplicateScheduleHandler,
|
||||
type DuplicateScheduleHandlerReturn,
|
||||
} from "@calcom/trpc/server/routers/viewer/availability/schedule/duplicate.handler";
|
||||
|
||||
export { getScheduleByEventSlugHandler } from "@calcom/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler";
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { TGetInputSchema } from "./get.schema";
|
||||
|
||||
type GetOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
user: Pick<NonNullable<TrpcSessionUser>, "id" | "timeZone" | "defaultScheduleId">;
|
||||
};
|
||||
input: TGetInputSchema;
|
||||
};
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import type { TGetByEventSlugInputSchema } from "./getScheduleByEventTypeSlug.sc
|
||||
|
||||
type GetOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
user: Pick<NonNullable<TrpcSessionUser>, "id" | "timeZone" | "defaultScheduleId">;
|
||||
prisma: PrismaClient;
|
||||
};
|
||||
input: TGetByEventSlugInputSchema;
|
||||
|
||||
@@ -5,18 +5,19 @@ import type { TrpcSessionUser } from "../../../types";
|
||||
|
||||
type ListWithTeamOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
user: Pick<NonNullable<TrpcSessionUser>, "id">;
|
||||
};
|
||||
};
|
||||
|
||||
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", "public"."EventType"."length", "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", "u"."username" as "username"
|
||||
FROM "public"."EventType"
|
||||
LEFT JOIN "public"."Team" AS "j1" ON ("j1"."id") = ("public"."EventType"."teamId")
|
||||
WHERE "public"."EventType"."userId" = ${userId}
|
||||
LEFT JOIN "public"."users" AS "u" ON ("u"."id") = ("public"."EventType"."userId")
|
||||
WHERE "public"."EventType"."userId" = ${userId} AND "public"."EventType"."teamId" IS NULL
|
||||
UNION
|
||||
SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "public"."EventType"."length", "j1"."name" as "teamName"
|
||||
SELECT "public"."EventType"."id", "public"."EventType"."teamId", "public"."EventType"."title", "public"."EventType"."slug", "public"."EventType"."length", "j1"."name" as "teamName", NULL as "username"
|
||||
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"
|
||||
@@ -30,6 +31,7 @@ export const listWithTeamHandler = async ({ ctx }: ListWithTeamOptions) => {
|
||||
slug: string;
|
||||
length: number;
|
||||
teamName: string | null;
|
||||
username: string | null;
|
||||
}[]
|
||||
>(query);
|
||||
|
||||
@@ -39,5 +41,6 @@ export const listWithTeamHandler = async ({ ctx }: ListWithTeamOptions) => {
|
||||
title: row.title,
|
||||
slug: row.slug,
|
||||
length: row.length,
|
||||
username: row.teamId ? null : row.username,
|
||||
}));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user