-
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({ | ||
|
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. 🔷 Medium: After deleting cache, the connectedCalendars list isn't invalidated, so cacheUpdatedAt can remain stale until a manual reload. Add an onSettled to invalidate connectedCalendars (and optionally integrations) and move the diff
|
||
| 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 |
|---|---|---|
| @@ -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.
🔷 Medium: Passing an empty update object may not trigger any write, so SelectedCalendar.updatedAt might not change, leaving the UI with a stale 'Last updated' value. Either have the repository explicitly bump updatedAt when data is empty or set it here to guarantee the timestamp updates. Also consider scoping the update to only calendars actually refreshed to avoid misleading statuses.