diff --git a/apps/api/v2/src/modules/atoms/controllers/atoms.event-types.controller.ts b/apps/api/v2/src/modules/atoms/controllers/atoms.event-types.controller.ts index e6fbd701d9..36c7dd8245 100644 --- a/apps/api/v2/src/modules/atoms/controllers/atoms.event-types.controller.ts +++ b/apps/api/v2/src/modules/atoms/controllers/atoms.event-types.controller.ts @@ -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> { + const eventTypes = await listWithTeamHandler({ ctx: { user } }); + return { + status: SUCCESS_STATUS, + data: eventTypes, + }; + } + @Get("/event-types/:eventSlug/public") async getPublicEventType( @Param("eventSlug") eventSlug: string, diff --git a/apps/api/v2/src/modules/atoms/controllers/atoms.schedules.controller.ts b/apps/api/v2/src/modules/atoms/controllers/atoms.schedules.controller.ts index 227a2decd6..dedeaad05c 100644 --- a/apps/api/v2/src/modules/atoms/controllers/atoms.schedules.controller.ts +++ b/apps/api/v2/src/modules/atoms/controllers/atoms.schedules.controller.ts @@ -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> { + const schedule = await this.schedulesService.getScheduleByEventSlug(user, eventSlug); + return { + status: SUCCESS_STATUS, + data: schedule, + }; + } + @Get("/schedules/all") @Version(VERSION_NEUTRAL) @UseGuards(ApiAuthGuard) diff --git a/apps/api/v2/src/modules/atoms/services/schedules-atom.service.ts b/apps/api/v2/src/modules/atoms/services/schedules-atom.service.ts index dc27f41930..5c3b4f129d 100644 --- a/apps/api/v2/src/modules/atoms/services/schedules-atom.service.ts +++ b/apps/api/v2/src/modules/atoms/services/schedules-atom.service.ts @@ -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, diff --git a/apps/web/modules/troubleshooter/components/AvailabilitySchedulesContainer.tsx b/apps/web/modules/troubleshooter/components/AvailabilitySchedulesContainer.tsx index 173e655838..4fa5fb1c66 100644 --- a/apps/web/modules/troubleshooter/components/AvailabilitySchedulesContainer.tsx +++ b/apps/web/modules/troubleshooter/components/AvailabilitySchedulesContainer.tsx @@ -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(); diff --git a/apps/web/modules/troubleshooter/components/CalendarToggleContainer.tsx b/apps/web/modules/troubleshooter/components/CalendarToggleContainer.tsx index 67fc3fd153..6c01659f4b 100644 --- a/apps/web/modules/troubleshooter/components/CalendarToggleContainer.tsx +++ b/apps/web/modules/troubleshooter/components/CalendarToggleContainer.tsx @@ -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 ( - -
- - } - suffixSlot={ -
- - {badgeText} - -
- }> -
- {props.calendars?.map((calendar) => { - return ; - })} -
- - ); -} - -function EmptyCalendarToggleItem() { - const { t } = useLocale(); - - return ( - -
- - } - suffixSlot={ -
- - {t("unavailable")} - -
- }> -
- -
- - ); -} 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 ( -
-

{t("calendars_were_checking_for_conflicts")}

- {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 ( - { - return { - active: item.isSelected, - name: item.name, - }; - })} - /> - ); - })} - - - ) : ( - - )} -
+ ); } diff --git a/apps/web/modules/troubleshooter/components/ConnectedAppsContainer.tsx b/apps/web/modules/troubleshooter/components/ConnectedAppsContainer.tsx index 00fe4898fc..01fd1c688f 100644 --- a/apps/web/modules/troubleshooter/components/ConnectedAppsContainer.tsx +++ b/apps/web/modules/troubleshooter/components/ConnectedAppsContainer.tsx @@ -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 ( diff --git a/apps/web/modules/troubleshooter/components/EventScheduleItem.tsx b/apps/web/modules/troubleshooter/components/EventScheduleItem.tsx index 66cfcd35b9..9509d6807b 100644 --- a/apps/web/modules/troubleshooter/components/EventScheduleItem.tsx +++ b/apps/web/modules/troubleshooter/components/EventScheduleItem.tsx @@ -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 ( -
- - } - title={schedule?.name ?? "Loading"} - suffixSlot={ - schedule && ( - - - {t("edit")} - - - ) - } - /> -
+ + + {t("edit")} + + + ) + } + /> ); } diff --git a/apps/web/modules/troubleshooter/components/EventTypeSelect.tsx b/apps/web/modules/troubleshooter/components/EventTypeSelect.tsx index f41a064ab7..2ded9b4b42 100644 --- a/apps/web/modules/troubleshooter/components/EventTypeSelect.tsx +++ b/apps/web/modules/troubleshooter/components/EventTypeSelect.tsx @@ -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 ( - 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, - }); - } - }} + ); } diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index e4a28093b6..24cabdf55c 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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", diff --git a/packages/features/troubleshooter/components/CalendarToggleContainerComponent.tsx b/packages/features/troubleshooter/components/CalendarToggleContainerComponent.tsx new file mode 100644 index 0000000000..532e9b5df1 --- /dev/null +++ b/packages/features/troubleshooter/components/CalendarToggleContainerComponent.tsx @@ -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 ( + +
+ + } + suffixSlot={ +
+ + {badgeText} + +
+ } + > +
+ {props.calendars?.map((calendar) => { + return ( + + ); + })} +
+ + ); +} + +function EmptyCalendarToggleItem({ + installCalendarAction, +}: { + installCalendarAction?: { href: string } | { onClick: () => void }; +}) { + const { t } = useLocale(); + + return ( + +
+ + } + suffixSlot={ +
+ + {t("unavailable")} + +
+ } + > + {installCalendarAction && ( +
+ +
+ )} + + ); +} + +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 ( +
+

+ {t("calendars_were_checking_for_conflicts")} +

+ {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 ( + { + return { + active: item.isSelected, + name: item.name, + }; + })} + /> + ); + })} + {manageCalendarsAction && ( + + )} + + ) : ( + + )} +
+ ); +} diff --git a/packages/features/troubleshooter/components/EventScheduleItemComponent.tsx b/packages/features/troubleshooter/components/EventScheduleItemComponent.tsx new file mode 100644 index 0000000000..6a60ed8728 --- /dev/null +++ b/packages/features/troubleshooter/components/EventScheduleItemComponent.tsx @@ -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 ( +
+ + } + title={schedule?.name ?? t("loading")} + suffixSlot={suffixSlot} + /> +
+ ); +} diff --git a/packages/features/troubleshooter/components/EventTypeSelectComponent.tsx b/packages/features/troubleshooter/components/EventTypeSelectComponent.tsx new file mode 100644 index 0000000000..b4e0484256 --- /dev/null +++ b/packages/features/troubleshooter/components/EventTypeSelectComponent.tsx @@ -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 ( + 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, + }); + } + }} + /> + ); +} diff --git a/apps/web/modules/troubleshooter/components/TroubleshooterListItemContainer.tsx b/packages/features/troubleshooter/components/TroubleshooterListItemContainer.tsx similarity index 100% rename from apps/web/modules/troubleshooter/components/TroubleshooterListItemContainer.tsx rename to packages/features/troubleshooter/components/TroubleshooterListItemContainer.tsx diff --git a/packages/features/troubleshooter/store.ts b/packages/features/troubleshooter/store.ts index 49e23ee4e2..910f95c5be 100644 --- a/packages/features/troubleshooter/store.ts +++ b/packages/features/troubleshooter/store.ts @@ -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((set, get) => ({ + isPlatform: false, selectedDate: getQueryParam("date") || null, setSelectedDate: (selectedDate: string | null) => { // unset selected date @@ -77,7 +81,9 @@ export const useTroubleshooterStore = create((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((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((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]); }; diff --git a/packages/platform/atoms/hooks/useEventTypesList.tsx b/packages/platform/atoms/hooks/useEventTypesList.tsx new file mode 100644 index 0000000000..8d2387c556 --- /dev/null +++ b/packages/platform/atoms/hooks/useEventTypesList.tsx @@ -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>("/atoms/event-types/list-with-team").then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return (res.data as ApiSuccessResponse)?.data; + } + throw new Error(res.data.error.message); + }); + }, + enabled: props?.enabled !== undefined ? props.enabled && isInit : isInit, + staleTime: 5000, + }); + + return eventTypes; +}; diff --git a/packages/platform/atoms/hooks/useScheduleByEventSlug.tsx b/packages/platform/atoms/hooks/useScheduleByEventSlug.tsx new file mode 100644 index 0000000000..c1d950f393 --- /dev/null +++ b/packages/platform/atoms/hooks/useScheduleByEventSlug.tsx @@ -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>(`/atoms/schedules/event-type/${props.eventSlug}`) + .then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return (res.data as ApiSuccessResponse)?.data; + } + throw new Error(res.data.error.message); + }); + }, + enabled: !!props.eventSlug && (props?.enabled !== undefined ? props.enabled && isInit : isInit), + staleTime: 5000, + }); + + return schedule; +}; diff --git a/packages/platform/atoms/index.ts b/packages/platform/atoms/index.ts index d1ac7ff4f9..77baf28fd6 100644 --- a/packages/platform/atoms/index.ts +++ b/packages/platform/atoms/index.ts @@ -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"; diff --git a/packages/platform/atoms/troubleshooter/index.ts b/packages/platform/atoms/troubleshooter/index.ts new file mode 100644 index 0000000000..d63af1c03c --- /dev/null +++ b/packages/platform/atoms/troubleshooter/index.ts @@ -0,0 +1 @@ +export { TroubleshooterPlatformWrapper } from "./wrappers/TroubleshooterPlatformWrapper"; diff --git a/packages/platform/atoms/troubleshooter/large-calendar/LargeCalendar.tsx b/packages/platform/atoms/troubleshooter/large-calendar/LargeCalendar.tsx new file mode 100644 index 0000000000..60ce7a1675 --- /dev/null +++ b/packages/platform/atoms/troubleshooter/large-calendar/LargeCalendar.tsx @@ -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 ( + + ); + } + + if (event.username) { + return ( + + ); + } + + return null; +}; diff --git a/packages/platform/atoms/troubleshooter/sidebar/CalendarToggleContainer.tsx b/packages/platform/atoms/troubleshooter/sidebar/CalendarToggleContainer.tsx new file mode 100644 index 0000000000..3bc298cb3f --- /dev/null +++ b/packages/platform/atoms/troubleshooter/sidebar/CalendarToggleContainer.tsx @@ -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 ( + + ); +} diff --git a/packages/platform/atoms/troubleshooter/sidebar/EventScheduleItem.tsx b/packages/platform/atoms/troubleshooter/sidebar/EventScheduleItem.tsx new file mode 100644 index 0000000000..b3dae59146 --- /dev/null +++ b/packages/platform/atoms/troubleshooter/sidebar/EventScheduleItem.tsx @@ -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 ; +} diff --git a/packages/platform/atoms/troubleshooter/sidebar/EventTypeSelect.tsx b/packages/platform/atoms/troubleshooter/sidebar/EventTypeSelect.tsx new file mode 100644 index 0000000000..a58a2a03a5 --- /dev/null +++ b/packages/platform/atoms/troubleshooter/sidebar/EventTypeSelect.tsx @@ -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 ( + + ); +} diff --git a/packages/platform/atoms/troubleshooter/sidebar/TroubleshooterSidebar.tsx b/packages/platform/atoms/troubleshooter/sidebar/TroubleshooterSidebar.tsx new file mode 100644 index 0000000000..6263e8293a --- /dev/null +++ b/packages/platform/atoms/troubleshooter/sidebar/TroubleshooterSidebar.tsx @@ -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 ( + + {name} + + ); +}; + +interface TroubleshooterSidebarProps { + onManageCalendarsClick?: () => void; + onInstallCalendarClick?: () => void; +} + +export const TroubleshooterSidebar = ({ + onManageCalendarsClick, + onInstallCalendarClick, +}: TroubleshooterSidebarProps): JSX.Element => { + const { t } = useLocale(); + + return ( +
+ + + + +
+ ); +}; diff --git a/packages/platform/atoms/troubleshooter/wrappers/TroubleshooterPlatformWrapper.tsx b/packages/platform/atoms/troubleshooter/wrappers/TroubleshooterPlatformWrapper.tsx new file mode 100644 index 0000000000..dd3ac0c43d --- /dev/null +++ b/packages/platform/atoms/troubleshooter/wrappers/TroubleshooterPlatformWrapper.tsx @@ -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 ( + + <> +
+
+
+ +
+ +
+ +
+
+
+ +
+ ); +}; + +interface TroubleshooterPlatformWrapperProps { + month?: string | null; + onManageCalendarsClick?: () => void; + onInstallCalendarClick?: () => void; +} + +export const TroubleshooterPlatformWrapper = ({ + month, + onManageCalendarsClick, + onInstallCalendarClick, +}: TroubleshooterPlatformWrapperProps): JSX.Element => { + return ( + + + + ); +}; diff --git a/packages/platform/examples/base/src/components/Navbar/index.tsx b/packages/platform/examples/base/src/components/Navbar/index.tsx index ae31fe13e2..bf7d38552f 100644 --- a/packages/platform/examples/base/src/components/Navbar/index.tsx +++ b/packages/platform/examples/base/src/components/Navbar/index.tsx @@ -25,6 +25,9 @@ export function Navbar({ username }: { username?: string }) {
  • Availability
  • +
  • + Troubleshooter +
  • EventTypes
  • diff --git a/packages/platform/examples/base/src/pages/troubleshooter.tsx b/packages/platform/examples/base/src/pages/troubleshooter.tsx new file mode 100644 index 0000000000..0c61975533 --- /dev/null +++ b/packages/platform/examples/base/src/pages/troubleshooter.tsx @@ -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 ( +
    + +
    + { + router.push("/calendars"); + }} + onInstallCalendarClick={() => { + router.push("/calendars"); + }} + /> +
    +
    + ); +} diff --git a/packages/platform/libraries/event-types.ts b/packages/platform/libraries/event-types.ts index a67a28c50f..34a791c291 100644 --- a/packages/platform/libraries/event-types.ts +++ b/packages/platform/libraries/event-types.ts @@ -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"; diff --git a/packages/platform/libraries/schedules.ts b/packages/platform/libraries/schedules.ts index ec641b06e6..5506a8d721 100644 --- a/packages/platform/libraries/schedules.ts +++ b/packages/platform/libraries/schedules.ts @@ -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"; diff --git a/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts b/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts index c5e031ab5a..9a03d91f1a 100644 --- a/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/schedule/get.handler.ts @@ -6,7 +6,7 @@ import type { TGetInputSchema } from "./get.schema"; type GetOptions = { ctx: { - user: NonNullable; + user: Pick, "id" | "timeZone" | "defaultScheduleId">; }; input: TGetInputSchema; }; diff --git a/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts b/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts index 2d10f84e51..a279cbe48b 100644 --- a/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts +++ b/packages/trpc/server/routers/viewer/availability/schedule/getScheduleByEventTypeSlug.handler.ts @@ -6,7 +6,7 @@ import type { TGetByEventSlugInputSchema } from "./getScheduleByEventTypeSlug.sc type GetOptions = { ctx: { - user: NonNullable; + user: Pick, "id" | "timeZone" | "defaultScheduleId">; prisma: PrismaClient; }; input: TGetByEventSlugInputSchema; diff --git a/packages/trpc/server/routers/viewer/eventTypes/listWithTeam.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/listWithTeam.handler.ts index 907c6b23a7..f7a1c8d61e 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/listWithTeam.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/listWithTeam.handler.ts @@ -5,18 +5,19 @@ import type { TrpcSessionUser } from "../../../types"; type ListWithTeamOptions = { ctx: { - user: NonNullable; + user: Pick, "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, })); };