Vault import: always show the label section, create labels inline
The label multiselect was gated on the pond already having labels — but before a first import that is the common case, so the section silently vanished and no import-wide label could be chosen. Render the fieldset unconditionally (with a hint when empty) and add an inline create field: POST the new label directly to get its id back, refresh the shared label query, and tick it right away. Same pond_admin permission as the dialog itself. The e2e pack now creates its label through the dialog instead of the API, covering exactly the empty-pond path that slipped through. Fixes #121 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn
This commit is contained in:
parent
e356d82d51
commit
b8546eb249
@ -50,11 +50,7 @@ test('a pond admin imports an Obsidian vault through the settings dialog', async
|
||||
`/api/v1/ponds/${pond.id}/pages`,
|
||||
{ title: `Vault Mount ${ts}` },
|
||||
);
|
||||
const label = await json<{ id: string; name: string }>(
|
||||
context,
|
||||
`/api/v1/ponds/${pond.id}/labels`,
|
||||
{ name: `import-${ts}` },
|
||||
);
|
||||
const labelName = `import-${ts}`;
|
||||
|
||||
const page = await context.newPage();
|
||||
await page.goto(`/p/${pond.slug}/settings`);
|
||||
@ -72,15 +68,16 @@ test('a pond admin imports an Obsidian vault through the settings dialog', async
|
||||
mimeType: 'application/zip',
|
||||
buffer: fixtureZip(),
|
||||
});
|
||||
// Mount under the prepared page and tag everything with the extra label.
|
||||
// Mount under the prepared page. The pond has no labels yet — the label
|
||||
// section must still be there (#121) and let the admin create one inline;
|
||||
// the fresh label comes back pre-ticked.
|
||||
await dialog
|
||||
.locator('li', { hasText: `Vault Mount ${ts}` })
|
||||
.locator('input')
|
||||
.check();
|
||||
await dialog
|
||||
.locator('li', { hasText: `import-${ts}` })
|
||||
.locator('input')
|
||||
.check();
|
||||
await dialog.locator('.vault-import-dialog__label-create input').fill(labelName);
|
||||
await dialog.locator('.vault-import-dialog__label-create-submit').click();
|
||||
await expect(dialog.locator('li', { hasText: labelName }).locator('input')).toBeChecked();
|
||||
await dialog.locator('.vault-import-dialog__start').click();
|
||||
|
||||
// The job runs; the dialog offers the way into the imported pages.
|
||||
@ -111,7 +108,7 @@ test('a pond admin imports an Obsidian vault through the settings dialog', async
|
||||
const tree = JSON.stringify(await labels.json());
|
||||
expect(tree).toContain('status');
|
||||
expect(tree).toContain('aktiv');
|
||||
expect(tree).toContain(label.name);
|
||||
expect(tree).toContain(labelName);
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type {
|
||||
ConversionJobView,
|
||||
LabelView,
|
||||
PageListItemView,
|
||||
PageView,
|
||||
PondView,
|
||||
@ -14,7 +15,7 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import { FormError } from '../components/forms';
|
||||
import { labelsKey, usePondLabels } from '../labels/use-pond-labels';
|
||||
import { apiGet, apiUploadFile } from '../lib/api';
|
||||
import { ApiError, apiGet, apiPost, apiUploadFile } from '../lib/api';
|
||||
import { useDismissable } from '../lib/use-dismissable';
|
||||
|
||||
const POLL_INTERVAL_MS = 1000;
|
||||
@ -77,6 +78,8 @@ function VaultImportDialog({
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [parentId, setParentId] = useState<string | null>(null);
|
||||
const [labelIds, setLabelIds] = useState<Set<string>>(new Set());
|
||||
const [newLabelName, setNewLabelName] = useState('');
|
||||
const [labelError, setLabelError] = useState<string | null>(null);
|
||||
const [frontmatter, setFrontmatter] = useState<VaultFrontmatterMode>('strip');
|
||||
const running = phase.kind === 'running';
|
||||
useDismissable(dialogRef, !running, onClose);
|
||||
@ -111,6 +114,31 @@ function VaultImportDialog({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a label right in the dialog and tick it (#121): before the first
|
||||
* import a pond usually has no labels at all, so the multiselect alone
|
||||
* would stay empty. POST directly (instead of useLabelMutations) to get
|
||||
* the new id back for auto-selection. Same pond_admin permission as the
|
||||
* dialog itself.
|
||||
*/
|
||||
async function createLabel(): Promise<void> {
|
||||
const name = newLabelName.trim();
|
||||
if (!name) return;
|
||||
setLabelError(null);
|
||||
try {
|
||||
const label = await apiPost<LabelView>(`/ponds/${pondId}/labels`, { name });
|
||||
await queryClient.invalidateQueries({ queryKey: labelsKey(pondId) });
|
||||
toggleLabel(label.id, true);
|
||||
setNewLabelName('');
|
||||
} catch (err) {
|
||||
setLabelError(
|
||||
err instanceof ApiError
|
||||
? tErrors(err.body.code, { defaultValue: err.body.message, ...(err.body.details ?? {}) })
|
||||
: tErrors('internal_error'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function start(): Promise<void> {
|
||||
if (!file) return;
|
||||
setError(null);
|
||||
@ -230,9 +258,11 @@ function VaultImportDialog({
|
||||
</ul>
|
||||
</fieldset>
|
||||
|
||||
{flat.length > 0 && (
|
||||
<fieldset className="vault-import-dialog__field" disabled={running}>
|
||||
<legend>{t('vault.labels')}</legend>
|
||||
{flat.length === 0 ? (
|
||||
<p className="vault-import-dialog__labels-empty">{t('vault.labelsEmpty')}</p>
|
||||
) : (
|
||||
<ul className="move-dialog__options">
|
||||
{flat.map((label) => (
|
||||
<li
|
||||
@ -255,8 +285,37 @@ function VaultImportDialog({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</fieldset>
|
||||
)}
|
||||
<div className="vault-import-dialog__label-create">
|
||||
<input
|
||||
type="text"
|
||||
value={newLabelName}
|
||||
placeholder={t('vault.labelName')}
|
||||
aria-label={t('vault.labelName')}
|
||||
onChange={(event) => setNewLabelName(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
// The dialog holds no <form>; Enter should create, not submit.
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void createLabel();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="button vault-import-dialog__label-create-submit"
|
||||
disabled={!newLabelName.trim()}
|
||||
onClick={() => void createLabel()}
|
||||
>
|
||||
{t('vault.labelCreate')}
|
||||
</button>
|
||||
</div>
|
||||
{labelError && (
|
||||
<p className="form-banner form-banner--error" role="alert">
|
||||
{labelError}
|
||||
</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="vault-import-dialog__field" disabled={running}>
|
||||
<legend>{t('vault.frontmatter')}</legend>
|
||||
|
||||
@ -1853,6 +1853,24 @@ button {
|
||||
max-height: 12rem;
|
||||
}
|
||||
|
||||
.vault-import-dialog__labels-empty {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
margin: 0 0 var(--space-1);
|
||||
}
|
||||
|
||||
/* Inline label creation (#121): input + button side by side. */
|
||||
.vault-import-dialog__label-create {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.vault-import-dialog__label-create input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* "Create this page" on the not-found screen (issue #115). */
|
||||
.create-missing-page {
|
||||
margin-top: var(--space-3);
|
||||
|
||||
@ -19,6 +19,9 @@
|
||||
"parent": "Einhängen unter",
|
||||
"parentRoot": "Oberste Ebene",
|
||||
"labels": "Zusätzliche Labels für alle importierten Seiten",
|
||||
"labelsEmpty": "Dieser Teich hat noch keine Labels — du kannst hier direkt eins anlegen.",
|
||||
"labelName": "Name des neuen Labels",
|
||||
"labelCreate": "Label anlegen",
|
||||
"frontmatter": "YAML-Frontmatter",
|
||||
"frontmatterStrip": "Entfernen",
|
||||
"frontmatterPreserve": "Als Code-Block erhalten",
|
||||
|
||||
@ -19,6 +19,9 @@
|
||||
"parent": "Mount under",
|
||||
"parentRoot": "Top level",
|
||||
"labels": "Additional labels for every imported page",
|
||||
"labelsEmpty": "This pond has no labels yet — you can create one right here.",
|
||||
"labelName": "Name of the new label",
|
||||
"labelCreate": "Create label",
|
||||
"frontmatter": "YAML frontmatter",
|
||||
"frontmatterStrip": "Remove it",
|
||||
"frontmatterPreserve": "Keep it as a code block",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user