dorfteich/apps/web/src/layout/NewPageForm.tsx
Claude Fable 5 15184876bd
Some checks failed
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
Sidebar folder view, label view, and the view toggle (#108)
The sidebar now presents pages as a collapsible tree built from parentId
(folder view) or grouped under the hierarchical label tree (label view,
read-only; multi-label pages appear under each label, untagged ones in
an 'unlabeled' group). The pond owner sets the default via a new
sidebarView pond setting (PATCH-merged like the other keys); every user
can override it locally (ui.sidebar.view.<pondId>), and the toggle sits
above the page list. Collapse state persists per pond.

New pages created while a page is open become its children — the inline
form says so and sends parentId. Reordering (buttons and drag-between)
now operates within one sibling group; the label filter stays a
folder-view feature and falls back to the flat list while active, so
the filtered order is never mistaken for a partial tree.

SidebarContent is keyed by pond id so the per-pond localStorage hooks
mount with the right key. e2e hooks (.sidebar__pages, .sidebar__page,
reorder buttons) kept; reorder/labels/content packs green locally, plus
a live smoke of nesting, collapse persistence, and both views.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:17:36 +02:00

76 lines
2.4 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod';
import { CreatePageInput, PageView, createPageInputSchema } from '@dorfteich/shared';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Field, FormError } from '../components/forms';
import { apiPost } from '../lib/api';
interface NewPageFormProps {
pondId: string;
pondSlug: string;
/** Parent for the new page (issue #108): the currently open page, so new
* pages nest under where the user is; `null` creates at the root. */
parentId?: string | null;
parentTitle?: string;
onCreated: () => void;
onCancel: () => void;
}
/** Inline "new page" prompt: title → create → editor opens (issue #26). */
export function NewPageForm({
pondId,
pondSlug,
parentId = null,
parentTitle,
onCreated,
onCancel,
}: NewPageFormProps): React.JSX.Element {
const { t } = useTranslation();
const navigate = useNavigate();
const [error, setError] = useState<unknown>(null);
const form = useForm<CreatePageInput>({ resolver: zodResolver(createPageInputSchema) });
const onSubmit = form.handleSubmit(async (input) => {
setError(null);
try {
const page = await apiPost<PageView>(`/ponds/${pondId}/pages`, { ...input, parentId });
onCreated();
navigate(`/p/${pondSlug}/${page.slug}`);
} catch (err) {
setError(err);
}
});
return (
<form className="sidebar__new-page-form" onSubmit={(event) => void onSubmit(event)} noValidate>
<FormError error={error} />
<Field label={t('layout.sidebar.newPageTitle')} error={form.formState.errors.title?.message}>
<input
type="text"
autoFocus
{...form.register('title')}
onKeyDown={(event) => {
if (event.key === 'Escape') onCancel();
}}
/>
</Field>
{parentId && parentTitle && (
<p className="sidebar__new-page-parent">
{t('layout.sidebar.newPageUnder', { title: parentTitle })}
</p>
)}
<div className="sidebar__new-page-actions">
<button type="submit" className="button" disabled={form.formState.isSubmitting}>
{t('layout.sidebar.create')}
</button>
<button type="button" className="linklike" onClick={onCancel}>
{t('layout.sidebar.cancel')}
</button>
</div>
</form>
);
}