-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add calendar cache status and actions (#22532) #2
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 |
|---|---|---|
|
|
@@ -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"; | ||
|
|
@@ -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> | ||
|
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. Empty wrapper div renders when dropdown returns nullLow Severity The Additional Locations (1) |
||
| }> | ||
| <div className="border-subtle border-t"> | ||
| {!fromOnboarding && ( | ||
|
|
@@ -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> | ||
|
|
@@ -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> | ||
| } | ||
| /> | ||
| ); | ||
|
|
@@ -162,6 +159,7 @@ export const SelectedCalendarsSettingsWebWrapper = (props: SelectedCalendarsSett | |
| refetchOnWindowFocus: false, | ||
| } | ||
| ); | ||
|
|
||
| const { isPending } = props; | ||
| const showScopeSelector = !!props.eventTypeId; | ||
| const isDisabled = disabledScope ? disabledScope === scope : false; | ||
|
|
||
| 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.
Hardcoded locale ignores user's language preference
Low Severity
The
Intl.DateTimeFormatcall hardcodes"en-US"for date formatting, while every other usage in the codebase passes a dynamic locale (e.g.,i18n.language). TheuseLocalehook already providesi18nwith the user's language, but the component only destructurest. Non-English users will see American-formatted dates in this dropdown while the rest of the app respects their locale.