From 760a400074a1a70ee5b11bcfe3a201c14f3d3729 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:04:03 +0100 Subject: [PATCH] feat: turn out of office off from the banner in the mail sidebar --- .../sidebar-vacation-banner.test.tsx | 26 ++++++++- components/layout/sidebar.tsx | 54 +++++++++++++++---- locales/ar/common.json | 1 + locales/ca/common.json | 1 + locales/cs/common.json | 1 + locales/da/common.json | 1 + locales/de/common.json | 1 + locales/en/common.json | 1 + locales/es/common.json | 1 + locales/et/common.json | 1 + locales/fa/common.json | 1 + locales/fr/common.json | 1 + locales/he/common.json | 1 + locales/hu/common.json | 1 + locales/it/common.json | 1 + locales/ja/common.json | 1 + locales/ko/common.json | 1 + locales/lv/common.json | 1 + locales/mn/common.json | 1 + locales/nb/common.json | 1 + locales/nl/common.json | 1 + locales/pl/common.json | 1 + locales/pt/common.json | 1 + locales/ro/common.json | 1 + locales/ru/common.json | 1 + locales/sk/common.json | 1 + locales/th/common.json | 1 + locales/tr/common.json | 1 + locales/uk/common.json | 1 + locales/vi/common.json | 1 + locales/zh-TW/common.json | 1 + locales/zh/common.json | 1 + 32 files changed, 99 insertions(+), 11 deletions(-) diff --git a/components/layout/__tests__/sidebar-vacation-banner.test.tsx b/components/layout/__tests__/sidebar-vacation-banner.test.tsx index 1d2c8ff25..a34168eed 100644 --- a/components/layout/__tests__/sidebar-vacation-banner.test.tsx +++ b/components/layout/__tests__/sidebar-vacation-banner.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; // "Vacation responder is active" opens Settings > Out of Office, not @@ -21,10 +21,13 @@ vi.mock('@/hooks/use-mailbox-drop', () => ({ })); vi.mock('@/hooks/use-tag-drop', () => ({ useTagDrop: () => ({ dropHandlers: {}, isValidDropTarget: false }) })); vi.mock('../account-switcher', () => ({ AccountSwitcher: () => null })); +const toast = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), info: vi.fn(), progress: vi.fn(), dismiss: vi.fn() })); +vi.mock('@/stores/toast-store', () => ({ toast })); const { Sidebar } = await import('../sidebar'); const { useVacationStore } = await import('@/stores/vacation-store'); const { buildSettingsPath } = await import('@/lib/deep-links'); +const { useAuthStore } = await import('@/stores/auth-store'); describe('vacation banner', () => { it('opens the Out of Office settings', () => { @@ -34,4 +37,25 @@ describe('vacation banner', () => { expect(nav.push).toHaveBeenCalledWith(buildSettingsPath('vacation')); expect(buildSettingsPath('vacation')).not.toBe('/settings'); }); + + // settings-p2-12: the banner says "Out of office is on", as the setting is + // called, and turns it off without a trip to Settings, like Gmail's "End now". + it('turns out of office off on the server', async () => { + const update = vi.fn(async () => true); + useAuthStore.setState({ client: {} as never }); + useVacationStore.setState({ isEnabled: true, isSupported: true, updateVacationResponse: update } as never); + render(); + fireEvent.click(screen.getByRole('button', { name: 'vacation_turn_off' })); + await waitFor(() => expect(update).toHaveBeenCalledWith(expect.anything(), { isEnabled: false }, undefined)); + expect(toast.error).not.toHaveBeenCalled(); + }); + + it('says so when turning it off fails', async () => { + const update = vi.fn(async () => { throw new Error('down'); }); + useAuthStore.setState({ client: {} as never }); + useVacationStore.setState({ isEnabled: true, isSupported: true, updateVacationResponse: update } as never); + render(); + fireEvent.click(screen.getByRole('button', { name: 'vacation_turn_off' })); + await waitFor(() => expect(toast.error).toHaveBeenCalledWith('vacation_save_failed')); + }); }); diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index aaf3672e5..3b41225c1 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -76,6 +76,7 @@ import { sidebarFolded, useUIStore } from "@/stores/ui-store"; import { useAuthStore } from "@/stores/auth-store"; import { unifiedLoadProgress } from "@/lib/unified-mailbox"; import { useVacationStore } from "@/stores/vacation-store"; +import { useManagedAccountStore } from "@/stores/managed-account-store"; import { useSettingsStore, getKeywordVisibility } from "@/stores/settings-store"; import { usePolicyStore } from "@/stores/policy-store"; import { useEmailStore, buildTagViewAccountClients } from "@/stores/email-store"; @@ -1070,26 +1071,59 @@ function DemoBanner({ ruled }: { ruled: boolean }) { ); } -function VacationBanner() { +export function VacationBanner() { const t = useTranslations('sidebar'); + const tNotifications = useTranslations('notifications'); + const tFilters = useTranslations('settings.filters'); const router = useRouter(); - const { isEnabled, isSupported } = useVacationStore(useShallow((state) => ({ isEnabled: state.isEnabled, isSupported: state.isSupported }))); + const { isEnabled, isSupported, isSaving } = useVacationStore(useShallow((state) => ({ + isEnabled: state.isEnabled, isSupported: state.isSupported, isSaving: state.isSaving, + }))); + // The store holds the reply of the account Settings last loaded it for. + const managedAccountId = useManagedAccountStore((s) => s.managedAccountId); + const client = useAuthStore((s) => s.client); + const [turningOff, setTurningOff] = useState(false); if (!isSupported || !isEnabled) return null; + // Like Gmail's "End now": off on the server, without opening Settings. + const turnOff = async () => { + if (!client || turningOff) return; + setTurningOff(true); + try { + await useVacationStore.getState().updateVacationResponse(client, { isEnabled: false }, managedAccountId ?? undefined); + } catch { + toast.error(tNotifications('vacation_save_failed')); + } finally { + setTurningOff(false); + } + }; + return ( - + + + ); } diff --git a/locales/ar/common.json b/locales/ar/common.json index 1bd640de3..7d6d7b68c 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -174,6 +174,7 @@ }, "clear_search": "مسح البحث", "vacation_active": "الرد التلقائي أثناء الغياب مفعّل", + "vacation_turn_off": "إيقاف", "demo_banner": "وضع تجريبي", "demo_reset": "إعادة تعيين", "demo_tour": "جولة", diff --git a/locales/ca/common.json b/locales/ca/common.json index 1d5b3f23c..adcf4c6b0 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Neteja la cerca", "vacation_active": "La resposta automàtica està activa", + "vacation_turn_off": "Desactiva", "demo_banner": "Mode de demostració", "demo_reset": "Reinicia", "demo_tour": "Visita guiada", diff --git a/locales/cs/common.json b/locales/cs/common.json index 0b2185b9f..b8e8a6f83 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Vymazat hledání", "vacation_active": "Automatická odpověď je aktivní", + "vacation_turn_off": "Vypnout", "demo_banner": "Demo režim", "demo_reset": "Resetovat", "demo_tour": "Průvodce", diff --git a/locales/da/common.json b/locales/da/common.json index c851d2dff..8218e8e43 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Ryd søgning", "vacation_active": "Fraværsbeskeder er aktive", + "vacation_turn_off": "Slå fra", "demo_banner": "Demo-tilstand", "demo_reset": "Nulstil", "demo_tour": "Rundvisning", diff --git a/locales/de/common.json b/locales/de/common.json index 3bc9f148f..f1a53aff3 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Suche löschen", "vacation_active": "Abwesenheitsnotiz ist aktiv", + "vacation_turn_off": "Ausschalten", "demo_banner": "Demo-Modus", "demo_reset": "Zurücksetzen", "demo_tour": "Tour", diff --git a/locales/en/common.json b/locales/en/common.json index 1582cb279..4c02c6e91 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Clear search", "vacation_active": "Vacation responder is active", + "vacation_turn_off": "Turn off", "demo_banner": "Demo Mode", "demo_reset": "Reset", "demo_tour": "Tour", diff --git a/locales/es/common.json b/locales/es/common.json index d44f166af..62b010314 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Limpiar búsqueda", "vacation_active": "Respuesta automática activa", + "vacation_turn_off": "Desactivar", "demo_banner": "Modo demo", "demo_reset": "Restablecer", "demo_tour": "Tour", diff --git a/locales/et/common.json b/locales/et/common.json index 29c3c1e9e..e635d900e 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Tühjenda otsing", "vacation_active": "Puhkuse vastaja on aktiivne", + "vacation_turn_off": "Lülita välja", "demo_banner": "Demorežiim", "demo_reset": "Lähtesta", "demo_tour": "Tutvustus", diff --git a/locales/fa/common.json b/locales/fa/common.json index 7adb7bd7c..04aafb11f 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -174,6 +174,7 @@ }, "clear_search": "پاک کردن جستجو", "vacation_active": "پاسخ‌گوی خودکار فعال است", + "vacation_turn_off": "خاموش کردن", "demo_banner": "حالت آزمایشی", "demo_reset": "بازنشانی", "demo_tour": "تور", diff --git a/locales/fr/common.json b/locales/fr/common.json index 3f042bf5f..423000310 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Effacer la recherche", "vacation_active": "Répondeur d'absence activé", + "vacation_turn_off": "Désactiver", "demo_banner": "Mode démo", "demo_reset": "Réinitialiser", "demo_tour": "Visite", diff --git a/locales/he/common.json b/locales/he/common.json index fd517e8ff..7674fe3c4 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -165,6 +165,7 @@ }, "clear_search": "נקה חיפוש", "vacation_active": "משיב חופשה פעיל", + "vacation_turn_off": "כיבוי", "demo_banner": "מצב הדגמה", "demo_reset": "אִתחוּל", "demo_tour": "סִיוּר", diff --git a/locales/hu/common.json b/locales/hu/common.json index fbdfc8d5d..5fba59ac3 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Keresés törlése", "vacation_active": "Automatikus válasz aktív", + "vacation_turn_off": "Kikapcsolás", "demo_banner": "Demó mód", "demo_reset": "Visszaállítás", "demo_tour": "Bemutató", diff --git a/locales/it/common.json b/locales/it/common.json index 4bca997d1..94ded4537 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Cancella ricerca", "vacation_active": "Risponditore automatico attivo", + "vacation_turn_off": "Disattiva", "demo_banner": "Modalità demo", "demo_reset": "Reimposta", "demo_tour": "Tour", diff --git a/locales/ja/common.json b/locales/ja/common.json index 221d08947..4a5f239e9 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -174,6 +174,7 @@ }, "clear_search": "検索をクリア", "vacation_active": "不在応答が有効です", + "vacation_turn_off": "オフにする", "demo_banner": "デモモード", "demo_reset": "リセット", "demo_tour": "ツアー", diff --git a/locales/ko/common.json b/locales/ko/common.json index bce249371..1171695ee 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -174,6 +174,7 @@ }, "clear_search": "검색어 지우기", "vacation_active": "부재중 자동 응답이 켜져 있어요", + "vacation_turn_off": "끄기", "demo_banner": "데모 모드", "demo_reset": "초기화", "demo_tour": "둘러보기", diff --git a/locales/lv/common.json b/locales/lv/common.json index a068c1422..388d46f2b 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Notīrīt meklēšanu", "vacation_active": "Atvaļinājuma atbilde ir aktīva", + "vacation_turn_off": "Izslēgt", "demo_banner": "Demo režīms", "demo_reset": "Atiestatīt", "demo_tour": "Ekskursija", diff --git a/locales/mn/common.json b/locales/mn/common.json index 08f55f409..9cd8d8911 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Хайлтыг арилгах", "vacation_active": "Амралтын хариулагч идэвхтэй байна", + "vacation_turn_off": "Унтраах", "demo_banner": "Демо горим", "demo_reset": "Дахин тохируулах", "demo_tour": "Аялал", diff --git a/locales/nb/common.json b/locales/nb/common.json index fad34e00d..6d3edf83e 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Tøm søket", "vacation_active": "Feriesvar er aktivt", + "vacation_turn_off": "Slå av", "demo_banner": "Demomodus", "demo_reset": "Tilbakestill", "demo_tour": "Omvisning", diff --git a/locales/nl/common.json b/locales/nl/common.json index 0c4a13c91..094fb1816 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Zoekopdracht wissen", "vacation_active": "Afwezigheidsmelder is actief", + "vacation_turn_off": "Uitzetten", "demo_banner": "Demomodus", "demo_reset": "Resetten", "demo_tour": "Rondleiding", diff --git a/locales/pl/common.json b/locales/pl/common.json index 7f0c65de7..9d86c1be0 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Wyczyść wyszukiwanie", "vacation_active": "Autoresponder wakacyjny jest aktywny", + "vacation_turn_off": "Wyłącz", "demo_banner": "Tryb demo", "demo_reset": "Resetuj", "demo_tour": "Przewodnik", diff --git a/locales/pt/common.json b/locales/pt/common.json index aa6151795..9b6c3728d 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Limpar busca", "vacation_active": "Resposta automática ativa", + "vacation_turn_off": "Desativar", "demo_banner": "Modo de demonstração", "demo_reset": "Repor", "demo_tour": "Tour", diff --git a/locales/ro/common.json b/locales/ro/common.json index 2e7d95aed..b01adf34d 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Șterge căutarea", "vacation_active": "Răspunsul automat de vacanță este activ", + "vacation_turn_off": "Dezactivați", "demo_banner": "Modul demo", "demo_reset": "Resetare", "demo_tour": "Tur de prezentare", diff --git a/locales/ru/common.json b/locales/ru/common.json index 3d4dc098f..deacf0b3f 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Очистить поиск", "vacation_active": "Автоответ на отпуск активен", + "vacation_turn_off": "Выключить", "demo_banner": "Режим демо", "demo_reset": "Сбросить", "demo_tour": "Тур", diff --git a/locales/sk/common.json b/locales/sk/common.json index 975bfd892..f977c9118 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Vymazať hľadanie", "vacation_active": "Automatická odpoveď je aktívna", + "vacation_turn_off": "Vypnúť", "demo_banner": "Demo režim", "demo_reset": "Resetovať", "demo_tour": "Sprievodca", diff --git a/locales/th/common.json b/locales/th/common.json index cf8c4dc09..c980ea48c 100644 --- a/locales/th/common.json +++ b/locales/th/common.json @@ -320,6 +320,7 @@ }, "clear_search": "ล้างการค้นหา", "vacation_active": "เปิดใช้งานการตอบกลับอัตโนมัติอยู่", + "vacation_turn_off": "ปิด", "demo_banner": "โหมดทดลองใช้งาน (Demo)", "demo_reset": "รีเซ็ตข้อมูล", "demo_tour": "แนะนำการใช้งาน", diff --git a/locales/tr/common.json b/locales/tr/common.json index c720854cb..61a54bed0 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Aramayı temizle", "vacation_active": "Tatil otomatik yanıtı aktif", + "vacation_turn_off": "Kapat", "demo_banner": "Demo Modu", "demo_reset": "Sıfırla", "demo_tour": "Tur", diff --git a/locales/uk/common.json b/locales/uk/common.json index 340024098..f9d95697b 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Очистити пошук", "vacation_active": "Автовідповідач активний", + "vacation_turn_off": "Вимкнути", "demo_banner": "Демонстраційний режим", "demo_reset": "Скинути", "demo_tour": "Тур", diff --git a/locales/vi/common.json b/locales/vi/common.json index b717d7c32..ddc49ed4c 100644 --- a/locales/vi/common.json +++ b/locales/vi/common.json @@ -174,6 +174,7 @@ }, "clear_search": "Xóa tìm kiếm", "vacation_active": "Đang bật trả lời tự động khi vắng mặt", + "vacation_turn_off": "Tắt", "demo_banner": "Chế độ demo", "demo_reset": "Đặt lại", "demo_tour": "Tham quan", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 31ec70b4b..34962695d 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -174,6 +174,7 @@ }, "clear_search": "清除搜尋", "vacation_active": "休假自動回覆已開啟", + "vacation_turn_off": "關閉", "demo_banner": "示範模式", "demo_reset": "重設", "demo_tour": "功能導覽", diff --git a/locales/zh/common.json b/locales/zh/common.json index ca69b6e4b..7b6c15c5c 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -174,6 +174,7 @@ }, "clear_search": "清除搜索", "vacation_active": "假期自动回复已开启", + "vacation_turn_off": "关闭", "demo_banner": "演示模式", "demo_reset": "重置", "demo_tour": "引导",