Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"analyze:browser": "BUNDLE_ANALYZE=browser next build",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "yarn copy-static && next dev --turbopack",
"dev:cron": "ts-node cron-tester.ts",
"dev:cron": "npx tsx cron-tester.ts",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add tsx as a devDependency or clarify workspace availability.

The script switches from ts-node to npx tsx, but tsx is not listed in this package's devDependencies. Using npx tsx will attempt to fetch the package from npm if it's not available locally, which may fail in offline environments or CI systems with restricted network access.

Verify the following:

  1. Is tsx available as a workspace dependency (e.g., in a root package.json or in packages matching the monorepo pattern)?
  2. Should tsx be added to devDependencies alongside or instead of ts-node?
  3. Is ts-node (line 196) still used elsewhere in this package or should it be removed?

Run this script to verify:


🏁 Script executed:

#!/bin/bash
# Check if tsx is listed in any workspace package.json files
fd --type f "package.json" --exec grep -l '"tsx"' {} \;

# Check all usages of ts-node and tsx in package.json files
echo "=== ts-node usages ===" && fd --type f "package.json" --exec grep -n "ts-node" {} + | grep -v node_modules
echo "=== tsx usages ===" && fd --type f "package.json" --exec grep -n "tsx" {} + | grep -v node_modules

# Verify cron-tester.ts exists
fd --type f "cron-tester.ts"

Length of output: 14685


Add tsx to devDependencies or replace npx tsx with ts-node.

The script change switches from ts-node to npx tsx, but tsx is not listed as a devDependency in apps/web/package.json or anywhere in the monorepo. Using npx tsx will attempt to fetch the package from npm on each execution, creating non-deterministic behavior and failing in offline or restricted network environments. Additionally, ts-node remains in devDependencies but is no longer used in this file (it is still used in lines 24–25 for other scripts).

Either:

  1. Add tsx to devDependencies alongside ts-node, or
  2. Use ts-node consistently for this script to maintain dependency clarity in the workspace
🤖 Prompt for AI Agents
In apps/web/package.json at line 11, the "dev:cron" script uses "npx tsx" but
the tsx package is not declared in devDependencies, causing nondeterministic
installs and failures offline; either add "tsx" to devDependencies (pick a
compatible version with the monorepo/tooling) so the script runs from the local
install instead of npx, or change the script back to use the already-declared
"ts-node" for consistency; update package.json accordingly and run the workspace
install to verify the change.

"dev-https": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --experimental-https",
"dx": "yarn dev",
"test-codegen": "yarn playwright codegen http://localhost:3000",
Expand Down
7 changes: 7 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -3379,5 +3379,12 @@
"timezone_mismatch_tooltip": "You are viewing the report based on your profile timezone ({{userTimezone}}), while your browser is set to timezone ({{browserTimezone}})",
"failed_bookings_by_field": "Failed Bookings By Field",
"event_type_no_hosts": "No hosts are assigned to event type",
"cache_status": "Cache Status",
"cache_last_updated": "Last updated: {{timestamp}}",
"delete_cached_data": "Delete cached data",
"cache_deleted_successfully": "Cache deleted successfully",
"error_deleting_cache": "Error deleting cache",
"confirm_delete_cache": "Are you sure you want to delete the cached data? This action cannot be undone.",
"yes_delete_cache": "Yes, delete cache",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
3 changes: 3 additions & 0 deletions packages/app-store/googlecalendar/lib/CalendarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,9 @@ export default class GoogleCalendarService implements Calendar {
const data = await this.fetchAvailability(parsedArgs);
await this.setAvailabilityInCache(parsedArgs, data);
}

// Update SelectedCalendar.updatedAt for all calendars under this credential
await SelectedCalendarRepository.updateManyByCredentialId(this.credential.id, {});
}

