diff --git a/client/src/components/Admin/AddonManager.test.tsx b/client/src/components/Admin/AddonManager.test.tsx
index ad5f1ccb0..0964cedc0 100644
--- a/client/src/components/Admin/AddonManager.test.tsx
+++ b/client/src/components/Admin/AddonManager.test.tsx
@@ -1,4 +1,4 @@
-// FE-ADMIN-ADDON-001 to FE-ADMIN-ADDON-025
+// FE-ADMIN-ADDON-001 to FE-ADMIN-ADDON-036
import { render, screen, waitFor } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { delay, http, HttpResponse } from 'msw';
@@ -418,7 +418,102 @@ describe('AddonManager', () => {
expect(isOn(addonToggle('Todo List'))).toBe(true);
});
- it('FE-ADMIN-ADDON-018: the collab sub-features render their state and report the toggled key', async () => {
+ it('FE-ADMIN-ADDON-033: document providers hang off Documents with their glyphs and stay out of the count', async () => {
+ server.use(addonsRoute([
+ buildAddon({ id: 'documents', name: 'Documents', icon: 'FileText', enabled: true }),
+ buildAddon({ id: 'journey', name: 'Journey', type: 'global', icon: 'Compass', enabled: false }),
+ buildAddon({ id: 'paperless', name: 'Paperless-ngx', description: 'Document archive', type: 'document_provider', enabled: true }),
+ buildAddon({ id: 'nextcloud', name: 'Nextcloud', description: 'WebDAV files', type: 'document_provider', enabled: false }),
+ ]));
+ render();
+
+ await screen.findByText('Paperless-ngx');
+ const paperlessRow = screen.getByText('Paperless-ngx').closest('li') as HTMLElement;
+ expect(paperlessRow.querySelector('svg')).toBeInTheDocument();
+ expect(isOn(subToggle('Paperless-ngx'))).toBe(true);
+ expect(isOn(subToggle('Nextcloud'))).toBe(false);
+ // Documents and Journey are the addons; an enabled provider is a shelf row
+ // and must not turn 1/2 into 2/2.
+ expect(screen.getByText('1/2')).toBeInTheDocument();
+ });
+
+ it('FE-ADMIN-ADDON-034: document providers stay hidden while Documents itself is off', async () => {
+ server.use(addonsRoute([
+ buildAddon({ id: 'documents', name: 'Documents', icon: 'FileText', enabled: false }),
+ buildAddon({ id: 'paperless', name: 'Paperless-ngx', type: 'document_provider', enabled: true }),
+ ]));
+ render();
+
+ await screen.findByText('Documents');
+ expect(screen.queryByText('Paperless-ngx')).not.toBeInTheDocument();
+ });
+
+ it('FE-ADMIN-ADDON-035: switching Documents off and on again shows the cascaded providers as off', async () => {
+ const user = userEvent.setup();
+ let loads = 0;
+ let documentsOn = true;
+ let paperlessOn = true;
+ server.use(
+ http.get('/api/admin/addons', () => {
+ loads += 1;
+ return HttpResponse.json({
+ addons: [
+ buildAddon({ id: 'documents', name: 'Documents', icon: 'FileText', enabled: documentsOn }),
+ buildAddon({ id: 'paperless', name: 'Paperless-ngx', type: 'document_provider', enabled: paperlessOn }),
+ ],
+ });
+ }),
+ http.put('/api/admin/addons/documents', async ({ request }) => {
+ documentsOn = (await request.json() as { enabled: boolean }).enabled;
+ if (!documentsOn) paperlessOn = false;
+ return HttpResponse.json({ success: true });
+ }),
+ );
+ render(<>>);
+ await screen.findByText('Paperless-ngx');
+
+ await user.click(addonToggle('Documents'));
+ await screen.findByText('Addon updated');
+ expect(loads).toBe(2);
+ expect(screen.queryByText('Paperless-ngx')).not.toBeInTheDocument();
+
+ await user.click(addonToggle('Documents'));
+
+ await screen.findByText('Paperless-ngx');
+ expect(isOn(subToggle('Paperless-ngx'))).toBe(false);
+ });
+
+ it('FE-ADMIN-ADDON-036: toggling a document provider persists it without re-reading the list', async () => {
+ const user = userEvent.setup();
+ let loads = 0;
+ let body: unknown = null;
+ server.use(
+ http.get('/api/admin/addons', () => {
+ loads += 1;
+ return HttpResponse.json({
+ addons: [
+ buildAddon({ id: 'documents', name: 'Documents', icon: 'FileText', enabled: true }),
+ buildAddon({ id: 'papra', name: 'Papra', type: 'document_provider', enabled: false }),
+ ],
+ });
+ }),
+ http.put('/api/admin/addons/papra', async ({ request }) => {
+ body = await request.json();
+ return HttpResponse.json({ success: true });
+ }),
+ );
+ render(<>>);
+ await screen.findByText('Papra');
+
+ await user.click(subToggle('Papra'));
+
+ await screen.findByText('Addon updated');
+ expect(body).toEqual({ enabled: true });
+ expect(loads).toBe(1);
+ expect(isOn(subToggle('Papra'))).toBe(true);
+ });
+
+ it('FE-ADMIN-ADDON-018:the collab sub-features render their state and report the toggled key', async () => {
const user = userEvent.setup();
const onToggleCollabFeature = vi.fn();
server.use(addonsRoute([buildAddon({ id: 'collab', name: 'Collab', enabled: true })]));
diff --git a/client/src/components/Admin/AddonManager.tsx b/client/src/components/Admin/AddonManager.tsx
index 5623c1ba0..6002d42c7 100644
--- a/client/src/components/Admin/AddonManager.tsx
+++ b/client/src/components/Admin/AddonManager.tsx
@@ -9,7 +9,7 @@ import CustomSelect from '../shared/CustomSelect'
import EmptyState from '../shared/EmptyState'
import DawarichIcon from '../shared/DawarichIcon'
import AirTrailIcon from '../shared/AirTrailIcon'
-import { DOCUMENT_PROVIDER_ICONS, SynologyDriveIcon } from '../shared/DocumentProviderIcons'
+import { DOCUMENT_PROVIDER_ICONS } from '../shared/DocumentProviderIcons'
import AddonTile from './AddonTile'
import AddonSubRow from './AddonSubRow'
@@ -31,10 +31,17 @@ function ImmichIcon({ size = 14 }: { size?: number }) {
)
}
+function SynologyIcon({ size = 14 }: { size?: number }) {
+ return (
+
+ )
+}
+
const PROVIDER_ICONS: Record> = {
immich: ImmichIcon,
- // Photos and Drive are the same brand, so they carry the same mark.
- synologyphotos: SynologyDriveIcon,
+ synologyphotos: SynologyIcon,
// The document providers' own marks, single-colour so they read as glyphs in
// a row of lucide icons and invert with the theme.
...DOCUMENT_PROVIDER_ICONS,
diff --git a/client/src/components/Files/docsync/DocSyncFlow.tsx b/client/src/components/Files/docsync/DocSyncFlow.tsx
index 6ec3f4c85..f09969b6d 100644
--- a/client/src/components/Files/docsync/DocSyncFlow.tsx
+++ b/client/src/components/Files/docsync/DocSyncFlow.tsx
@@ -1,7 +1,7 @@
import { ArrowLeft, ArrowRight } from 'lucide-react'
import { useTranslation } from '../../../i18n/TranslationContext'
import { DOCUMENT_PROVIDER_ICONS } from '../../shared/DocumentProviderIcons'
-import TrekMark from '../../shared/TrekMark'
+import TrekIcon from '../../shared/TrekIcon'
import { Badge } from './DocSyncBits'
export type SyncDirection = 'both' | 'pull' | 'push'
@@ -70,7 +70,7 @@ export default function DocSyncFlow({
}
+ glyph={}
/>
diff --git a/client/src/components/shared/DocumentProviderIcons.test.tsx b/client/src/components/shared/DocumentProviderIcons.test.tsx
index b3ffc558e..c5ee09395 100644
--- a/client/src/components/shared/DocumentProviderIcons.test.tsx
+++ b/client/src/components/shared/DocumentProviderIcons.test.tsx
@@ -7,14 +7,14 @@
* person would notice if they slipped: a provider the server seeds but the
* client has no glyph for (the row renders nothing at all), a mark that keeps a
* brand colour and so stays black on a dark background, and the sizing contract
- * the callers rely on — `size` for the bare lucide-sized rows, a `className`
+ * the callers rely on: `size` for the bare lucide-sized rows, a `className`
* everywhere the surrounding Tailwind box decides how big the glyph is.
*/
import { describe, it, expect } from 'vitest'
import { DOCUMENT_PROVIDER_IDS } from '@trek/shared'
import { render } from '../../../tests/helpers/render'
import { DOCUMENT_PROVIDER_ICONS } from './DocumentProviderIcons'
-import TrekMark from './TrekMark'
+import TrekIcon from './TrekIcon'
const marks = Object.entries(DOCUMENT_PROVIDER_ICONS)
@@ -76,7 +76,7 @@ describe('DOCUMENT_PROVIDER_ICONS', () => {
}
})
- it('FE-DOCSYNC-ICONS-006: stays out of the accessibility tree — the provider name is next to it', () => {
+ it('FE-DOCSYNC-ICONS-006: stays out of the accessibility tree, the provider name is next to it', () => {
for (const [id, Icon] of marks) {
expect(renderMark(Icon).getAttribute('aria-hidden'), id).toBe('true')
}
@@ -117,9 +117,9 @@ describe('DOCUMENT_PROVIDER_ICONS', () => {
})
})
-describe('TrekMark', () => {
- it('FE-DOCSYNC-ICONS-011: renders TREK\u2019s mark in currentColor', () => {
- const { container } = render()
+describe('TrekIcon', () => {
+ it('FE-DOCSYNC-ICONS-011: renders TREK\u2019s app icon in currentColor', () => {
+ const { container } = render()
const svg = container.querySelector('svg')
expect(svg).not.toBeNull()
@@ -128,7 +128,7 @@ describe('TrekMark', () => {
})
it('FE-DOCSYNC-ICONS-012: takes a className, which is the only thing that sizes it', () => {
- const { container } = render()
+ const { container } = render()
const svg = container.querySelector('svg')!
expect(svg.getAttribute('class')).toBe('h-6 w-6 text-content')
@@ -137,18 +137,18 @@ describe('TrekMark', () => {
})
it('FE-DOCSYNC-ICONS-013: forwards the rest of its svg props', () => {
- const { container } = render()
+ const { container } = render()
const svg = container.querySelector('svg')!
- expect(svg.getAttribute('data-testid')).toBe('trek-mark')
+ expect(svg.getAttribute('data-testid')).toBe('trek-icon')
expect(svg.getAttribute('role')).toBe('img')
})
it('FE-DOCSYNC-ICONS-014: is decorative by default and can be unhidden by the caller', () => {
- const { container } = render()
+ const { container } = render()
expect(container.querySelector('svg')!.getAttribute('aria-hidden')).toBe('true')
- const labelled = render()
+ const labelled = render()
const svg = labelled.container.querySelector('svg')!
expect(svg.getAttribute('aria-hidden')).toBe('false')
expect(svg.getAttribute('aria-label')).toBe('TREK')
diff --git a/client/src/components/shared/TrekIcon.tsx b/client/src/components/shared/TrekIcon.tsx
new file mode 100644
index 000000000..e10775f68
--- /dev/null
+++ b/client/src/components/shared/TrekIcon.tsx
@@ -0,0 +1,19 @@
+import type { SVGProps } from 'react'
+
+/**
+ * TREK's square app icon, from public/icons/icon.svg. The wordmark with the API
+ * pill is `TrekMark`.
+ *
+ * Inlined rather than loaded as an so it takes `currentColor` and follows
+ * the theme like the provider glyphs it sits next to in the document-sync flow.
+ * The path is the icon file's, unchanged.
+ */
+export default function TrekIcon({ className, ...props }: SVGProps) {
+ return (
+
+ )
+}
diff --git a/client/src/components/shared/TrekMark.tsx b/client/src/components/shared/TrekMark.tsx
index aff863077..807582809 100644
--- a/client/src/components/shared/TrekMark.tsx
+++ b/client/src/components/shared/TrekMark.tsx
@@ -1,19 +1,21 @@
-import type { SVGProps } from 'react'
+import React from 'react'
/**
- * TREK's own mark, from public/icons/icon.svg.
+ * The TREK wordmark, including the pill.
*
- * Inlined rather than loaded as an so it takes `currentColor`: the
- * document-sync flow puts it next to provider glyphs that all follow the theme,
- * and a fixed-colour file would be the one thing in the row that ignores dark
- * mode. The path is the icon file's, unchanged.
+ * `fill="currentColor"` on the mark and the two pill classes below are what let
+ * one file serve both themes: the letters take the surrounding text colour, the
+ * pill inverts against it. No second asset, no dark-mode variant to keep in
+ * sync, and it stays correct if the accent or the surface ever changes.
*/
-export default function TrekMark({ className, ...props }: SVGProps) {
+export default function TrekMark(props: React.SVGProps): React.ReactElement {
return (
-
+ <>
+
+
+ >
)
}
diff --git a/client/src/mobile/screens/trip/tabs/MDocSyncSheet.tsx b/client/src/mobile/screens/trip/tabs/MDocSyncSheet.tsx
index 23aff8dbc..05a66e40b 100644
--- a/client/src/mobile/screens/trip/tabs/MDocSyncSheet.tsx
+++ b/client/src/mobile/screens/trip/tabs/MDocSyncSheet.tsx
@@ -10,7 +10,7 @@ import MConfirmSheet from '../../settings/MConfirmSheet'
import { TileHeader } from '../sheets/MTripSheetUi'
import { useTranslation } from '../../../../i18n'
import { DOCUMENT_PROVIDER_ICONS } from '../../../../components/shared/DocumentProviderIcons'
-import TrekMark from '../../../../components/shared/TrekMark'
+import TrekIcon from '../../../../components/shared/TrekIcon'
import {
useDocSync, type DocSyncLink, type DocSyncProvider,
} from '../../../../components/Files/docsync/useDocSync'
@@ -265,7 +265,7 @@ function DetailView({
a phone does not have. */}