onlyworlds_obsidian-plugin/Modals/FieldSelectionModal.ts
Titus 7d36a3f137 Link Elements writes [[wikilinks]] + modal UX (was writing raw ids)
THE bug: NoteLinker.writeLink wrote raw UUIDs into link fields — the one
write path never updated for the 3.0.0 readability change, so linking via
the command produced unreadable uuid fields. Now writes [[Name]] wikilinks
(sanitized to match the on-disk basename), merging by name AND id so an
existing raw-id or wikilink entry isn't duplicated — old raw-id links
self-heal to wikilinks on next edit. Merge logic unit-verified (add / re-add /
raw-id-upgrade / colon-name).

Modal UX (Captain's asks): field picker shows each field's current link count
(empty vs filled), sorts single links first then empty-before-filled, dims
already-filled fields, Add/Edit button label. README notes native Property
autocomplete as a second linking path.

Gate: tsc clean, build clean, 60/60 tests + merge unit-check.

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

72 lines
2.1 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').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();
}
}