Files
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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: [email protected] <[email protected]>

* 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

381 lines
14 KiB
TypeScript

"use client";
import dayjs from "@calcom/dayjs";
import { ColumnFilterType, ZDateRangeFilterValue } from "@calcom/features/data-table";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import ServerTrans from "@calcom/lib/components/ServerTrans";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { showToast } from "@calcom/ui/components/toast";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { ClockIcon } from "@coss/ui/icons";
import { keepPreviousData } from "@tanstack/react-query";
import {
createColumnHelper,
getCoreRowModel,
getFilteredRowModel,
useReactTable,
} from "@tanstack/react-table";
import { usePathname } from "next/navigation";
import { useEffect, useMemo, useRef, useState } from "react";
import {
DataTableFilters,
DataTableSegment,
DataTableToolbar,
DataTableWrapper,
} from "~/data-table/components";
import { DataTableProvider } from "~/data-table/DataTableProvider";
import { useDataTable } from "~/data-table/hooks/useDataTable";
import { useFilterValue } from "~/data-table/hooks/useFilterValue";
import { useSegments } from "~/data-table/hooks/useSegments";
import CreateNewOutOfOfficeEntryButton from "./CreateNewOutOfOfficeEntryButton";
import { OutOfOfficeTab, OutOfOfficeToggleGroup } from "./OutOfOfficeToggleGroup";
import type { BookingRedirectForm } from "./types";
interface OutOfOfficeEntry {
id: number;
uuid: string;
start: Date;
end: Date;
toUserId: number | null;
toUser: {
username: string;
name: string;
} | null;
reason: {
id: number;
emoji: string;
reason: string;
userId: number;
} | null;
notes: string | null;
showNotePublicly: boolean | null;
user: { id: number; avatarUrl: string; username: string; email: string; name: string } | null;
canEditAndDelete: boolean;
}
export default function OutOfOfficeEntriesList({
onOpenCreateDialog,
onOpenEditDialog,
}: {
onOpenCreateDialog: () => void;
onOpenEditDialog: (entry: BookingRedirectForm) => void;
}) {
const { t } = useLocale();
const pathname = usePathname();
if (!pathname) return null;
return (
<SettingsHeader
title={t("out_of_office")}
description={t("out_of_office_description")}
borderInShellHeader={true}
CTA={
<div className="flex gap-2">
<OutOfOfficeToggleGroup />
<CreateNewOutOfOfficeEntryButton data-testid="add_entry_ooo" onClick={onOpenCreateDialog} />
</div>
}>
<div className="border-subtle rounded-b-lg border border-t-0 px-4 py-6 sm:px-6">
<DataTableProvider tableIdentifier={pathname} useSegments={useSegments}>
<OutOfOfficeEntriesListContent
onOpenCreateDialog={onOpenCreateDialog}
onOpenEditDialog={onOpenEditDialog}
/>
</DataTableProvider>
</div>
</SettingsHeader>
);
}
function OutOfOfficeEntriesListContent({
onOpenCreateDialog,
onOpenEditDialog,
}: {
onOpenCreateDialog: () => void;
onOpenEditDialog: (entry: BookingRedirectForm) => void;
}) {
const { t } = useLocale();
const tableContainerRef = useRef<HTMLDivElement>(null);
const [deletedEntry, setDeletedEntry] = useState(0);
const { searchTerm } = useDataTable();
const searchParams = useCompatSearchParams();
const selectedTab = searchParams?.get("type") ?? OutOfOfficeTab.MINE;
const endDateRange = useFilterValue("dateRange", ZDateRangeFilterValue)?.data;
const { data, isPending, fetchNextPage, isFetching, refetch, hasNextPage } =
trpc.viewer.ooo.outOfOfficeEntriesList.useInfiniteQuery(
{
limit: 10,
fetchTeamMembersEntries: false,
searchTerm,
endDateFilterStartRange: endDateRange?.startDate ?? undefined,
endDateFilterEndRange: endDateRange?.endDate ?? undefined,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
placeholderData: keepPreviousData,
}
);
useEffect(() => {
refetch();
}, [deletedEntry, selectedTab, refetch]);
const totalRowCount = data?.pages?.[0]?.meta?.totalRowCount ?? 0;
const flatData = useMemo(
() =>
isPending || isFetching ? new Array(5).fill(null) : (data?.pages?.flatMap((page) => page.rows) ?? []),
[data, isPending, isFetching]
) as OutOfOfficeEntry[];
const deleteOutOfOfficeEntryMutation = trpc.viewer.ooo.outOfOfficeEntryDelete.useMutation({
onSuccess: () => {
showToast(t("success_deleted_entry_out_of_office"), "success");
setDeletedEntry((previousValue) => previousValue + 1);
},
onError: () => {
showToast(`An error occurred`, "error");
},
});
const memoColumns = useMemo(() => {
const columnHelper = createColumnHelper<OutOfOfficeEntry>();
return [
columnHelper.accessor((row) => row, {
id: "dateRange",
header: t("date_range"),
enableColumnFilter: true,
enableSorting: false,
cell: () => null,
filterFn: () => true,
meta: {
filter: {
type: ColumnFilterType.DATE_RANGE,
dateRangeOptions: {
range: "past",
},
},
},
}),
columnHelper.display({
id: "outOfOffice",
header: `${t("out_of_office")} (${totalRowCount})`,
size: 570,
cell: ({ row }) => {
const item = row.original;
return (
<>
{row.original && !isPending && !isFetching ? (
<div
className="flex flex-row items-center gap-3 py-2"
data-testid={`table-redirect-${item.toUser?.username || "n-a"}`}>
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-subtle">
{item?.reason?.emoji || "🏝️"}
</div>
<div className="flex flex-col">
<p className="font-bold">
{dayjs.utc(item.start).format("ll")} - {dayjs.utc(item.end).format("ll")}
</p>
<p>
{item.toUser?.username ? (
<ServerTrans
t={t}
i18nKey="ooo_forwarding_to"
values={{
username: item.toUser?.username,
}}
components={[<span key="ooo-username" className="text-subtle font-bold" />]}
/>
) : (
<>{t("ooo_not_forwarding")}</>
)}
</p>
{item.notes && (
<p>
<span className="text-subtle">{t("notes")}: </span>
<span data-testid={`ooo-entry-note-${item.toUser?.username || "n-a"}`}>
{item.notes}
</span>
</p>
)}
</div>
</div>
) : (
<SkeletonText className="h-8 w-full" />
)}
</>
);
},
}),
columnHelper.display({
id: "actions",
size: 90,
cell: ({ row }) => {
const item = row.original;
return (
<>
{row.original && !isPending && !isFetching ? (
<div className="flex flex-row items-center justify-end gap-x-2" data-testid="ooo-actions">
<Tooltip content={t("edit")}>
<Button
className="self-center rounded-lg border"
type="button"
color="secondary"
variant="icon"
data-testid={`ooo-edit-${item.toUser?.username || "n-a"}`}
StartIcon="pencil"
onClick={() => {
const startDateOffset = -1 * item.start.getTimezoneOffset();
const endDateOffset = -1 * item.end.getTimezoneOffset();
const outOfOfficeEntryData: BookingRedirectForm = {
uuid: item.uuid,
dateRange: {
startDate: dayjs(item.start).subtract(startDateOffset, "minute").toDate(),
endDate: dayjs(item.end).subtract(endDateOffset, "minute").startOf("d").toDate(),
},
startDateOffset,
endDateOffset,
toTeamUserId: item.toUserId,
reasonId: item.reason?.id ?? 1,
notes: item.notes ?? undefined,
showNotePublicly: item.showNotePublicly ?? false,
forUserId: item.user?.id || null,
forUserName:
item.user?.name ||
(item.user?.email &&
(() => {
const emailName = item.user?.email.split("@")[0];
return emailName.charAt(0).toUpperCase() + emailName.slice(1);
})()),
forUserAvatar: item.user?.avatarUrl,
toUserName: item.toUser?.name || item.toUser?.username,
};
onOpenEditDialog(outOfOfficeEntryData);
}}
disabled={isPending || isFetching || !item.canEditAndDelete}
/>
</Tooltip>
<Tooltip content={t("delete")}>
<Button
className="self-center rounded-lg border"
type="button"
color="destructive"
variant="icon"
disabled={
deleteOutOfOfficeEntryMutation.isPending ||
isPending ||
isFetching ||
!item.canEditAndDelete
}
StartIcon="trash-2"
data-testid={`ooo-delete-${item.toUser?.username || "n-a"}`}
onClick={() => {
deleteOutOfOfficeEntryMutation.mutate({
outOfOfficeUid: item.uuid,
});
}}
/>
</Tooltip>
</div>
) : (
<SkeletonText className="h-8 w-full" />
)}
</>
);
},
}),
];
}, [
selectedTab,
isPending,
isFetching,
onOpenEditDialog,
t,
deleteOutOfOfficeEntryMutation,
totalRowCount,
]);
const table = useReactTable({
data: flatData,
columns: memoColumns,
initialState: {
columnVisibility: {
dateRange: false,
},
},
enableHiding: false,
enableRowSelection: false,
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
});
return (
<>
<DataTableWrapper
testId="ooo-list-data-table"
rowClassName=""
table={table}
isPending={isPending}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
isFetching={isFetching}
totalRowCount={totalRowCount}
tableContainerRef={tableContainerRef}
paginationMode="infinite"
ToolbarLeft={
<>
<DataTableToolbar.SearchBar />
<DataTableFilters.FilterBar table={table} />
</>
}
ToolbarRight={
<>
<DataTableFilters.ClearFiltersButton />
<DataTableSegment.SaveButton />
<DataTableSegment.Select />
</>
}
EmptyView={
<EmptyScreen
className="mt-6"
headline={searchTerm ? t("no_result_found_for", { searchTerm }) : t("ooo_empty_title")}
description={t("ooo_empty_description")}
buttonRaw={<CreateNewOutOfOfficeEntryButton size="sm" onClick={onOpenCreateDialog} />}
customIcon={
<div className="mt-4 h-[102px]">
<div className="flex h-full flex-col items-center justify-center p-2 md:mt-0 md:p-0">
<div className="relative">
<div className="dark:bg-darkgray-50 absolute -left-3 -top-3 -z-20 h-[70px] w-[70px] -rotate-24 rounded-3xl border-2 border-[#e5e7eb] p-8 opacity-40 dark:opacity-80">
<div className="w-12" />
</div>
<div className="dark:bg-darkgray-50 absolute -top-3 left-3 -z-10 h-[70px] w-[70px] rotate-24 rounded-3xl border-2 border-[#e5e7eb] p-8 opacity-60 dark:opacity-90">
<div className="w-12" />
</div>
<div className="dark:bg-darkgray-50 text-inverted relative z-0 flex h-[70px] w-[70px] items-center justify-center rounded-3xl border-2 border-[#e5e7eb] bg-white">
<ClockIcon size={28} className="text-black" />
<div className="dark:bg-darkgray-50 absolute right-4 top-5 h-[12px] w-[12px] rotate-56 bg-white text-lg font-bold" />
<span className="absolute right-4 top-3 font-sans text-sm font-extrabold text-black">
z
</span>
</div>
</div>
</div>
</div>
}
/>
}
/>
</>
);
}