mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(docsync): give the sync flow its own TREK icon and restore the wordmark
The document-sync flow needed TREK's square app icon next to the provider glyphs, and the PR got it by rewriting the shared TrekMark component. TrekMark is the wordmark with the API pill that the admin Places API card, the mobile admin block and the release notes popup draw, so all three showed the app icon instead, and AdminSettingsTab FE-ADMSET-031 failed on the missing "TREK Places API" image. TrekMark is back to its dev version. The app icon now lives in TrekIcon, which DocSyncFlow and MDocSyncSheet use. AddonManager had also switched Synology Photos to the Synology Drive glyph, which renders as a block and sat off-line from the Immich row under Journey. Photos keeps its own icon again. The new Documents shelf gets tests: its rows and glyphs, staying out of the addon count, hiding while Documents is off, the re-read after Documents is switched off, and a provider toggle that saves without re-reading.
This commit is contained in:
@@ -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(<AddonManager />);
|
||||
|
||||
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(<AddonManager />);
|
||||
|
||||
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(<><ToastContainer /><AddonManager /></>);
|
||||
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(<><ToastContainer /><AddonManager /></>);
|
||||
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 })]));
|
||||
|
||||
@@ -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 (
|
||||
<svg viewBox="0 0 24 24" width={size} height={size} style={{ flexShrink: 0 }}>
|
||||
<path d="M17.895 11.927a3.196 3.196 0 0 1 .394-1.53l-.008.017a2.677 2.677 0 0 1 1.075-1.108l.014-.007a3.181 3.181 0 0 1 1.523-.382h.05-.003q1.346 0 2.2.871.854.871.86 2.203c0 .895-.29 1.635-.867 2.226s-1.306.886-2.183.886c-.566 0-1.1-.137-1.571-.379l.019.009a2.535 2.535 0 0 1-1.115-1.067l-.007-.013q-.38-.708-.381-1.726zm1.593.083c0 .591.138 1.043.42 1.349a1.365 1.365 0 0 0 2.066.002l.001-.002c.275-.307.413-.764.413-1.357s-.138-1.033-.413-1.342a1.371 1.371 0 0 0-2.066-.001l-.001.002c-.281.306-.42.758-.42 1.345zm-1.602 2.941H16.33v-3.015c0-.635-.032-1.044-.101-1.234a.876.876 0 0 0-.328-.435l-.003-.002a.938.938 0 0 0-.521-.156h-.027.001-.012c-.27 0-.521.084-.727.228l.004-.003a1.115 1.115 0 0 0-.444.576l-.002.008c-.083.248-.121.696-.121 1.359v2.673H12.5V9.027h1.439v.867c.518-.656 1.167-.98 1.952-.98h.021c.335 0 .655.067.946.189l-.016-.006c.261.105.48.268.648.475l.002.003c.141.185.247.404.304.643l.002.012c.057.278.089.597.089.924l-.002.135v-.007zM6.413 9.028h1.654l1.412 4.204 1.376-4.204h1.611l-2.067 5.693-.38 1.038a4.158 4.158 0 0 1-.4.807l.01-.017a1.637 1.637 0 0 1-.422.443l-.005.003c-.17.113-.367.203-.578.26l-.014.003c-.232.064-.499.1-.774.1h-.025.001a4.13 4.13 0 0 1-.911-.105l.028.005-.129-1.229c.198.046.426.074.659.077h.002c.36 0 .628-.106.8-.318a2.27 2.27 0 0 0 .395-.807l.004-.016zM0 12.29l1.592-.149q.147.802.586 1.181.439.379 1.192.375c.528 0 .927-.113 1.197-.335.27-.222.4-.486.4-.782v-.024a.751.751 0 0 0-.167-.474l.001.001c-.113-.132-.309-.252-.59-.347-.193-.074-.631-.191-1.312-.365-.882-.216-1.496-.486-1.85-.804A2.147 2.147 0 0 1 .3 8.936v-.019V8.908c0-.431.132-.831.358-1.163l-.005.007a2.226 2.226 0 0 1 1.003-.826l.015-.005c.442-.184.973-.281 1.602-.281q1.529 0 2.304.676c.516.457.785 1.057.811 1.809l-1.649.055c-.073-.413-.219-.714-.452-.899-.233-.185-.579-.276-1.034-.276-.476 0-.85.098-1.118.298a.59.59 0 0 0-.261.49v.011-.001.002c0 .201.095.379.242.493l.001.001c.205.179.709.36 1.507.546.798.186 1.388.387 1.769.59.374.196.678.48.893.825l.006.01c.214.345.326.786.326 1.305 0 .489-.146.944-.396 1.325l.006-.009c-.264.408-.64.724-1.084.908l-.016.006c-.475.194-1.065.298-1.772.298-1.029 0-1.819-.241-2.373-.722-.554-.481-.879-1.177-.986-2.091z" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const PROVIDER_ICONS: Record<string, ComponentType<{ size?: number }>> = {
|
||||
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,
|
||||
|
||||
@@ -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({
|
||||
<End
|
||||
name={t('docsync.flow.trek')}
|
||||
count={holdings.inTrek}
|
||||
glyph={<TrekMark className="h-6 w-6 text-content" />}
|
||||
glyph={<TrekIcon className="h-6 w-6 text-content" />}
|
||||
/>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2.5">
|
||||
|
||||
@@ -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(<TrekMark />)
|
||||
describe('TrekIcon', () => {
|
||||
it('FE-DOCSYNC-ICONS-011: renders TREK\u2019s app icon in currentColor', () => {
|
||||
const { container } = render(<TrekIcon />)
|
||||
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(<TrekMark className="h-6 w-6 text-content" />)
|
||||
const { container } = render(<TrekIcon className="h-6 w-6 text-content" />)
|
||||
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(<TrekMark data-testid="trek-mark" role="img" />)
|
||||
const { container } = render(<TrekIcon data-testid="trek-icon" role="img" />)
|
||||
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(<TrekMark />)
|
||||
const { container } = render(<TrekIcon />)
|
||||
expect(container.querySelector('svg')!.getAttribute('aria-hidden')).toBe('true')
|
||||
|
||||
const labelled = render(<TrekMark aria-hidden={false} aria-label="TREK" />)
|
||||
const labelled = render(<TrekIcon aria-hidden={false} aria-label="TREK" />)
|
||||
const svg = labelled.container.querySelector('svg')!
|
||||
expect(svg.getAttribute('aria-hidden')).toBe('false')
|
||||
expect(svg.getAttribute('aria-label')).toBe('TREK')
|
||||
|
||||
@@ -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 <img> 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<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 512 512" aria-hidden className={className} {...props}>
|
||||
<g transform="translate(56,51) scale(0.267)">
|
||||
<path d="M 855.636719 699.203125 L 222.246094 699.203125 C 197.679688 699.203125 179.90625 675.75 186.539062 652.101562 L 360.429688 32.390625 C 364.921875 16.386719 379.511719 5.328125 396.132812 5.328125 L 1029.527344 5.328125 C 1054.089844 5.328125 1071.867188 28.777344 1065.230469 52.429688 L 891.339844 672.136719 C 886.851562 688.140625 872.257812 699.203125 855.636719 699.203125 Z M 444.238281 1166.980469 L 533.773438 847.898438 C 540.410156 824.246094 522.632812 800.796875 498.070312 800.796875 L 172.472656 800.796875 C 155.851562 800.796875 141.261719 811.855469 136.769531 827.859375 L 47.234375 1146.941406 C 40.597656 1170.59375 58.375 1194.042969 82.9375 1194.042969 L 408.535156 1194.042969 C 425.15625 1194.042969 439.75 1182.984375 444.238281 1166.980469 Z M 609.003906 827.859375 L 435.113281 1447.570312 C 428.476562 1471.21875 446.253906 1494.671875 470.816406 1494.671875 L 1104.210938 1494.671875 C 1120.832031 1494.671875 1135.421875 1483.609375 1139.914062 1467.605469 L 1313.804688 847.898438 C 1320.441406 824.246094 1302.664062 800.796875 1278.101562 800.796875 L 644.707031 800.796875 C 628.085938 800.796875 613.492188 811.855469 609.003906 827.859375 Z M 1056.105469 333.019531 L 966.570312 652.101562 C 959.933594 675.75 977.710938 699.203125 1002.273438 699.203125 L 1327.871094 699.203125 C 1344.492188 699.203125 1359.085938 688.140625 1363.574219 672.136719 L 1453.109375 353.054688 C 1459.746094 329.40625 1441.96875 305.953125 1417.40625 305.953125 L 1091.808594 305.953125 C 1075.1875 305.953125 1060.597656 317.015625 1056.105469 333.019531 Z" fill="currentColor" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -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. */}
|
||||
<section className="rounded-2xl border border-[color:var(--m-rowbr)] bg-m-sheetop p-3">
|
||||
<div className="flex items-stretch gap-2">
|
||||
<EndBox glyph={<TrekMark className="h-[18px] w-[18px]" />} count={holdings.inTrek} name={t('docsync.flow.trek')} />
|
||||
<EndBox glyph={<TrekIcon className="h-[18px] w-[18px]" />} count={holdings.inTrek} name={t('docsync.flow.trek')} />
|
||||
<EndBox
|
||||
glyph={ProviderIcon ? <ProviderIcon className="h-[18px] w-[18px]" /> : null}
|
||||
count={holdings.atProvider}
|
||||
|
||||
Reference in New Issue
Block a user