Files
Benny JooandDevin AI ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

196 lines
6.9 KiB
TypeScript

import { updateEmbedBookerState } from "@calcom/embed-core/src/embed-iframe";
import { sdkActionManager } from "@calcom/embed-core/src/sdk-event";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { isBookingDryRun } from "@calcom/features/bookings/Booker/utils/isBookingDryRun";
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
import { useTimesForSchedule } from "@calcom/features/schedules/hooks/useTimesForSchedule";
import { getRoutedTeamMemberIdsFromSearchParams } from "@calcom/lib/bookings/getRoutedTeamMemberIdsFromSearchParams";
import { PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS } from "@calcom/lib/constants";
import { trpc } from "@calcom/trpc/react";
import { useSearchParams } from "next/navigation";
import { useApiV2AvailableSlots } from "./useApiV2AvailableSlots";
export type UseScheduleWithCacheArgs = {
username?: string | null;
eventSlug?: string | null;
eventId?: number | null;
month?: string | null;
timezone?: string | null;
selectedDate?: string | null;
duration?: number | null;
dayCount?: number | null;
rescheduleUid?: string | null;
isTeamEvent?: boolean;
orgSlug?: string;
teamMemberEmail?: string | null;
useApiV2?: boolean;
enabled?: boolean;
/***
* Required when prefetching is needed
*/
bookerLayout?: {
layout: string;
extraDays: number;
columnViewExtraDays: { current: number };
};
};
const getAvailabilityLoadedEventPayload = ({
eventId,
eventSlug,
}: {
eventId: number;
eventSlug: string;
}) => {
return {
eventId,
eventSlug,
};
};
export const useSchedule = ({
month,
timezone,
username,
eventSlug,
eventId,
selectedDate,
duration,
dayCount,
rescheduleUid,
isTeamEvent,
orgSlug,
teamMemberEmail,
useApiV2 = false,
enabled: enabledProp = true,
bookerLayout,
}: UseScheduleWithCacheArgs) => {
const bookerState = useBookerStore((state) => state.state);
const [startTime, endTime] = useTimesForSchedule({
month,
dayCount,
selectedDate,
bookerLayout,
});
const searchParams = useSearchParams();
const routedTeamMemberIds = searchParams
? getRoutedTeamMemberIdsFromSearchParams(new URLSearchParams(searchParams.toString()))
: null;
const skipContactOwner = searchParams ? searchParams.get("cal.skipContactOwner") === "true" : false;
const utils = trpc.useUtils();
const queuedFormResponseId = searchParams?.get("cal.queuedFormResponseId");
const email = searchParams?.get("email");
// We allow skipping the schedule fetch as a requirement for prerendering in iframe through embed as when the pre-rendered iframe is connected, then we would fetch the availability, which would be upto-date
// Also, a reuse through Headless Router could completely change the availability as different team members are selected and thus it is unnecessary to fetch the schedule
const skipGetSchedule = searchParams?.get("cal.skipSlotsFetch") === "true";
const embedConnectVersion = searchParams?.get("cal.embed.connectVersion") || "0";
const input = {
isTeamEvent,
usernameList: getUsernameList(username ?? ""),
// Prioritize slug over id, since slug is the first value we get available.
// If we have a slug, we don't need to fetch the id.
// TODO: are queries using eventTypeId faster? Even tho we lost time fetching the id with the slug.
...(eventSlug ? { eventTypeSlug: eventSlug } : { eventTypeId: eventId ?? 0 }),
// @TODO: Old code fetched 2 days ago if we were fetching the current month.
// Do we want / need to keep that behavior?
startTime,
// if `prefetchNextMonth` is true, two months are fetched at once.
endTime,
// We use a placeholder value that is there to keep TS happy, but still invalid to tell us that it shouldn't actually be passed in request(and wouldn't because enabled is false if timezone is nullish)
// TODO: Better approach here is to use `skipToken` from react-query which requires an upgrade of react-query
timeZone: timezone ?? "PLACEHOLDER_TIMEZONE",
duration: duration ? `${duration}` : undefined,
rescheduleUid,
orgSlug,
teamMemberEmail,
routedTeamMemberIds,
skipContactOwner,
...(queuedFormResponseId ? { queuedFormResponseId } : {}),
email,
// Ensures that connectVersion causes a refresh of the data
...(embedConnectVersion ? { embedConnectVersion } : {}),
_isDryRun: searchParams ? isBookingDryRun(searchParams) : false,
};
const options = {
trpc: {
context: {
skipBatch: true,
},
},
// It allows people who might not have the tab in focus earlier, to get latest available slots
// It might not work correctly in iframes, so we have refetchInterval to take care of that.
// But where it works, it should give user latest availability even if they come back to tab before refetchInterval.
refetchOnWindowFocus: true,
// It allows long sitting users to get latest available slots
refetchInterval: PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS * 1000,
enabled:
!skipGetSchedule &&
Boolean(username) &&
Boolean(month) &&
Boolean(timezone) &&
// Should only wait for one or the other, not both.
(Boolean(eventSlug) || Boolean(eventId) || eventId === 0) &&
enabledProp,
};
const isCallingApiV2Slots = useApiV2 && Boolean(isTeamEvent) && options.enabled;
// API V2 query for team events
const teamScheduleV2 = useApiV2AvailableSlots({
...input,
enabled: isCallingApiV2Slots,
duration: input.duration ? Number(input.duration) : undefined,
routedTeamMemberIds: input.routedTeamMemberIds ?? undefined,
teamMemberEmail: input.teamMemberEmail ?? undefined,
eventTypeId: eventId ?? undefined,
});
const schedule = trpc.viewer.slots.getSchedule.useQuery(input, {
...options,
// Only enable if we're not using API V2
enabled: options.enabled && !isCallingApiV2Slots,
});
if (isCallingApiV2Slots && !teamScheduleV2.failureReason) {
updateEmbedBookerState({
bookerState,
slotsQuery: teamScheduleV2,
});
if (teamScheduleV2.isSuccess && eventId && eventSlug) {
sdkActionManager?.fire("availabilityLoaded", getAvailabilityLoadedEventPayload({ eventId, eventSlug }));
}
return {
...teamScheduleV2,
/**
* Invalidates the request and resends it regardless of any other configuration including staleTime
*/
invalidate: () => {
return teamScheduleV2.refetch();
},
};
}
updateEmbedBookerState({
bookerState,
slotsQuery: schedule,
});
if (schedule.isSuccess && eventId && eventSlug) {
sdkActionManager?.fire("availabilityLoaded", getAvailabilityLoadedEventPayload({ eventId, eventSlug }));
}
return {
...schedule,
/**
* Invalidates the request and resends it regardless of any other configuration including staleTime
*/
invalidate: () => {
return utils.viewer.slots.getSchedule.invalidate(input);
},
};
};