Move pond settings to a TopBar gear, pin the trash link to the sidebar bottom (M10 follow-up)
All checks were successful
CD / Build and push images (push) Successful in 1m8s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m42s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m31s
CI / Import/export fidelity gate (push) Successful in 47s
All checks were successful
CD / Build and push images (push) Successful in 1m8s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m42s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m31s
CI / Import/export fidelity gate (push) Successful in 47s
- owners get a Settings icon next to the pond name while a pond route
is active (same lucide set, localized aria-label/tooltip via the
existing labels:link key); gone on non-pond routes and for non-owners
- the trash stays a text link but pins to the sidebar's bottom
(.sidebar is a flex column now; .sidebar__footer uses margin-top:auto)
- trash.spec: delete flows go through the #101 overflow menu (was
missed in 65f30a5 — the pack is not part of CI)
- markdown.spec: replace the wait for the 'saved' status removed in #36
with polling the export endpoint (pre-existing local failure, same
category as the known image.spec one)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
This commit is contained in:
parent
e740ea6c01
commit
33121cd73d
@ -117,7 +117,15 @@ test('page menu downloads the page as Markdown matching its content', async ({ b
|
|||||||
await enterEditMode(page);
|
await enterEditMode(page);
|
||||||
await page.locator('.ProseMirror').click();
|
await page.locator('.ProseMirror').click();
|
||||||
await page.keyboard.type('export me please');
|
await page.keyboard.type('export me please');
|
||||||
await expect(page.getByRole('status')).toHaveText(/saved|gespeichert/i, { timeout: 10000 });
|
|
||||||
|
// There is no "saved" status since #36 (collab autosaves continuously) —
|
||||||
|
// poll the export endpoint until the typed content has been persisted.
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => (await context.request.get(`/api/v1/pages/${pageId}/export/markdown`)).text(),
|
||||||
|
{ timeout: 10000 },
|
||||||
|
)
|
||||||
|
.toBe('export me please');
|
||||||
|
|
||||||
const exported = await context.request.get(`/api/v1/pages/${pageId}/export/markdown`);
|
const exported = await context.request.get(`/api/v1/pages/${pageId}/export/markdown`);
|
||||||
expect(exported.headers()['content-disposition']).toContain(`${pageSlug}.md`);
|
expect(exported.headers()['content-disposition']).toContain(`${pageSlug}.md`);
|
||||||
|
|||||||
@ -43,7 +43,9 @@ test('deleting a page hides it from the sidebar and shows a trash hint on direct
|
|||||||
|
|
||||||
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
||||||
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
||||||
await page.getByRole('button', { name: /move to trash|papierkorb verschieben/i }).click();
|
// Delete sits behind the TopBar overflow menu since #101.
|
||||||
|
await page.getByRole('button', { name: /more actions|weitere aktionen/i }).click();
|
||||||
|
await page.getByRole('menuitem', { name: /move to trash|papierkorb verschieben/i }).click();
|
||||||
|
|
||||||
// Deleting navigates away; going back to the same URL now 404s with a hint.
|
// Deleting navigates away; going back to the same URL now 404s with a hint.
|
||||||
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
||||||
@ -64,7 +66,9 @@ test('restoring a page from the trash brings it back', async ({ browser }) => {
|
|||||||
|
|
||||||
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
||||||
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
||||||
await page.getByRole('button', { name: /move to trash|papierkorb verschieben/i }).click();
|
// Delete sits behind the TopBar overflow menu since #101.
|
||||||
|
await page.getByRole('button', { name: /more actions|weitere aktionen/i }).click();
|
||||||
|
await page.getByRole('menuitem', { name: /move to trash|papierkorb verschieben/i }).click();
|
||||||
|
|
||||||
await page.goto(`/p/${pondSlug}/trash`);
|
await page.goto(`/p/${pondSlug}/trash`);
|
||||||
const item = trashItem(page, title);
|
const item = trashItem(page, title);
|
||||||
@ -87,7 +91,9 @@ test('purging a page from the trash removes it for good', async ({ browser }) =>
|
|||||||
|
|
||||||
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
await page.goto(`/p/${pondSlug}/${pageSlug}`);
|
||||||
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
await page.getByRole('button', { name: /edit|bearbeiten/i }).click();
|
||||||
await page.getByRole('button', { name: /move to trash|papierkorb verschieben/i }).click();
|
// Delete sits behind the TopBar overflow menu since #101.
|
||||||
|
await page.getByRole('button', { name: /more actions|weitere aktionen/i }).click();
|
||||||
|
await page.getByRole('menuitem', { name: /move to trash|papierkorb verschieben/i }).click();
|
||||||
|
|
||||||
await page.goto(`/p/${pondSlug}/trash`);
|
await page.goto(`/p/${pondSlug}/trash`);
|
||||||
const item = trashItem(page, title);
|
const item = trashItem(page, title);
|
||||||
|
|||||||
@ -126,16 +126,6 @@ export function Sidebar({ collapsed }: SidebarProps): React.JSX.Element {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
{isOwner && (
|
|
||||||
<Link to={`/p/${pondSlug}/settings`} className="linklike sidebar__settings-link">
|
|
||||||
{tLabels('link')}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{isOwner && (
|
|
||||||
<Link to={`/p/${pondSlug}/trash`} className="linklike sidebar__trash-link">
|
|
||||||
{t('editor:trash.link')}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{flat.length > 0 && (
|
{flat.length > 0 && (
|
||||||
@ -282,6 +272,16 @@ export function Sidebar({ collapsed }: SidebarProps): React.JSX.Element {
|
|||||||
<p className="visually-hidden sidebar__announce" role="status" aria-live="polite">
|
<p className="visually-hidden sidebar__announce" role="status" aria-live="polite">
|
||||||
{announcement}
|
{announcement}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* The trash stays a text link, pinned to the sidebar's bottom
|
||||||
|
(M10 follow-up; pond settings moved to the TopBar gear icon). */}
|
||||||
|
{isOwner && (
|
||||||
|
<div className="sidebar__footer">
|
||||||
|
<Link to={`/p/${pondSlug}/trash`} className="linklike sidebar__trash-link">
|
||||||
|
{t('editor:trash.link')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -1,13 +1,17 @@
|
|||||||
import { Menu, Search } from 'lucide-react';
|
import type { PondView } from '@dorfteich/shared';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Menu, Search, Settings } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { useAuth } from '../auth/auth-context';
|
import { useAuth } from '../auth/auth-context';
|
||||||
|
import { apiGet } from '../lib/api';
|
||||||
import { SearchPalette } from '../search/SearchPalette';
|
import { SearchPalette } from '../search/SearchPalette';
|
||||||
import { NotificationsBell } from '../notifications/NotificationsBell';
|
import { NotificationsBell } from '../notifications/NotificationsBell';
|
||||||
import { usePageActionsSlot } from './page-actions';
|
import { usePageActionsSlot } from './page-actions';
|
||||||
import { PondSwitcher } from './PondSwitcher';
|
import { PondSwitcher } from './PondSwitcher';
|
||||||
|
import { useCurrentPondRoute } from './use-pond-route';
|
||||||
|
|
||||||
/** True when focus is in a field where "/" should type, not open search. */
|
/** True when focus is in a field where "/" should type, not open search. */
|
||||||
function isTypingTarget(target: EventTarget | null): boolean {
|
function isTypingTarget(target: EventTarget | null): boolean {
|
||||||
@ -28,6 +32,15 @@ export function TopBar({ sidebarCollapsed, onToggleSidebar }: TopBarProps): Reac
|
|||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [searchOpen, setSearchOpen] = useState(false);
|
const [searchOpen, setSearchOpen] = useState(false);
|
||||||
const { setElement: setPageActionsElement, setPresenceElement } = usePageActionsSlot();
|
const { setElement: setPageActionsElement, setPresenceElement } = usePageActionsSlot();
|
||||||
|
// Pond-settings shortcut next to the pond name (M10 follow-up): owners get
|
||||||
|
// a gear icon while a pond route is active. Shares the sidebar's query key.
|
||||||
|
const { pondSlug } = useCurrentPondRoute();
|
||||||
|
const pond = useQuery({
|
||||||
|
queryKey: ['pond', pondSlug],
|
||||||
|
queryFn: () => apiGet<PondView>(`/ponds/${pondSlug}`),
|
||||||
|
enabled: Boolean(user && pondSlug),
|
||||||
|
});
|
||||||
|
const isPondOwner = Boolean(user && pond.data && user.id === pond.data.ownerId);
|
||||||
|
|
||||||
async function handleLogout(): Promise<void> {
|
async function handleLogout(): Promise<void> {
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
@ -63,6 +76,16 @@ export function TopBar({ sidebarCollapsed, onToggleSidebar }: TopBarProps): Reac
|
|||||||
Dorfteich
|
Dorfteich
|
||||||
</Link>
|
</Link>
|
||||||
{user && <PondSwitcher />}
|
{user && <PondSwitcher />}
|
||||||
|
{isPondOwner && pondSlug && (
|
||||||
|
<Link
|
||||||
|
to={`/p/${pondSlug}/settings`}
|
||||||
|
className="icon-button topbar__pond-settings"
|
||||||
|
aria-label={t('labels:link')}
|
||||||
|
title={t('labels:link')}
|
||||||
|
>
|
||||||
|
<Settings aria-hidden />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<span className="topbar__spacer" />
|
<span className="topbar__spacer" />
|
||||||
{/* Page-scoped slots, rendered only for signed-in users: live presence
|
{/* Page-scoped slots, rendered only for signed-in users: live presence
|
||||||
(#102) and the page's icon actions (#101) portal in while a page
|
(#102) and the page's icon actions (#101) portal in while a page
|
||||||
|
|||||||
@ -88,6 +88,9 @@ button {
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
width: var(--sidebar-width);
|
width: var(--sidebar-width);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
/* Column flex so the trash footer can pin to the bottom (margin-top: auto). */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
border-right: 1px solid var(--color-border);
|
border-right: 1px solid var(--color-border);
|
||||||
background: var(--color-bg-subtle);
|
background: var(--color-bg-subtle);
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
@ -176,6 +179,9 @@ button {
|
|||||||
|
|
||||||
.sidebar__new-page {
|
.sidebar__new-page {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
/* The sidebar is a flex column now; keep the stretched button reading
|
||||||
|
like the left-aligned link it used to be. */
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__new-page-form {
|
.sidebar__new-page-form {
|
||||||
@ -1447,8 +1453,9 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__settings-link {
|
.sidebar__footer {
|
||||||
font-size: 0.85rem;
|
margin-top: auto;
|
||||||
|
padding-top: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pond settings page + label manager. */
|
/* Pond settings page + label manager. */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user