From 96259f0e3514d947214c91379d3fc5f9da3e4fa6 Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 8 Oct 2026 05:40:31 -0400 Subject: [PATCH] Warn before removing File Browser and disabling local Cloud access --- neode-ui/src/locales/en.json | 5 ++ neode-ui/src/locales/es.json | 5 ++ neode-ui/src/utils/cloudStorageDependency.ts | 4 + neode-ui/src/views/AppDetails.vue | 1 + neode-ui/src/views/Apps.vue | 1 + neode-ui/src/views/ContainerAppDetails.vue | 10 ++- .../ContainerFileBrowserRemoval.test.ts | 36 +++++++++ .../src/views/apps/AppsUninstallModal.vue | 20 +++-- .../apps/__tests__/AppsUninstallModal.test.ts | 75 +++++++++++++------ 9 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 neode-ui/src/utils/cloudStorageDependency.ts create mode 100644 neode-ui/src/views/__tests__/ContainerFileBrowserRemoval.test.ts diff --git a/neode-ui/src/locales/en.json b/neode-ui/src/locales/en.json index 562e39ff..a7f8b502 100644 --- a/neode-ui/src/locales/en.json +++ b/neode-ui/src/locales/en.json @@ -167,6 +167,11 @@ "noResults": "No apps matching \"{query}\"", "uninstallTitle": "Uninstall App?", "uninstallConfirm": "Are you sure you want to uninstall {name}? This will remove the app and stop its container.", + "fileBrowserWarningTitle": "Cloud needs File Browser", + "fileBrowserWarning": "Removing File Browser stops access to your local files, photos, music and documents in Cloud. Installing Nextcloud or Immich does not replace this connection yet. Keep File Browser, or reinstall it to restore access. A different storage system needs a supported Cloud connection first.", + "fileBrowserDeleteDataHelp": "Leave this off to keep your Cloud files for reinstalling File Browser. Turning it on also deletes its stored files and app data.", + "keepFileBrowser": "Keep File Browser", + "removeFileBrowser": "Remove File Browser", "deleteAppDataLabel": "Delete app data and reset it", "deleteAppDataHelp": "Check this to remove the app's data, backups, and persistent files. Leave it off to reinstall later without starting over.", "dismissError": "Dismiss error", diff --git a/neode-ui/src/locales/es.json b/neode-ui/src/locales/es.json index 84d09d6f..48f7d7c5 100644 --- a/neode-ui/src/locales/es.json +++ b/neode-ui/src/locales/es.json @@ -167,6 +167,11 @@ "noResults": "No se encontraron aplicaciones para \"{query}\"", "uninstallTitle": "\u00bfDesinstalar aplicaci\u00f3n?", "uninstallConfirm": "\u00bfEst\u00e1 seguro de que desea desinstalar {name}? Esto eliminar\u00e1 la aplicaci\u00f3n y detendr\u00e1 su contenedor.", + "fileBrowserWarningTitle": "Cloud necesita File Browser", + "fileBrowserWarning": "Eliminar File Browser impide acceder a tus archivos locales, fotos, m\u00fasica y documentos en Cloud. Instalar Nextcloud o Immich todav\u00eda no sustituye esta conexi\u00f3n. Conserva File Browser o vuelve a instalarlo para recuperar el acceso. Otro sistema de almacenamiento necesita primero una conexi\u00f3n compatible con Cloud.", + "fileBrowserDeleteDataHelp": "Deja esta opci\u00f3n desactivada para conservar tus archivos de Cloud al reinstalar File Browser. Si la activas, tambi\u00e9n se eliminan sus archivos almacenados y los datos de la aplicaci\u00f3n.", + "keepFileBrowser": "Conservar File Browser", + "removeFileBrowser": "Eliminar File Browser", "deleteAppDataLabel": "Eliminar los datos de la aplicaci\u00f3n y restablecerla", "deleteAppDataHelp": "Marque esto para eliminar los datos, respaldos y archivos persistentes de la aplicaci\u00f3n. Desm\u00e1rquelo para reinstalar m\u00e1s tarde sin empezar de cero.", "dismissError": "Descartar error", diff --git a/neode-ui/src/utils/cloudStorageDependency.ts b/neode-ui/src/utils/cloudStorageDependency.ts new file mode 100644 index 00000000..10362b9f --- /dev/null +++ b/neode-ui/src/utils/cloudStorageDependency.ts @@ -0,0 +1,4 @@ +// Cloud's local library currently uses File Browser, including legacy container IDs. +export function isCloudStorageApp(appId: string): boolean { + return appId === 'filebrowser' || appId === 'archy-filebrowser' +} diff --git a/neode-ui/src/views/AppDetails.vue b/neode-ui/src/views/AppDetails.vue index 2d30a802..6598625f 100644 --- a/neode-ui/src/views/AppDetails.vue +++ b/neode-ui/src/views/AppDetails.vue @@ -57,6 +57,7 @@ { }) async function handleRemove() { + const cloudStorageDependency = isCloudStorageApp(appId.value) const ok = await appConfirm({ - message: t('apps.uninstallConfirm', { name: appName.value }), - confirmLabel: t('apps.uninstallTitle'), + title: cloudStorageDependency ? t('apps.fileBrowserWarningTitle') : t('apps.uninstallTitle'), + message: cloudStorageDependency + ? t('apps.fileBrowserWarning') + : t('apps.uninstallConfirm', { name: appName.value }), + confirmLabel: t(cloudStorageDependency ? 'apps.removeFileBrowser' : 'apps.uninstallTitle'), + cancelLabel: t(cloudStorageDependency ? 'apps.keepFileBrowser' : 'common.cancel'), danger: true, }) if (!ok) { diff --git a/neode-ui/src/views/__tests__/ContainerFileBrowserRemoval.test.ts b/neode-ui/src/views/__tests__/ContainerFileBrowserRemoval.test.ts new file mode 100644 index 00000000..afb25bbf --- /dev/null +++ b/neode-ui/src/views/__tests__/ContainerFileBrowserRemoval.test.ts @@ -0,0 +1,36 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { flushPromises, mount, type VueWrapper } from '@vue/test-utils' +import ContainerAppDetails from '../ContainerAppDetails.vue' + +const fixture = vi.hoisted(() => ({ + confirm: vi.fn(), remove: vi.fn(), push: vi.fn(), +})) +vi.mock('vue-router', () => ({ + useRoute: () => ({ params: { id: 'filebrowser' } }), + useRouter: () => ({ push: fixture.push }), +})) +vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) })) +vi.mock('@/composables/useAppConfirm', () => ({ appConfirm: fixture.confirm })) +vi.mock('@/stores/container', () => ({ useContainerStore: () => ({ + getContainerStatus: async () => ({ id: 'fixture', image: 'fixture', state: 'running', created: '2026-10-08' }), + getContainerLogs: async () => [], fetchHealthStatus: async () => {}, getHealthStatus: () => 'healthy', + removeContainer: fixture.remove, +}) })) +let wrapper: VueWrapper | undefined +afterEach(() => { wrapper?.unmount(); vi.clearAllMocks() }) + +describe('legacy File Browser removal route', () => { + it.each([false, true])('requires the Cloud warning confirmation (%s)', async accepted => { + fixture.confirm.mockResolvedValue(accepted) + wrapper = mount(ContainerAppDetails, { global: { stubs: { BackButton: true, ContainerStatus: true } } }) + await flushPromises() + await wrapper.findAll('button').find(button => button.text() === 'common.remove')!.trigger('click') + await flushPromises() + expect(fixture.confirm).toHaveBeenCalledWith(expect.objectContaining({ + title: 'apps.fileBrowserWarningTitle', message: 'apps.fileBrowserWarning', + confirmLabel: 'apps.removeFileBrowser', cancelLabel: 'apps.keepFileBrowser', danger: true, + })) + expect(fixture.remove).toHaveBeenCalledTimes(accepted ? 1 : 0) + if (accepted) expect(fixture.remove).toHaveBeenCalledWith('filebrowser') + }) +}) diff --git a/neode-ui/src/views/apps/AppsUninstallModal.vue b/neode-ui/src/views/apps/AppsUninstallModal.vue index 40aa9e79..6c86f2b4 100644 --- a/neode-ui/src/views/apps/AppsUninstallModal.vue +++ b/neode-ui/src/views/apps/AppsUninstallModal.vue @@ -13,7 +13,7 @@ role="dialog" aria-modal="true" aria-labelledby="uninstall-dialog-title" - class="glass-card p-6 max-w-2xl w-full relative z-10" + class="glass-card p-6 max-w-2xl w-full relative z-10 max-h-[90dvh] overflow-y-auto" >
@@ -26,6 +26,10 @@

{{ t('apps.uninstallConfirm', { name: appTitle }) }}

+
+

{{ t('apps.fileBrowserWarningTitle') }}

+

{{ t('apps.fileBrowserWarning') }}

+
@@ -47,7 +51,7 @@ @click="$emit('close')" class="px-4 py-2 glass-button rounded-lg text-sm font-medium" > - {{ t('common.cancel') }} + {{ t(cloudStorageDependency ? 'apps.keepFileBrowser' : 'common.cancel') }}
@@ -76,12 +80,14 @@