From 62ab5cbb7632d8bd62d662fb14ff3edeffc4940a Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 16:40:03 +0100 Subject: [PATCH] feat: add a Files on this computer settings page to the desktop app The settings nav test drew every page but recovery; the new page waits for the desktop app's sync engine like recovery waits for its probe, so the test leaves it out too and a new test checks it shows while the engine runs. --- app/boot-skeleton.css | 2 + .../boot-skeleton/settings-skeleton.tsx | 2 + components/files-sync/files-sync-prompts.tsx | 156 ++++++++ .../files-sync/files-sync-status-line.tsx | 60 +++ components/files-sync/files-sync-text.ts | 46 +++ .../__tests__/desktop-files-settings.test.tsx | 314 ++++++++++++++++ .../__tests__/settings-boot-skeleton.test.tsx | 19 +- .../settings-search-anchors.test.tsx | 17 +- .../settings/desktop-files-settings.tsx | 346 ++++++++++++++++++ components/settings/settings-app.tsx | 6 + components/settings/settings-tab-icons.ts | 4 +- lib/settings-search.ts | 11 + lib/settings-tabs.ts | 2 +- locales/ar/common.json | 106 ++++++ locales/ca/common.json | 106 ++++++ locales/cs/common.json | 106 ++++++ locales/da/common.json | 106 ++++++ locales/de/common.json | 106 ++++++ locales/en/common.json | 106 ++++++ locales/es/common.json | 106 ++++++ locales/et/common.json | 106 ++++++ locales/fa/common.json | 106 ++++++ locales/fr/common.json | 106 ++++++ locales/he/common.json | 106 ++++++ locales/hu/common.json | 106 ++++++ locales/it/common.json | 106 ++++++ locales/ja/common.json | 106 ++++++ locales/ko/common.json | 106 ++++++ locales/lv/common.json | 106 ++++++ locales/mn/common.json | 106 ++++++ locales/nb/common.json | 106 ++++++ locales/nl/common.json | 106 ++++++ locales/pl/common.json | 106 ++++++ locales/pt/common.json | 106 ++++++ locales/ro/common.json | 106 ++++++ locales/ru/common.json | 106 ++++++ locales/sk/common.json | 106 ++++++ locales/th/common.json | 106 ++++++ locales/tr/common.json | 106 ++++++ locales/uk/common.json | 106 ++++++ locales/vi/common.json | 106 ++++++ locales/zh-TW/common.json | 106 ++++++ locales/zh/common.json | 106 ++++++ 43 files changed, 4158 insertions(+), 7 deletions(-) create mode 100644 components/files-sync/files-sync-prompts.tsx create mode 100644 components/files-sync/files-sync-status-line.tsx create mode 100644 components/files-sync/files-sync-text.ts create mode 100644 components/settings/__tests__/desktop-files-settings.test.tsx create mode 100644 components/settings/desktop-files-settings.tsx diff --git a/app/boot-skeleton.css b/app/boot-skeleton.css index dbf7543cf..0592e4bfd 100644 --- a/app/boot-skeleton.css +++ b/app/boot-skeleton.css @@ -220,6 +220,7 @@ [data-boot-settings-tab="calendar"] [data-sk-settings-tab="calendar"], [data-boot-settings-tab="contacts"] [data-sk-settings-tab="contacts"], [data-boot-settings-tab="files"] [data-sk-settings-tab="files"], + [data-boot-settings-tab="files_sync"] [data-sk-settings-tab="files_sync"], [data-boot-settings-tab="addons"] [data-sk-settings-tab="addons"], [data-boot-settings-tab="offline"] [data-sk-settings-tab="offline"], [data-boot-settings-tab="privacy"] [data-sk-settings-tab="privacy"], @@ -239,6 +240,7 @@ @custom-variant boot-no-settings-addons (&:where([data-boot-settings-tabs]:not([data-boot-settings-tabs~="addons"]) *)); @custom-variant boot-no-settings-offline (&:where([data-boot-settings-tabs]:not([data-boot-settings-tabs~="offline"]) *)); @custom-variant boot-settings-recovery (&:where([data-boot-settings-tabs~="recovery"] *)); +@custom-variant boot-settings-files-sync (&:where([data-boot-settings-tabs~="files_sync"] *)); /* Settings pages with one section have no section jump row (SectionNav). */ @custom-variant boot-settings-one-section (&:where([data-boot-settings-tab="security"] *, [data-boot-settings-tab="filters"] *, [data-boot-settings-tab="vacation"] *, [data-boot-settings-tab="recovery"] *, [data-boot-settings-tab="addons"] *)); diff --git a/components/boot-skeleton/settings-skeleton.tsx b/components/boot-skeleton/settings-skeleton.tsx index c49515063..82477c6bb 100644 --- a/components/boot-skeleton/settings-skeleton.tsx +++ b/components/boot-skeleton/settings-skeleton.tsx @@ -27,6 +27,8 @@ const OPTIONAL: Partial> = { calendar: "boot-no-calendar:hidden boot-no-settings-calendar:hidden", contacts: "boot-no-settings-contacts:hidden", files: "boot-no-files:hidden boot-no-settings-files:hidden", + // Desktop app only, while the files sync runs. + files_sync: "hidden boot-settings-files-sync:flex", addons: "boot-no-settings-addons:hidden", // Not in the static Lite build. offline: "boot-no-settings-offline:hidden", diff --git a/components/files-sync/files-sync-prompts.tsx b/components/files-sync/files-sync-prompts.tsx new file mode 100644 index 000000000..fdf938506 --- /dev/null +++ b/components/files-sync/files-sync-prompts.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useEffect, useState, useSyncExternalStore } from "react"; +import { useTranslations } from "next-intl"; +import { AlertTriangle } from "@/components/icons"; +import { Button } from "@/components/ui/button"; +import { Dialog } from "@/components/ui/dialog"; +import { useFilesSyncStore } from "@/stores/files-sync-store"; +import type { FilesRoot } from "@/lib/desktop/types"; +import type { FilesSyncRootStatus } from "@/lib/files-sync/types"; +import { rootName } from "./files-sync-text"; + +/* + * The mass-delete question (FILES-SYNC-BRIEF: more than 50 items or more than half a + * pair at once pauses the pair and asks). The settings page asks it in the folder's + * row; everywhere else FilesSyncPrompts asks it in a dialog, so the user sees it + * wherever they are. + */ + +let inlineHosts = 0; +const inlineListeners = new Set<() => void>(); +const subscribeInline = (listener: () => void) => { + inlineListeners.add(listener); + return () => { inlineListeners.delete(listener); }; +}; +const inlineShown = () => inlineHosts > 0; + +/** The settings page asks in its rows while it's open: the dialog stays away meanwhile. */ +export function useInlineMassDeleteQuestions(): void { + useEffect(() => { + inlineHosts += 1; + inlineListeners.forEach((listener) => listener()); + return () => { + inlineHosts -= 1; + inlineListeners.forEach((listener) => listener()); + }; + }, []); +} + +/** The question's text, for a root in `confirm-delete`. */ +export function useMassDeleteText(root: FilesRoot, massDelete: NonNullable): string { + const t = useTranslations("files_sync"); + // The side the items went from: this computer's folder, or the Files folder. + return massDelete.side === "local" + ? t("mass_delete.local", { count: massDelete.count, folder: rootName(root, t("drive.root_name")) }) + : t("mass_delete.remote", { count: massDelete.count, folder: root.remoteLabel || t("drive.root_name") }); +} + +interface AnswerButtonsProps { + rootId: string; + count: number; + /** After an answer went out. */ + onAnswered?: () => void; +} + +/** "Delete them" and "Bring them back", straight to the engine. */ +export function MassDeleteAnswers({ rootId, count, onAnswered }: AnswerButtonsProps) { + const t = useTranslations("files_sync"); + const controller = useFilesSyncStore((s) => s.controller); + const [busy, setBusy] = useState(false); + const answer = (allow: boolean) => { + if (!controller) return; + setBusy(true); + void controller.confirmMassDelete(rootId, allow).catch(() => {}).finally(() => { + setBusy(false); + onAnswered?.(); + }); + }; + return ( + <> + + + + ); +} + +/** The question inside a folder's row on the settings page. */ +export function MassDeleteQuestion({ root, status }: { root: FilesRoot; status: FilesSyncRootStatus }) { + const massDelete = status.massDelete ?? { count: 0, side: "local" as const }; + const text = useMassDeleteText(root, massDelete); + return ( +
+

+

+
+ +
+
+ ); +} + +function PromptDialog({ root, status, onLater }: { root: FilesRoot; status: FilesSyncRootStatus; onLater: () => void }) { + const t = useTranslations("files_sync"); + const massDelete = status.massDelete!; + const text = useMassDeleteText(root, massDelete); + return ( + +