-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add calendar cache status and actions (#22532) #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: calendar-cache-foundation
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainMock’s
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. 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: 💡 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: 🏁 Script executed: #!/bin/bash
# Find and examine the ICalendarCacheRepository interface definition
fd -e ts -e tsx | xargs rg -l "ICalendarCacheRepository" | head -5Length of output: 251 🏁 Script executed: #!/bin/bash
# Search for the actual interface definition
rg -nA 10 "interface ICalendarCacheRepository" --type tsLength 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.tsLength 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.tsLength 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.tsLength of output: 207 Mock's
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 |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain
The refactor of For 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 Two small follow-ups:
-static async updateManyByCredentialId(credentialId: number, data: Prisma.SelectedCalendarUpdateInput) {
+static async updateManyByCredentialId(
+ credentialId: number,
+ data: Prisma.SelectedCalendarUpdateManyMutationInput,
+) {Also applies to: 400-405 🌐 Web query: 💡 Result: Short answer
Details & example
TypeScript example
Sources
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 -B1Length 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 -20Length 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.tsLength of output: 55 Length of output: 96 🏁 Script executed: #!/bin/bash
# Search for all calls to updateManyByCredentialId to see usage patterns
rg -n 'updateManyByCredentialId' --type=ts -A2 -B1Length 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.tsLength of output: 474 🏁 Script executed: #!/bin/bash
# Check for Prisma types in generated or referenced files
rg -n 'SelectedCalendarUpdateManyMutationInput|SelectedCalendarUpdateInput' --type=ts | head -20Length of output: 297 Fix Verification confirms both concerns:
Required fixes:
🤖 Prompt for AI Agents |
||
|
|
||
| static async findUniqueOrThrow({ where }: { where: Prisma.SelectedCalendarWhereInput }) { | ||
|
|
@@ -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, | ||
|
|
||
| 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Add
tsxas a devDependency or clarify workspace availability.The script switches from
ts-nodetonpx tsx, buttsxis not listed in this package's devDependencies. Usingnpx tsxwill 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:
tsxavailable as a workspace dependency (e.g., in a rootpackage.jsonor in packages matching the monorepo pattern)?tsxbe added todevDependenciesalongside or instead ofts-node?ts-node(line 196) still used elsewhere in this package or should it be removed?Run this script to verify:
🏁 Script executed:
Length of output: 14685
Add
tsxto devDependencies or replacenpx tsxwithts-node.The script change switches from
ts-nodetonpx tsx, buttsxis not listed as a devDependency inapps/web/package.jsonor anywhere in the monorepo. Usingnpx tsxwill attempt to fetch the package from npm on each execution, creating non-deterministic behavior and failing in offline or restricted network environments. Additionally,ts-noderemains in devDependencies but is no longer used in this file (it is still used in lines 24–25 for other scripts).Either:
tsxtodevDependenciesalongsidets-node, orts-nodeconsistently for this script to maintain dependency clarity in the workspace🤖 Prompt for AI Agents