onlyworlds_obsidian-plugin/Modals/FieldSelectionModal.ts
Titus 4a0ec0f395 Link picker: toggle list (add AND remove) + search box + shows existing
The picker was add-only — calling it 'Edit' but never letting you unlink was
the real wrong. Reworked ElementSelectionModal into a toggle list: every
element of the type shown, already-linked ones pre-checked, click toggles
link/unlink, checked-first ordering, search box for long lists. Multi-link
confirms a set on Done; single-link is radio-style (pick sets + closes) with a
Clear-link option to unset. NoteLinker computes current links (resolving
[[Name]] via the path-aware resolver) to pre-check, and writeLink now REPLACES
the field with exactly the chosen set instead of merge-only.

Gate: tsc clean, build clean, 60/60 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:27:59 +02:00

74 lines
2.2 KiB
TypeScript

import { App, Modal, Setting } from 'obsidian';
export interface LinkFieldChoice {
/** frontmatter key, e.g. "location", "traits" */
key: string;
/** display label, e.g. "Location", "Traits" */
label: string;
/** true for multi_link (YAML list), false for single_link (string) */
multi: boolean;
/** target element type (lowercase singular), e.g. "location" */
target: string;
/** how many links this field currently holds on the note (0 = empty) */
count: number;
}
/**
* Step 1 of the Link Elements flow (Phase B): choose which link FIELD to fill.
* The element picker (ElementSelectionModal) follows for the chosen field.
*/
export class FieldSelectionModal extends Modal {
private category: string;
private fields: LinkFieldChoice[];
private onChoose: (choice: LinkFieldChoice) => void;
constructor(
app: App,
category: string,
fields: LinkFieldChoice[],
onChoose: (choice: LinkFieldChoice) => void
) {
super(app);
this.category = category;
this.fields = fields;
this.onChoose = onChoose;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl('h2', { text: `Link a field on this ${this.category}` });
// Single links first, then multi; within each, empty fields before filled
// (you usually want to fill a gap), then alphabetical.
const sorted = [...this.fields].sort((a, b) => {
if (a.multi !== b.multi) return a.multi ? 1 : -1;
if ((a.count === 0) !== (b.count === 0)) return a.count === 0 ? -1 : 1;
return a.label.localeCompare(b.label);
});
for (const field of sorted) {
const kind = field.multi ? 'Multi' : 'Single';
const state = field.count > 0
? `${field.count} linked`
: 'empty';
const setting = new Setting(contentEl)
.setName(field.label)
.setDesc(`${kind}${field.target} · ${state}`)
.addButton((b) =>
b.setButtonText(field.count > 0 ? 'Edit' : 'Add').setTooltip(
field.count > 0 ? 'Add or remove links' : 'Add links'
).onClick(() => {
this.close();
this.onChoose(field);
})
);
// Dim fields that already have links so empty ones stand out.
if (field.count > 0) setting.nameEl.style.opacity = '0.7';
}
}
onClose() {
this.contentEl.empty();
}
}