mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
feat(client): descriptive category names, descriptions and id badges in the storage panel
This commit is contained in:
@@ -74,7 +74,7 @@ describe('AdminStoragePanel', () => {
|
||||
const uploads = backendRow('uploads-local');
|
||||
expect(within(uploads).getByText('Local')).toBeInTheDocument();
|
||||
expect(within(uploads).getByText('Built-in')).toBeInTheDocument();
|
||||
expect(within(uploads).getByText(/Used by: files/)).toBeInTheDocument();
|
||||
expect(within(uploads).getByText(/Used by: Trip documents/)).toBeInTheDocument();
|
||||
const env = backendRow('place-photos-local');
|
||||
expect(within(env).getByText('Environment')).toBeInTheDocument();
|
||||
expect(within(env).getByText(/read-only/)).toBeInTheDocument();
|
||||
@@ -155,7 +155,7 @@ describe('AdminStoragePanel', () => {
|
||||
}),
|
||||
);
|
||||
fireEvent.click(within(backendRow('off-box')).getByRole('button', { name: 'Remove' }));
|
||||
expect(screen.getByText(/Still assigned to: covers/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Still assigned to: Cover images/)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Remove backend' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save changes' }));
|
||||
await screen.findByText('Storage configuration saved');
|
||||
@@ -219,7 +219,7 @@ describe('AdminStoragePanel', () => {
|
||||
expect(screen.queryByTestId('storage-backend-mirror')).not.toBeInTheDocument();
|
||||
const primary = backendRow('backups-local');
|
||||
expect(within(primary).getByText('Mirrored to: off-box')).toBeInTheDocument();
|
||||
expect(within(primary).getByText(/Used by: .*backups/)).toBeInTheDocument();
|
||||
expect(within(primary).getByText(/Used by: .*Backups/)).toBeInTheDocument();
|
||||
const replica = backendRow('off-box');
|
||||
expect(within(replica).getByText('Replica of: backups-local')).toBeInTheDocument();
|
||||
});
|
||||
@@ -323,4 +323,14 @@ describe('AdminStoragePanel', () => {
|
||||
expect(within(row).getByRole('button', { name: 'Remove' })).toBeInTheDocument();
|
||||
expect(within(row).queryByRole('button', { name: 'Edit' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-ADMIN-STOR-017: category rows show the display name, the raw id badge, and the description; photos is gone', async () => {
|
||||
await renderPanel();
|
||||
const files = categoryRow('files');
|
||||
expect(within(files).getByText('Trip documents')).toBeInTheDocument();
|
||||
expect(within(files).getByText('files')).toBeInTheDocument(); // the monospace id badge
|
||||
expect(within(files).getByText(/tickets, PDFs, booking confirmations/)).toBeInTheDocument();
|
||||
expect(within(categoryRow('photos-google')).getByText(/re-fetchable, safe to lose/)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('storage-category-photos')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,10 @@ import {
|
||||
} from './storageModel'
|
||||
import { useStorageAdmin } from './useStorageAdmin'
|
||||
|
||||
/** Display-name mapper for joined category lists — the raw id renders only in the badge. */
|
||||
const categoryNames = (t: (key: string) => string, ids: readonly string[]): string =>
|
||||
ids.map((id) => t(`storage.category.${id}`)).join(', ')
|
||||
|
||||
function TestResult({ result, failedLabel, okLabel }: {
|
||||
result: StorageTestResponse | undefined
|
||||
failedLabel: string
|
||||
@@ -106,7 +110,7 @@ export default function AdminStoragePanel(): React.ReactElement {
|
||||
const usedAsReplicaBy = isDegenerate ? [] : replicaOfPrimaries(draft, name)
|
||||
return [
|
||||
t('storage.remove.body', { name }),
|
||||
assigned.length > 0 ? t('storage.remove.stillAssigned', { categories: assigned.join(', ') }) : '',
|
||||
assigned.length > 0 ? t('storage.remove.stillAssigned', { categories: categoryNames(t, assigned) }) : '',
|
||||
usedAsReplicaBy.length > 0 ? t('storage.remove.usedAsReplicaBy', { primaries: usedAsReplicaBy.join(', ') }) : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -194,7 +198,7 @@ export default function AdminStoragePanel(): React.ReactElement {
|
||||
</div>
|
||||
<p className="text-xs mt-1 text-content-faint">
|
||||
{row.categories.length > 0
|
||||
? t('storage.backends.usedBy', { categories: row.categories.join(', ') })
|
||||
? t('storage.backends.usedBy', { categories: categoryNames(t, row.categories) })
|
||||
: t('storage.backends.unused')}
|
||||
</p>
|
||||
{row.mirrorTargets.length > 0 && (
|
||||
@@ -293,9 +297,13 @@ export default function AdminStoragePanel(): React.ReactElement {
|
||||
const viaMirror = effective[category] !== selectedPrimary
|
||||
return (
|
||||
<div key={category} data-testid={`storage-category-${category}`}>
|
||||
<label className="block text-sm font-medium mb-1.5 text-content-secondary">
|
||||
<label className="block text-sm font-medium mb-1 text-content-secondary">
|
||||
{t(`storage.category.${category}`)}
|
||||
<span className="ml-2 px-1.5 py-0.5 rounded border font-mono font-normal text-xs border-edge text-content-faint">
|
||||
{category}
|
||||
</span>
|
||||
</label>
|
||||
<p className="text-xs mb-1.5 text-content-faint">{t(`storage.categoryDesc.${category}`)}</p>
|
||||
<CustomSelect
|
||||
value={selectedPrimary}
|
||||
onChange={(value) => setCategory(category, String(value))}
|
||||
|
||||
@@ -45,6 +45,10 @@ function valuesOf(backend: StorageBackend | null): FieldValues {
|
||||
return values
|
||||
}
|
||||
|
||||
/** Display-name mapper for joined category lists — the raw id renders only in the badge. */
|
||||
const categoryNames = (t: (key: string) => string, ids: readonly string[]): string =>
|
||||
ids.map((id) => t(`storage.category.${id}`)).join(', ')
|
||||
|
||||
/** Same behavior contract as the desktop BackendForm, rendered on the M* primitives. */
|
||||
function MBackendForm({
|
||||
initial,
|
||||
@@ -299,7 +303,7 @@ export default function MAdminStoragePanel(): React.ReactElement {
|
||||
const usedAsReplicaBy = isDegenerate ? [] : replicaOfPrimaries(draft, name)
|
||||
return [
|
||||
t('storage.remove.body', { name }),
|
||||
assigned.length > 0 ? t('storage.remove.stillAssigned', { categories: assigned.join(', ') }) : '',
|
||||
assigned.length > 0 ? t('storage.remove.stillAssigned', { categories: categoryNames(t, assigned) }) : '',
|
||||
usedAsReplicaBy.length > 0 ? t('storage.remove.usedAsReplicaBy', { primaries: usedAsReplicaBy.join(', ') }) : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -377,7 +381,7 @@ export default function MAdminStoragePanel(): React.ReactElement {
|
||||
</div>
|
||||
<p className="mt-1 font-geist text-[0.625rem] text-m-muted">
|
||||
{row.categories.length > 0
|
||||
? t('storage.backends.usedBy', { categories: row.categories.join(', ') })
|
||||
? t('storage.backends.usedBy', { categories: categoryNames(t, row.categories) })
|
||||
: t('storage.backends.unused')}
|
||||
</p>
|
||||
{row.mirrorTargets.length > 0 && (
|
||||
@@ -514,7 +518,18 @@ export default function MAdminStoragePanel(): React.ReactElement {
|
||||
const changed = selectedPrimary !== primaryNameOf(state, draft, stateEntry.backend)
|
||||
const viaMirror = effective[category] !== selectedPrimary
|
||||
return (
|
||||
<MAdminField key={category} label={t(`storage.category.${category}`)}>
|
||||
<MAdminField
|
||||
key={category}
|
||||
label={
|
||||
<>
|
||||
{t(`storage.category.${category}`)}{' '}
|
||||
<span className="rounded border border-[color:var(--m-rowbr)] px-1 font-geist text-[0.625rem] font-normal text-m-faint">
|
||||
{category}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
hint={t(`storage.categoryDesc.${category}`)}
|
||||
>
|
||||
<div data-testid={`m-storage-category-${category}`} className="contents">
|
||||
<MSetSelectRow
|
||||
label={
|
||||
|
||||
@@ -151,9 +151,23 @@ describe('MAdminStoragePanel', () => {
|
||||
);
|
||||
fireEvent.click(within(screen.getByTestId('m-storage-category-places')).getByRole('button'));
|
||||
fireEvent.click(within(screen.getByRole('dialog', { name: 'Place images' })).getByRole('button', { name: 'backups-local' })); // picker option = primary name
|
||||
expect(screen.getByText(/re-fetchable/)).toBeInTheDocument();
|
||||
// getByText(/re-fetchable/) is now ambiguous — the photos-google/photos-trek category
|
||||
// descriptions also say "re-fetchable". The cache warning is the only role="note" on
|
||||
// screen here (the mirror-targets latency note only renders while a form is open).
|
||||
expect(screen.getByRole('note')).toHaveTextContent(/re-fetchable/);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save changes' }));
|
||||
await screen.findByText('Storage configuration saved');
|
||||
expect((putBody as { categories: Record<string, string> }).categories.places).toBe('mirror');
|
||||
});
|
||||
|
||||
it('FE-MOB-MSTOR-009: category rows show display name, id badge and description; photos is gone', async () => {
|
||||
await renderPanel();
|
||||
const files = screen.getByTestId('m-storage-category-files');
|
||||
// The testid wraps only the select row — assert name/badge/description through the field around it.
|
||||
expect(screen.getByText('Trip documents')).toBeInTheDocument();
|
||||
expect(screen.getByText('files')).toBeInTheDocument();
|
||||
expect(screen.getByText(/tickets, PDFs, booking confirmations/)).toBeInTheDocument();
|
||||
expect(within(files).getByRole('button')).toBeInTheDocument(); // row still tappable
|
||||
expect(screen.queryByTestId('m-storage-category-photos')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user