Pond lifecycle in the UI: create shared ponds, delete from settings
Some checks failed
CD / Build and push images (push) Successful in 3m54s
CD / Deploy to Test (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m8s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Failing after 11s
CD / Promote to Int (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m40s
CI / Import/export fidelity gate (push) Successful in 54s
Some checks failed
CD / Build and push images (push) Successful in 3m54s
CD / Deploy to Test (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m8s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Failing after 11s
CD / Promote to Int (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m40s
CI / Import/export fidelity gate (push) Successful in 54s
The pond switcher grows a "+ New pond" entry with an inline form (name + optional description, quota errors surfaced translated); the pond settings of shared ponds end in a danger section that moves the pond to the site-level trash after typing its name to confirm. Personal ponds keep hiding the section. .button--danger is now a solid red button (also fixes the admin restore button, which showed red text on the accent-green background). Manuals no longer call these actions API-only; covered by a members-pack e2e test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
This commit is contained in:
parent
0c9d44e9c9
commit
627f128ab8
@ -98,3 +98,41 @@ test('non-admin members see the member list read-only', async ({ browser }) => {
|
|||||||
await owner.close();
|
await owner.close();
|
||||||
await member.close();
|
await member.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shared ponds are created from the switcher and deleted from settings', async ({
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
const owner = await contextForUser(browser, BASE_URL, 'fixture-user');
|
||||||
|
const page = await owner.newPage();
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Create via the "+ New pond" flow in the pond switcher.
|
||||||
|
const pondName = `Switcher ${Date.now()}`;
|
||||||
|
// The switcher only mounts once the ponds query resolves — wait for its
|
||||||
|
// dedicated class instead of grabbing the first generic menu trigger.
|
||||||
|
await page.locator('.pond-switcher__trigger').click();
|
||||||
|
await page.locator('.pond-switcher__create').click();
|
||||||
|
await page.locator('.pond-switcher__form input').first().fill(pondName);
|
||||||
|
await page.locator('.pond-switcher__actions button[type="submit"]').click();
|
||||||
|
// Creation lands on the new pond's home.
|
||||||
|
await expect(page).toHaveURL(/\/p\/switcher-/);
|
||||||
|
|
||||||
|
// Delete from the danger section — the button stays disabled until the
|
||||||
|
// typed confirmation matches the pond name exactly.
|
||||||
|
const slug = new URL(page.url()).pathname.split('/')[2];
|
||||||
|
await page.goto(`/p/${slug}/settings`);
|
||||||
|
const danger = page.locator('.pond-delete');
|
||||||
|
await expect(danger).toBeVisible();
|
||||||
|
await expect(danger.locator('.pond-delete__submit')).toBeDisabled();
|
||||||
|
await danger.locator('input').fill(pondName);
|
||||||
|
await danger.locator('.pond-delete__submit').click();
|
||||||
|
await expect(page).toHaveURL(/\/$|\/p\/fixture-user/);
|
||||||
|
expect((await owner.request.get(`/api/v1/ponds/${slug}`)).status()).toBe(404);
|
||||||
|
|
||||||
|
// The personal pond never offers deletion.
|
||||||
|
await page.goto('/p/fixture-user/settings');
|
||||||
|
await expect(page.locator('.pond-export')).toBeVisible();
|
||||||
|
await expect(page.locator('.pond-delete')).toHaveCount(0);
|
||||||
|
|
||||||
|
await owner.close();
|
||||||
|
});
|
||||||
|
|||||||
@ -1,22 +1,42 @@
|
|||||||
import type { PondView } from '@dorfteich/shared';
|
import type { PondView } from '@dorfteich/shared';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } 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 { FormError } from '../components/forms';
|
||||||
|
import { apiGet, apiPost } from '../lib/api';
|
||||||
import { useDismissable } from '../lib/use-dismissable';
|
import { useDismissable } from '../lib/use-dismissable';
|
||||||
import { useCurrentPondRoute } from './use-pond-route';
|
import { useCurrentPondRoute } from './use-pond-route';
|
||||||
|
|
||||||
/** Top-bar dropdown to switch between the signed-in user's ponds (issue #26). */
|
/**
|
||||||
|
* Top-bar dropdown to switch between the signed-in user's ponds (issue #26)
|
||||||
|
* — and to create a new shared pond right from the menu. Creation is
|
||||||
|
* quota-gated server-side (`additional shared ponds per user`); a rejected
|
||||||
|
* attempt surfaces the translated error instead of hiding the option.
|
||||||
|
*/
|
||||||
export function PondSwitcher(): React.JSX.Element | null {
|
export function PondSwitcher(): React.JSX.Element | null {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { pondSlug } = useCurrentPondRoute();
|
const { pondSlug } = useCurrentPondRoute();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [error, setError] = useState<unknown>(null);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
useDismissable(menuRef, open, () => setOpen(false));
|
|
||||||
|
const close = (): void => {
|
||||||
|
setOpen(false);
|
||||||
|
setCreating(false);
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
useDismissable(menuRef, open, close);
|
||||||
|
|
||||||
const ponds = useQuery({
|
const ponds = useQuery({
|
||||||
queryKey: ['ponds'],
|
queryKey: ['ponds'],
|
||||||
queryFn: () => apiGet<PondView[]>('/ponds'),
|
queryFn: () => apiGet<PondView[]>('/ponds'),
|
||||||
@ -26,32 +46,98 @@ export function PondSwitcher(): React.JSX.Element | null {
|
|||||||
if (!ponds.data || ponds.data.length === 0) return null;
|
if (!ponds.data || ponds.data.length === 0) return null;
|
||||||
const current = ponds.data.find((pond) => pond.slug === pondSlug);
|
const current = ponds.data.find((pond) => pond.slug === pondSlug);
|
||||||
|
|
||||||
|
const create = async (event: React.FormEvent): Promise<void> => {
|
||||||
|
event.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const pond = await apiPost<PondView>('/ponds', {
|
||||||
|
name: name.trim(),
|
||||||
|
description: description.trim(),
|
||||||
|
});
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['ponds'] });
|
||||||
|
close();
|
||||||
|
setName('');
|
||||||
|
setDescription('');
|
||||||
|
navigate(`/p/${pond.slug}`);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user-menu" ref={menuRef}>
|
<div className="user-menu" ref={menuRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="user-menu__trigger"
|
className="user-menu__trigger pond-switcher__trigger"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
aria-label={t('layout.pondSwitcher.label')}
|
aria-label={t('layout.pondSwitcher.label')}
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => (open ? close() : setOpen(true))}
|
||||||
>
|
>
|
||||||
{current?.name ?? t('layout.pondSwitcher.trigger')}
|
{current?.name ?? t('layout.pondSwitcher.trigger')}
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && !creating && (
|
||||||
<div className="user-menu__list" role="menu">
|
<div className="user-menu__list" role="menu">
|
||||||
{ponds.data.map((pond) => (
|
{ponds.data.map((pond) => (
|
||||||
<Link
|
<Link key={pond.id} role="menuitem" to={`/p/${pond.slug}`} onClick={close}>
|
||||||
key={pond.id}
|
|
||||||
role="menuitem"
|
|
||||||
to={`/p/${pond.slug}`}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
{pond.name}
|
{pond.name}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="menuitem"
|
||||||
|
className="pond-switcher__create"
|
||||||
|
onClick={() => setCreating(true)}
|
||||||
|
>
|
||||||
|
{t('pond.create.menuItem')}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{open && creating && (
|
||||||
|
<form className="user-menu__list pond-switcher__form" onSubmit={(e) => void create(e)}>
|
||||||
|
<h3>{t('pond.create.title')}</h3>
|
||||||
|
<FormError error={error} />
|
||||||
|
<label>
|
||||||
|
{t('pond.create.nameLabel')}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
required
|
||||||
|
maxLength={80}
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
{t('pond.create.descriptionLabel')}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={description}
|
||||||
|
maxLength={500}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<p className="pond-switcher__hint">{t('pond.create.quotaHint')}</p>
|
||||||
|
<div className="pond-switcher__actions">
|
||||||
|
<button type="submit" className="button" disabled={busy || name.trim() === ''}>
|
||||||
|
{t('pond.create.submit')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button button--outline"
|
||||||
|
onClick={() => {
|
||||||
|
setCreating(false);
|
||||||
|
setError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('pond.create.cancel')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { EffectivePermissionsInspector } from '../access/EffectivePermissionsIns
|
|||||||
import { PondFileManager } from '../files/PondFileManager';
|
import { PondFileManager } from '../files/PondFileManager';
|
||||||
import { apiGet } from '../lib/api';
|
import { apiGet } from '../lib/api';
|
||||||
import { MemberManager } from '../members/MemberManager';
|
import { MemberManager } from '../members/MemberManager';
|
||||||
|
import { DeletePondSection } from '../ponds/DeletePondSection';
|
||||||
import { PondPluginSettings } from '../plugins/PondPluginSettings';
|
import { PondPluginSettings } from '../plugins/PondPluginSettings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -126,6 +127,9 @@ export function PondSettingsPage(): React.JSX.Element {
|
|||||||
{tExport('pond.zip')}
|
{tExport('pond.zip')}
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
{canModify && pond.data.type === 'shared' && (
|
||||||
|
<DeletePondSection pondId={pond.data.id} pondName={pond.data.name} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
69
apps/web/src/ponds/DeletePondSection.tsx
Normal file
69
apps/web/src/ponds/DeletePondSection.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { FormError } from '../components/forms';
|
||||||
|
import { apiDelete } from '../lib/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pond's danger zone: move a shared pond to the site-level trash
|
||||||
|
* (`DELETE /ponds/:id`, Pond-Admin-gated in the api; a site admin can
|
||||||
|
* restore it). Type-to-confirm with the pond name — the same backstop
|
||||||
|
* pattern as the backup restore. Personal ponds never render this
|
||||||
|
* section: they are the account's home and the api refuses anyway.
|
||||||
|
*/
|
||||||
|
export function DeletePondSection({
|
||||||
|
pondId,
|
||||||
|
pondName,
|
||||||
|
}: {
|
||||||
|
pondId: string;
|
||||||
|
pondName: string;
|
||||||
|
}): React.JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [confirm, setConfirm] = useState('');
|
||||||
|
const [error, setError] = useState<unknown>(null);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
const remove = async (event: React.FormEvent): Promise<void> => {
|
||||||
|
event.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await apiDelete(`/ponds/${pondId}`);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['ponds'] });
|
||||||
|
navigate('/');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="pond-delete">
|
||||||
|
<h2>{t('pond.delete.title')}</h2>
|
||||||
|
<p className="pond-delete__hint">{t('pond.delete.hint')}</p>
|
||||||
|
<form onSubmit={(e) => void remove(e)}>
|
||||||
|
<FormError error={error} />
|
||||||
|
<label>
|
||||||
|
{t('pond.delete.confirmLabel', { name: pondName })}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={confirm}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={(e) => setConfirm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="button button--danger pond-delete__submit"
|
||||||
|
disabled={busy || confirm !== pondName}
|
||||||
|
>
|
||||||
|
{t('pond.delete.submit')}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2627,7 +2627,81 @@ button {
|
|||||||
|
|
||||||
.button--danger {
|
.button--danger {
|
||||||
border-color: var(--color-danger);
|
border-color: var(--color-danger);
|
||||||
color: var(--color-danger);
|
background: var(--color-danger);
|
||||||
|
color: var(--color-accent-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--danger:hover:not(:disabled) {
|
||||||
|
background: #8a0718;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pond creation form in the switcher + pond danger zone */
|
||||||
|
.pond-switcher__create {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
background: none;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pond-switcher__form {
|
||||||
|
padding: var(--space-3);
|
||||||
|
min-width: 18rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pond-switcher__form label {
|
||||||
|
display: block;
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pond-switcher__form input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pond-switcher__hint {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pond-switcher__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pond-delete {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
padding-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pond-delete__hint {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pond-delete label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pond-delete input {
|
||||||
|
display: block;
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
width: min(24rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pond-delete__submit {
|
||||||
|
margin-top: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* API tokens (issue #104) */
|
/* API tokens (issue #104) */
|
||||||
|
|||||||
@ -8,8 +8,9 @@ bar (visible on pond routes when you may modify the pond).
|
|||||||
## Ponds in one minute
|
## Ponds in one minute
|
||||||
|
|
||||||
Every member gets a **personal pond** automatically. Additional
|
Every member gets a **personal pond** automatically. Additional
|
||||||
**shared ponds** are created through the API (`POST /api/v1/ponds`) and
|
**shared ponds** are created via **"+ New pond"** at the bottom of the
|
||||||
are subject to the per-user quota the site admin sets ("additional
|
pond switcher in the top bar (or through the API, `POST /api/v1/ponds`)
|
||||||
|
and are subject to the per-user quota the site admin sets ("additional
|
||||||
shared ponds per user", default 0). The creator becomes the pond admin.
|
shared ponds per user", default 0). The creator becomes the pond admin.
|
||||||
|
|
||||||
## Name, description, appearance
|
## Name, description, appearance
|
||||||
@ -98,7 +99,7 @@ against the pond's quota; the current usage is shown.
|
|||||||
## Export and deletion
|
## Export and deletion
|
||||||
|
|
||||||
- **Export**: the whole pond as a ZIP of Markdown files plus media.
|
- **Export**: the whole pond as a ZIP of Markdown files plus media.
|
||||||
- **Delete pond**: shared ponds can be moved to the site-level trash by
|
- **Delete pond**: the danger section at the bottom of the pond
|
||||||
their admin via the API (`DELETE /api/v1/ponds/<id>` — there is no UI
|
settings moves a shared pond to the site-level trash (type the pond
|
||||||
button yet); a site admin can restore them. Your personal pond cannot
|
name to confirm); a site admin can restore it. Your personal pond
|
||||||
be deleted — it is your account's home.
|
cannot be deleted — it is your account's home.
|
||||||
|
|||||||
@ -19,7 +19,8 @@ instance administration the [site-admin guide](site-admin-guide.md).
|
|||||||
## Ponds and pages
|
## Ponds and pages
|
||||||
|
|
||||||
- The **pond switcher** in the top bar moves you between the ponds you
|
- The **pond switcher** in the top bar moves you between the ponds you
|
||||||
can see. The **sidebar** lists the pages of the current pond — sort
|
can see — and **"+ New pond"** at its bottom creates a new shared pond
|
||||||
|
(subject to a quota the site admin sets). The **sidebar** lists the pages of the current pond — sort
|
||||||
them A–Z, by creation date, or drag them into a manual order (the sort
|
them A–Z, by creation date, or drag them into a manual order (the sort
|
||||||
mode is a pond setting).
|
mode is a pond setting).
|
||||||
- **+ New page** at the bottom of the sidebar creates a page. Page
|
- **+ New page** at the bottom of the sidebar creates a page. Page
|
||||||
|
|||||||
@ -48,5 +48,23 @@
|
|||||||
"title": "Seite nicht gefunden",
|
"title": "Seite nicht gefunden",
|
||||||
"body": "Die aufgerufene Adresse existiert nicht.",
|
"body": "Die aufgerufene Adresse existiert nicht.",
|
||||||
"home": "Zurück zur Startseite"
|
"home": "Zurück zur Startseite"
|
||||||
|
},
|
||||||
|
"pond": {
|
||||||
|
"create": {
|
||||||
|
"menuItem": "+ Neuer Teich",
|
||||||
|
"title": "Gemeinsamen Teich anlegen",
|
||||||
|
"nameLabel": "Name",
|
||||||
|
"descriptionLabel": "Beschreibung (optional)",
|
||||||
|
"submit": "Teich anlegen",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"quotaHint": "Gemeinsame Teiche zählen gegen dein Kontingent — frag eine Site-Adminin/einen Site-Admin, falls das Anlegen abgelehnt wird."
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"title": "Teich löschen",
|
||||||
|
"hint": "Verschiebt diesen Teich mit allen Seiten in den Instanz-Papierkorb. Ein Site-Admin kann ihn wiederherstellen. Dein persönlicher Teich kann nicht gelöscht werden.",
|
||||||
|
"confirmLabel": "Zur Bestätigung den Teichnamen eintippen: {{name}}",
|
||||||
|
"submit": "Teich löschen",
|
||||||
|
"deleted": "Teich gelöscht."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,5 +48,23 @@
|
|||||||
"title": "Page not found",
|
"title": "Page not found",
|
||||||
"body": "The address you opened does not exist.",
|
"body": "The address you opened does not exist.",
|
||||||
"home": "Back to the start page"
|
"home": "Back to the start page"
|
||||||
|
},
|
||||||
|
"pond": {
|
||||||
|
"create": {
|
||||||
|
"menuItem": "+ New pond",
|
||||||
|
"title": "Create a shared pond",
|
||||||
|
"nameLabel": "Name",
|
||||||
|
"descriptionLabel": "Description (optional)",
|
||||||
|
"submit": "Create pond",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"quotaHint": "Shared ponds count against your quota — ask a site admin if creation is rejected."
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"title": "Delete pond",
|
||||||
|
"hint": "Moves this pond with all of its pages to the site-level trash. A site admin can restore it. Your personal pond cannot be deleted.",
|
||||||
|
"confirmLabel": "Type the pond name to confirm: {{name}}",
|
||||||
|
"submit": "Delete pond",
|
||||||
|
"deleted": "Pond deleted."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user