feat: add date systems menu to calendar days
Right-clicking a calendar day now lists every configured date-prefixed note system. Journal notes are singletons; {title} formats support multiple notes per date, such as meetings.\n\nAdds default Journal and Meeting systems, configurable settings UI, safe filename/title handling, one shared dated-note creation path, and a compiled menu smoke test fixture in the test vault.
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
type PaneType,
|
||||
} from 'obsidian';
|
||||
import type WaypointPlugin from 'src/main';
|
||||
import type { DateSystemNotes } from 'src/main';
|
||||
import { getMonthGrid } from 'src/utils/date-utils';
|
||||
import { BookmarkItem } from 'src/models/bookmark';
|
||||
|
||||
@@ -224,6 +225,11 @@ export class WaypointView extends ItemView {
|
||||
this.plugin.openPeriodNote('day', day.date, this.app.workspace.getLeaf('tab'));
|
||||
}
|
||||
});
|
||||
cell.addEventListener('contextmenu', (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.showDayContextMenu(event, day.date);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +242,79 @@ export class WaypointView extends ItemView {
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
/**
|
||||
* Right-click menu for a day cell: every date system's existing notes for
|
||||
* that date, plus the actions that would create the ones it is missing.
|
||||
*/
|
||||
private showDayContextMenu(event: MouseEvent, date: moment.Moment): void {
|
||||
const menu = new Menu();
|
||||
|
||||
menu.addItem((i) => i.setTitle(date.format('dddd, MMMM D, YYYY')).setIsLabel(true));
|
||||
|
||||
for (const bucket of this.plugin.findDateSystemNotes(date)) {
|
||||
// Many-per-date systems can always take another note; one-per-date
|
||||
// systems only offer creation while their single note is missing.
|
||||
const canCreate = bucket.multiple || bucket.notes.length === 0;
|
||||
if (bucket.notes.length + (canCreate ? 1 : 0) === 0) continue;
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
for (const note of bucket.notes) {
|
||||
menu.addItem((i) =>
|
||||
i
|
||||
.setTitle(note.label)
|
||||
.setIcon(bucket.system.icon)
|
||||
.onClick((evt) => this.focusFile(note.file, Keymap.isModEvent(evt))),
|
||||
);
|
||||
}
|
||||
|
||||
if (!canCreate) continue;
|
||||
|
||||
const noun = bucket.system.name.toLowerCase();
|
||||
menu.addItem((i) =>
|
||||
i
|
||||
// The ellipsis promises the prompt that a free-text title needs.
|
||||
.setTitle(bucket.multiple ? `New ${noun} note…` : `New ${noun} note`)
|
||||
.setIcon('plus')
|
||||
.onClick((evt) => this.createDateSystemNote(bucket, date, Keymap.isModEvent(evt))),
|
||||
);
|
||||
}
|
||||
|
||||
menu.showAtPosition({ x: event.clientX, y: event.clientY });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a date system's note, asking for a title first when the system
|
||||
* holds many notes per date. A held modifier opens the result in a new pane.
|
||||
*/
|
||||
private createDateSystemNote(bucket: DateSystemNotes, date: moment.Moment, newLeaf: PaneType | boolean): void {
|
||||
if (!bucket.multiple) {
|
||||
void this.plugin.openDateSystemNote(bucket.system, date, {
|
||||
leaf: newLeaf ? this.app.workspace.getLeaf(newLeaf) : undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const name = bucket.system.name;
|
||||
new PromptModal(
|
||||
this.app,
|
||||
{
|
||||
title: `New ${name.toLowerCase()} note`,
|
||||
placeholder: `${name} with…`,
|
||||
cta: 'Create',
|
||||
},
|
||||
(title) => {
|
||||
// `getLeaf` materialises the pane immediately, so the target pane is
|
||||
// resolved only after the title is confirmed — cancelling the prompt
|
||||
// must not leave an empty tab behind.
|
||||
void this.plugin.openDateSystemNote(bucket.system, date, {
|
||||
title,
|
||||
leaf: newLeaf ? this.app.workspace.getLeaf(newLeaf) : undefined,
|
||||
});
|
||||
},
|
||||
).open();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════
|
||||
// Recent Files panel
|
||||
// ════════════════════════════════════════
|
||||
@@ -1062,7 +1141,7 @@ export class WaypointView extends ItemView {
|
||||
}
|
||||
|
||||
private promptRename(item: BookmarkItem): void {
|
||||
new RenameModal(this.app, item.label, (newLabel) => {
|
||||
new PromptModal(this.app, { title: 'Rename bookmark', initialValue: item.label }, (newLabel) => {
|
||||
if (newLabel && newLabel.trim()) {
|
||||
this.plugin.updateBookmark(item.id, { label: newLabel.trim() });
|
||||
}
|
||||
@@ -1077,24 +1156,34 @@ export class WaypointView extends ItemView {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rename modal ──
|
||||
// ── Text prompt modal ──
|
||||
|
||||
class RenameModal extends Modal {
|
||||
private currentValue: string;
|
||||
interface PromptModalOptions {
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
initialValue?: string;
|
||||
/** Submit button label. Defaults to 'Save'. */
|
||||
cta?: string;
|
||||
}
|
||||
|
||||
/** Single-line text prompt, shared by bookmark renaming and note creation. */
|
||||
class PromptModal extends Modal {
|
||||
private options: PromptModalOptions;
|
||||
private onSubmit: (value: string) => void;
|
||||
|
||||
constructor(app: App, currentValue: string, onSubmit: (value: string) => void) {
|
||||
constructor(app: App, options: PromptModalOptions, onSubmit: (value: string) => void) {
|
||||
super(app);
|
||||
this.currentValue = currentValue;
|
||||
this.options = options;
|
||||
this.onSubmit = onSubmit;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.titleEl.setText('Rename bookmark');
|
||||
this.titleEl.setText(this.options.title);
|
||||
|
||||
const input = this.contentEl.createEl('input', {
|
||||
type: 'text',
|
||||
value: this.currentValue,
|
||||
value: this.options.initialValue ?? '',
|
||||
placeholder: this.options.placeholder ?? '',
|
||||
});
|
||||
input.style.width = '100%';
|
||||
input.style.marginBottom = '12px';
|
||||
@@ -1107,7 +1196,7 @@ class RenameModal extends Modal {
|
||||
cancelBtn.style.marginRight = '8px';
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
|
||||
const saveBtn = btnContainer.createEl('button', { text: 'Save', cls: 'mod-cta' });
|
||||
const saveBtn = btnContainer.createEl('button', { text: this.options.cta ?? 'Save', cls: 'mod-cta' });
|
||||
saveBtn.addEventListener('click', () => {
|
||||
this.onSubmit(input.value);
|
||||
this.close();
|
||||
|
||||
Reference in New Issue
Block a user