async createSelectedCalendar(
Expand Down
157 changes: 157 additions & 0 deletions packages/features/apps/components/CredentialActionsDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"use client";

import { useState } from "react";

import { useLocale } from "@calcom/lib/hooks/useLocale";
import { GOOGLE_CALENDAR_TYPE } from "@calcom/platform-constants";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { ConfirmationDialogContent } from "@calcom/ui/components/dialog";
import { Dialog } from "@calcom/ui/components/dialog";
import {
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@calcom/ui/components/dropdown";
import { showToast } from "@calcom/ui/components/toast";

interface CredentialActionsDropdownProps {
credentialId: number;
integrationType: string;
cacheUpdatedAt?: Date | null;
onSuccess?: () => void;
delegationCredentialId?: string | null;
disableConnectionModification?: boolean;
}

export default function CredentialActionsDropdown({
credentialId,
integrationType,
cacheUpdatedAt,
onSuccess,
delegationCredentialId,
disableConnectionModification,
}: CredentialActionsDropdownProps) {
const { t } = useLocale();
const [dropdownOpen, setDropdownOpen] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [disconnectModalOpen, setDisconnectModalOpen] = useState(false);

const deleteCacheMutation = trpc.viewer.calendars.deleteCache.useMutation({
onSuccess: () => {
showToast(t("cache_deleted_successfully"), "success");
onSuccess?.();
},
onError: () => {
showToast(t("error_deleting_cache"), "error");
},
});

const utils = trpc.useUtils();
const disconnectMutation = trpc.viewer.credentials.delete.useMutation({
onSuccess: () => {
showToast(t("app_removed_successfully"), "success");
onSuccess?.();
},
onError: () => {
showToast(t("error_removing_app"), "error");
},
async onSettled() {
await utils.viewer.calendars.connectedCalendars.invalidate();
await utils.viewer.apps.integrations.invalidate();
},
});

const isGoogleCalendar = integrationType === GOOGLE_CALENDAR_TYPE;
const canDisconnect = !delegationCredentialId && !disableConnectionModification;
const hasCache = isGoogleCalendar && cacheUpdatedAt;

if (!canDisconnect && !hasCache) {
return null;
}

return (
<>
<Dropdown open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button type="button" variant="icon" color="secondary" StartIcon="ellipsis" />
</DropdownMenuTrigger>
<DropdownMenuContent>
{hasCache && (
<>
<DropdownMenuItem className="focus:ring-muted">
<div className="px-2 py-1">
<div className="text-sm font-medium text-gray-900 dark:text-white">{t("cache_status")}</div>
<div className="text-xs text-gray-500 dark:text-white">
{t("cache_last_updated", {
timestamp: new Intl.DateTimeFormat("en-US", {
dateStyle: "short",
timeStyle: "short",
}).format(new Date(cacheUpdatedAt)),
interpolation: { escapeValue: false },
})}
</div>
</div>
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<DropdownItem
type="button"
color="destructive"
StartIcon="trash"
onClick={() => {
setDeleteModalOpen(true);
setDropdownOpen(false);
}}>
{t("delete_cached_data")}
</DropdownItem>
</DropdownMenuItem>
</>
)}
{canDisconnect && hasCache && <hr className="my-1" />}
{canDisconnect && (
<DropdownMenuItem className="outline-none">
<DropdownItem
type="button"
color="destructive"
StartIcon="trash"
onClick={() => {
setDisconnectModalOpen(true);
setDropdownOpen(false);
}}>
{t("remove_app")}
</DropdownItem>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</Dropdown>

<Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}>
<ConfirmationDialogContent
variety="danger"
title={t("delete_cached_data")}
confirmBtnText={t("yes_delete_cache")}
onConfirm={() => {
deleteCacheMutation.mutate({ credentialId });
setDeleteModalOpen(false);
}}>
{t("confirm_delete_cache")}
</ConfirmationDialogContent>
</Dialog>

<Dialog open={disconnectModalOpen} onOpenChange={setDisconnectModalOpen}>
<ConfirmationDialogContent
variety="danger"
title={t("remove_app")}
confirmBtnText={t("yes_remove_app")}
onConfirm={() => {
disconnectMutation.mutate({ id: credentialId });
setDisconnectModalOpen(false);
}}>
{t("are_you_sure_you_want_to_remove_this_app")}
</ConfirmationDialogContent>
</Dialog>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ export interface ICalendarCacheRepository {
userId: number | null;
args: FreeBusyArgs;
}): Promise<CalendarCache | null>;
getCacheStatusByCredentialIds(
credentialIds: number[]
): Promise<{ credentialId: number; updatedAt: Date | null }[]>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ export class CalendarCacheRepositoryMock implements ICalendarCacheRepository {
async deleteManyByCredential() {
log.info(`Skipping deleteManyByCredential due to calendar-cache being disabled`);
}

async getCacheStatusByCredentialIds() {
log.info(`Skipping getCacheStatusByCredentialIds due to calendar-cache being disabled`);
return [];
}
Comment on lines +27 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Mock’s getCacheStatusByCredentialIds signature doesn’t match the interface

CalendarCacheRepositoryMock implements ICalendarCacheRepository, but defines:

async getCacheStatusByCredentialIds() {
  log.info(`Skipping getCacheStatusByCredentialIds due to calendar-cache being disabled`);
  return [];
}

The interface (per summary) expects a parameter and a specific result shape (e.g. (credentialIds: number[]) => Promise<{ credentialId: number; updatedAt: Date | null }[]>). This mismatch will cause a TypeScript implementation error.

You can fix it by aligning the signature and leveraging the interface’s return type:

-  async getCacheStatusByCredentialIds() {
-    log.info(`Skipping getCacheStatusByCredentialIds due to calendar-cache being disabled`);
-    return [];
-  }
+  async getCacheStatusByCredentialIds(
+    _credentialIds: number[],
+  ): ReturnType<ICalendarCacheRepository["getCacheStatusByCredentialIds"]> {
+    log.info(`Skipping getCacheStatusByCredentialIds due to calendar-cache being disabled`);
+    return [];
+  }

🌐 Web query:

In TypeScript, when a class `implements` an interface that declares `getCacheStatusByCredentialIds(credentialIds: number[]): Promise<...>`, is it valid for the class method to omit the parameter, or must the signature include it?

💡 Result:

Short answer: the class method must be compatible with the interface signature — you should declare the parameter (with a compatible type). Implementing an interface does not automatically copy parameter types into your class, and omitting the parameter or leaving it untyped can produce errors or implicit any. See the TypeScript handbook and discussion. [1][2]

Sources:
[1] TypeScript Handbook — Implementing an interface.
[2] Stack Overflow — "Implementing a Typescript class interface does not preserve method types."


🏁 Script executed:

#!/bin/bash
# Find and examine the ICalendarCacheRepository interface definition
fd -e ts -e tsx | xargs rg -l "ICalendarCacheRepository" | head -5

Length of output: 251


🏁 Script executed:

#!/bin/bash
# Search for the actual interface definition
rg -nA 10 "interface ICalendarCacheRepository" --type ts

Length of output: 1217


🏁 Script executed:

#!/bin/bash
# Check the mock file to see if there are any TypeScript comments indicating errors
cat packages/features/calendar-cache/calendar-cache.repository.mock.ts

Length of output: 1058


🏁 Script executed:

#!/bin/bash
# Get full interface definition including getCacheStatusByCredentialIds
rg -nA 30 "interface ICalendarCacheRepository" packages/features/calendar-cache/calendar-cache.repository.interface.ts

Length of output: 896


🏁 Script executed:

#!/bin/bash
# Search specifically for getCacheStatusByCredentialIds in interface
rg -nB 2 -A 5 "getCacheStatusByCredentialIds" packages/features/calendar-cache/calendar-cache.repository.interface.ts

Length of output: 207


Mock's getCacheStatusByCredentialIds signature doesn't match the interface

CalendarCacheRepositoryMock implements ICalendarCacheRepository, but the mock method at lines 27-30 omits the required parameter:

Interface requires:

getCacheStatusByCredentialIds(credentialIds: number[]): Promise<{ credentialId: number; updatedAt: Date | null }[]>

Mock defines:

async getCacheStatusByCredentialIds() {
  log.info(`Skipping getCacheStatusByCredentialIds due to calendar-cache being disabled`);
  return [];
}

TypeScript will error: the class method must declare the parameter to satisfy the interface contract. Add the parameter:

-  async getCacheStatusByCredentialIds() {
+  async getCacheStatusByCredentialIds(_credentialIds: number[]) {
    log.info(`Skipping getCacheStatusByCredentialIds due to calendar-cache being disabled`);
    return [];
  }

Note: This issue affects all mock methods in the class—each omits its interface parameters.

🤖 Prompt for AI Agents
In packages/features/calendar-cache/calendar-cache.repository.mock.ts around
lines 27 to 30, the mock method getCacheStatusByCredentialIds (and other mock
methods in this class) omit the parameters required by ICalendarCacheRepository;
update each mock method signature to accept the same parameters as the interface
(e.g., getCacheStatusByCredentialIds(credentialIds: number[])) and keep the
current behavior (log and return []/appropriate stub) — if parameters are
unused, name them with a leading underscore (e.g., _credentialIds) to satisfy
TypeScript and linter rules, and ensure return types match the interface
promises.

}
17 changes: 17 additions & 0 deletions packages/features/calendar-cache/calendar-cache.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,21 @@ export class CalendarCacheRepository implements ICalendarCacheRepository {
},
});
}

async getCacheStatusByCredentialIds(credentialIds: number[]) {
const cacheStatuses = await prisma.calendarCache.groupBy({
by: ["credentialId"],
where: {
credentialId: { in: credentialIds },
},
_max: {
updatedAt: true,
},
});

return cacheStatuses.map((cache) => ({
credentialId: cache.credentialId,
updatedAt: cache._max.updatedAt,
}));
}
}
10 changes: 8 additions & 2 deletions packages/lib/getConnectedDestinationCalendars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ type ReturnTypeGetConnectedCalendars = Awaited<ReturnType<typeof getConnectedCal
type ConnectedCalendarsFromGetConnectedCalendars = ReturnTypeGetConnectedCalendars["connectedCalendars"];

export type UserWithCalendars = Pick<User, "id" | "email"> & {
allSelectedCalendars: Pick<SelectedCalendar, "externalId" | "integration" | "eventTypeId">[];
userLevelSelectedCalendars: Pick<SelectedCalendar, "externalId" | "integration" | "eventTypeId">[];
allSelectedCalendars: Pick<
SelectedCalendar,
"externalId" | "integration" | "eventTypeId" | "updatedAt" | "googleChannelId"
>[];
userLevelSelectedCalendars: Pick<
SelectedCalendar,
"externalId" | "integration" | "eventTypeId" | "updatedAt" | "googleChannelId"
>[];
destinationCalendar: DestinationCalendar | null;
};

Expand Down
10 changes: 8 additions & 2 deletions packages/lib/server/repository/selectedCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,7 @@ export class SelectedCalendarRepository {
}

static async findMany({ where, select, orderBy }: FindManyArgs) {
const args = { where, select, orderBy } satisfies Prisma.SelectedCalendarFindManyArgs;
return await prisma.selectedCalendar.findMany(args);
return await prisma.selectedCalendar.findMany({ where, select, orderBy });
}
Comment on lines 259 to 261

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

updateManyByCredentialId helper is fine, but callers must not pass {}

The refactor of findMany to directly call prisma.selectedCalendar.findMany({ where, select, orderBy }) is a nice simplification.

For updateManyByCredentialId:

static async updateManyByCredentialId(credentialId: number, data: Prisma.SelectedCalendarUpdateInput) {
  return await prisma.selectedCalendar.updateMany({
    where: { credentialId },
    data,
  });
}

This is a reasonable wrapper, but it assumes callers always provide at least one field in data. The current usage in GoogleCalendarService.fetchAvailabilityAndSetCache passes {}, which will cause Prisma to throw.

Two small follow-ups:

  • Ensure all call sites pass a non-empty data object (e.g., { updatedAt: new Date() }).
  • Optionally tighten the type to match updateMany more closely:
-static async updateManyByCredentialId(credentialId: number, data: Prisma.SelectedCalendarUpdateInput) {
+static async updateManyByCredentialId(
+  credentialId: number,
+  data: Prisma.SelectedCalendarUpdateManyMutationInput,
+) {

Also applies to: 400-405


🌐 Web query:

What is the correct `data` type and requirements for `prisma.<model>.updateMany` in Prisma Client (e.g. does it accept `data: {}` and which TypeScript type should be used)?

💡 Result:

Short answer

  • updateMany requires a non‑empty data argument typed as XOR<ModelUpdateManyMutationInput, ModelUncheckedUpdateManyInput> (exposed via Prisma's generated types). [1][2]

Details & example

  • Signature (docs): data: XOR<ModelUpdateManyMutationInput, ModelUncheckedUpdateManyInput> — and data is required. [1][2]
  • Practically: you must supply at least one updatable field (even though individual fields on the input type are optional). Passing an empty {} is not meaningful and will fail (you must specify which columns to change). [1][2]

TypeScript example

  • Use the generated types or the args type:
    • prisma.user.updateMany({ where: {...}, data: { role: 'ADMIN' } })
    • The data param is of the generated union type (UserUpdateManyMutationInput | UserUncheckedUpdateManyInput via XOR). [1]

Sources

  • Prisma Client API — updateMany (prisma docs). [1]
  • Prisma CRUD guide — updateMany examples. [2]


---

🏁 Script executed:

```shell
#!/bin/bash
# Inspect the GoogleCalendarService call to updateManyByCredentialId
cd /repo && cat packages/lib/server/services/googleCalendarService.ts | sed -n '400,410p'

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Also search for all calls to updateManyByCredentialId to see usage patterns
cd /repo && rg -n 'updateManyByCredentialId' --type=ts -A2 -B1

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Check the Prisma schema or generated types for SelectedCalendar to confirm correct type names
cd /repo && rg -n 'SelectedCalendarUpdateManyMutationInput|SelectedCalendarUpdateInput' --type=ts | head -20

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Look at the helper definition again to see the exact type used
cd /repo && sed -n '255,270p' packages/lib/server/repository/selectedCalendar.ts

Length of output: 55



---

🏁 Script executed:

```shell
#!/bin/bash
# Inspect the GoogleCalendarService call to updateManyByCredentialId
sed -n '400,410p' packages/lib/server/services/googleCalendarService.ts

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Search for all calls to updateManyByCredentialId to see usage patterns
rg -n 'updateManyByCredentialId' --type=ts -A2 -B1

Length of output: 835


🏁 Script executed:

#!/bin/bash
# Look at the helper definition to see the exact type used
sed -n '255,270p' packages/lib/server/repository/selectedCalendar.ts

Length of output: 474


🏁 Script executed:

#!/bin/bash
# Check for Prisma types in generated or referenced files
rg -n 'SelectedCalendarUpdateManyMutationInput|SelectedCalendarUpdateInput' --type=ts | head -20

Length of output: 297


Fix updateManyByCredentialId call and type signature—passes empty data object

Verification confirms both concerns:

  1. Runtime error: The call at packages/app-store/googlecalendar/lib/CalendarService.ts:1024 passes {} to updateManyByCredentialId, but Prisma's updateMany requires at least one field in data and will throw.

  2. Incorrect type: The helper uses Prisma.SelectedCalendarUpdateInput (for single-record updates), but updateMany requires the union type Prisma.SelectedCalendarUpdateManyMutationInput | Prisma.SelectedCalendarUpdateManyInput.

Required fixes:

  • Line 1024 in packages/app-store/googlecalendar/lib/CalendarService.ts: Change {} to at least one field, e.g., { updatedAt: new Date() }.
  • Line 400 in packages/lib/server/repository/selectedCalendar.ts: Update the type parameter:
    data: Prisma.SelectedCalendarUpdateManyMutationInput
🤖 Prompt for AI Agents
In packages/lib/server/repository/selectedCalendar.ts around line 400 and
packages/app-store/googlecalendar/lib/CalendarService.ts around line 1024, the
updateMany helper uses the wrong Prisma type and callers pass an empty data
object which causes Prisma to throw; change the helper's data parameter type to
Prisma.SelectedCalendarUpdateManyMutationInput (the correct type for updateMany)
and update the call at CalendarService.ts:1024 to supply at least one field (for
example set updatedAt to new Date()) instead of passing an empty object so
Prisma.updateMany receives a non-empty data payload.


static async findUniqueOrThrow({ where }: { where: Prisma.SelectedCalendarWhereInput }) {
Expand Down Expand Up @@ -398,6 +397,13 @@ export class SelectedCalendarRepository {
});
}

static async updateManyByCredentialId(credentialId: number, data: Prisma.SelectedCalendarUpdateInput) {
return await prisma.selectedCalendar.updateMany({
where: { credentialId },
data,
});
}

static async setErrorInWatching({ id, error }: { id: string; error: string }) {
await SelectedCalendarRepository.updateById(id, {
error,
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/server/repository/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,8 @@ export class UserRepository {
eventTypeId: true,
externalId: true,
integration: true,
updatedAt: true,
googleChannelId: true,
},
},
completedOnboarding: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link from "next/link";
import React from "react";

import AppListCard from "@calcom/features/apps/components/AppListCard";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import CredentialActionsDropdown from "@calcom/features/apps/components/CredentialActionsDropdown";
import AdditionalCalendarSelector from "@calcom/features/calendars/AdditionalCalendarSelector";
import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
import { useLocale } from "@calcom/lib/hooks/useLocale";
Expand Down Expand Up @@ -67,18 +67,16 @@ const ConnectedCalendarList = ({
description={connectedCalendar.primary?.email ?? connectedCalendar.integration.description}
className="border-subtle mt-4 rounded-lg border"
actions={
// Delegation credential can't be disconnected
!connectedCalendar.delegationCredentialId &&
!disableConnectionModification && (
<div className="flex w-32 justify-end">
<DisconnectIntegration
credentialId={connectedCalendar.credentialId}
trashIcon
onSuccess={onChanged}
buttonProps={{ className: "border border-default" }}
/>
</div>
)
<div className="flex w-32 justify-end">
<CredentialActionsDropdown
credentialId={connectedCalendar.credentialId}
integrationType={connectedCalendar.integration.type}
cacheUpdatedAt={connectedCalendar.cacheUpdatedAt}
onSuccess={onChanged}
delegationCredentialId={connectedCalendar.delegationCredentialId}
disableConnectionModification={disableConnectionModification}
/>
</div>
}>
<div className="border-subtle border-t">
{!fromOnboarding && (
Expand All @@ -97,7 +95,7 @@ const ConnectedCalendarList = ({
destination={cal.externalId === destinationCalendarId}
credentialId={cal.credentialId}
eventTypeId={shouldUseEventTypeScope ? eventTypeId : null}
delegationCredentialId={connectedCalendar.delegationCredentialId}
delegationCredentialId={connectedCalendar.delegationCredentialId || null}
/>
))}
</ul>
Expand All @@ -122,17 +120,16 @@ const ConnectedCalendarList = ({
}
iconClassName="h-10 w-10 ml-2 mr-1 mt-0.5"
actions={
// Delegation credential can't be disconnected
!connectedCalendar.delegationCredentialId && (
<div className="flex w-32 justify-end">
<DisconnectIntegration
credentialId={connectedCalendar.credentialId}
trashIcon
onSuccess={onChanged}
buttonProps={{ className: "border border-default" }}
/>
</div>
)
<div className="flex w-32 justify-end">
<CredentialActionsDropdown
credentialId={connectedCalendar.credentialId}
integrationType={connectedCalendar.integration.type}
cacheUpdatedAt={connectedCalendar.cacheUpdatedAt}
onSuccess={onChanged}
delegationCredentialId={connectedCalendar.delegationCredentialId}
disableConnectionModification={disableConnectionModification}
/>
</div>
}
/>
);
Expand Down Expand Up @@ -162,6 +159,7 @@ export const SelectedCalendarsSettingsWebWrapper = (props: SelectedCalendarsSett
refetchOnWindowFocus: false,
}
);

const { isPending } = props;
const showScopeSelector = !!props.eventTypeId;
const isDisabled = disabledScope ? disabledScope === scope : false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Warnings:

- Added the required column `updatedAt` to the `CalendarCache` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
-- Add the column with a default value to safely handle existing rows
ALTER TABLE "CalendarCache" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT NOW();
2 changes: 2 additions & 0 deletions packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -1715,6 +1715,8 @@ model CalendarCache {
key String
value Json
expiresAt DateTime
// Provide an initial value for legacy rows and future raw inserts
updatedAt DateTime @default(now()) @updatedAt
credentialId Int
userId Int?
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
Expand Down
9 changes: 9 additions & 0 deletions packages/trpc/server/routers/viewer/calendars/_router.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { z } from "zod";

import authedProcedure from "../../../procedures/authedProcedure";
import { router } from "../../../trpc";
import { ZConnectedCalendarsInputSchema } from "./connectedCalendars.schema";
Expand All @@ -22,4 +24,11 @@ export const calendarsRouter = router({

return setDestinationCalendarHandler({ ctx, input });
}),

deleteCache: authedProcedure
.input(z.object({ credentialId: z.number() }))
.mutation(async ({ ctx, input }) => {
const { deleteCacheHandler } = await import("./deleteCache.handler");
return deleteCacheHandler({ ctx, input });
}),
});
Loading