mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
refactor: consolidate file selector modals into FileSelectorModal
Create a new unified FileSelectorModal that replaces AttachmentSelectModal and ICSNoteLinkModal. The new modal provides: - Fuzzy search for existing files - Shift+Enter to create new files with typed name - Configurable filter (markdown only, all files, or custom) - Simple footer showing create option with filename preview Updated usages: - TimeblockCreationModal: attachments (filter: all) - TimeblockInfoModal: attachments (filter: all) - ICSEventContextMenu: note linking (filter: markdown) - ICSEventInfoModal: note linking (filter: markdown) Also improved task selector create preview spacing. Removed: - src/modals/AttachmentSelectModal.ts - src/modals/ICSNoteLinkModal.ts
This commit is contained in:
parent
bd632ea401
commit
f03ddbce58
10 changed files with 504 additions and 231 deletions
|
|
@ -19,6 +19,7 @@ const CSS_FILES = [
|
|||
'styles/reminder-modal.css', // Reminder modal component with proper BEM scoping
|
||||
'styles/date-picker.css', // Enhanced date/time picker styling
|
||||
'styles/task-selector-with-create-modal.css', // TaskSelectorWithCreateModal component with proper BEM scoping
|
||||
'styles/file-selector-modal.css', // FileSelectorModal component with proper BEM scoping
|
||||
'styles/unscheduled-tasks-selector-modal.css', // UnscheduledTasksSelectorModal component with proper BEM scoping
|
||||
'styles/task-action-palette-modal.css', // TaskActionPaletteModal component with proper BEM scoping
|
||||
'styles/time-entry-editor-modal.css', // TimeEntryEditorModal component with proper BEM scoping
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import TaskNotesPlugin from "../main";
|
|||
import { ICSEvent } from "../types";
|
||||
import { ICSEventInfoModal } from "../modals/ICSEventInfoModal";
|
||||
import { ICSNoteCreationModal } from "../modals/ICSNoteCreationModal";
|
||||
import { ICSNoteLinkModal } from "../modals/ICSNoteLinkModal";
|
||||
import { openFileSelector } from "../modals/FileSelectorModal";
|
||||
import { SafeAsync } from "../utils/safeAsync";
|
||||
import { ContextMenu } from "./ContextMenu";
|
||||
|
||||
|
|
@ -205,34 +205,34 @@ export class ICSEventContextMenu {
|
|||
private async linkExistingNote(): Promise<void> {
|
||||
await SafeAsync.execute(
|
||||
async () => {
|
||||
const modal = new ICSNoteLinkModal(
|
||||
this.options.plugin.app,
|
||||
this.options.plugin,
|
||||
async (file) => {
|
||||
await SafeAsync.execute(
|
||||
async () => {
|
||||
await this.options.plugin.icsNoteService.linkNoteToICS(
|
||||
file.path,
|
||||
this.options.icsEvent
|
||||
);
|
||||
new Notice(
|
||||
this.t("contextMenus.ics.notices.linkSuccess", {
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
openFileSelector(this.options.plugin, async (file) => {
|
||||
if (!file) return;
|
||||
|
||||
// Trigger update callback if provided
|
||||
if (this.options.onUpdate) {
|
||||
this.options.onUpdate();
|
||||
}
|
||||
},
|
||||
{
|
||||
errorMessage: this.t("contextMenus.ics.notices.linkFailure"),
|
||||
await SafeAsync.execute(
|
||||
async () => {
|
||||
await this.options.plugin.icsNoteService.linkNoteToICS(
|
||||
file.path,
|
||||
this.options.icsEvent
|
||||
);
|
||||
new Notice(
|
||||
this.t("contextMenus.ics.notices.linkSuccess", {
|
||||
name: file.name,
|
||||
})
|
||||
);
|
||||
|
||||
// Trigger update callback if provided
|
||||
if (this.options.onUpdate) {
|
||||
this.options.onUpdate();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
},
|
||||
{
|
||||
errorMessage: this.t("contextMenus.ics.notices.linkFailure"),
|
||||
}
|
||||
);
|
||||
}, {
|
||||
placeholder: "Search notes to link...",
|
||||
filter: "markdown",
|
||||
});
|
||||
},
|
||||
{
|
||||
errorMessage: this.t("contextMenus.ics.notices.linkSelectionFailure"),
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
import {
|
||||
App,
|
||||
FuzzySuggestModal,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
SearchResult,
|
||||
parseFrontMatterAliases,
|
||||
} from "obsidian";
|
||||
import type TaskNotesPlugin from "../main";
|
||||
|
||||
export class AttachmentSelectModal extends FuzzySuggestModal<TAbstractFile> {
|
||||
private onChoose: (file: TAbstractFile) => void;
|
||||
private plugin: TaskNotesPlugin;
|
||||
|
||||
constructor(app: App, plugin: TaskNotesPlugin, onChoose: (file: TAbstractFile) => void) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.onChoose = onChoose;
|
||||
this.setPlaceholder("Type to search for files and notes...");
|
||||
this.setInstructions([
|
||||
{ command: "↑↓", purpose: "to navigate" },
|
||||
{ command: "↵", purpose: "to select" },
|
||||
{ command: "esc", purpose: "to cancel" },
|
||||
]);
|
||||
}
|
||||
|
||||
getItems(): TAbstractFile[] {
|
||||
return this.app.vault.getAllLoadedFiles();
|
||||
}
|
||||
|
||||
getItemText(file: TAbstractFile): string {
|
||||
let text = `${file.name} ${file.path}`;
|
||||
|
||||
// Add aliases to searchable text
|
||||
if (file instanceof TFile) {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
if (cache?.frontmatter) {
|
||||
const aliases = parseFrontMatterAliases(cache.frontmatter);
|
||||
if (aliases && aliases.length > 0) {
|
||||
text += ` ${aliases.join(" ")}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
renderSuggestion(value: { item: TAbstractFile; match: SearchResult }, el: HTMLElement) {
|
||||
const file = value.item;
|
||||
el.empty();
|
||||
|
||||
const container = el.createDiv({ cls: "attachment-suggestion" });
|
||||
|
||||
// File name (main line)
|
||||
const nameEl = container.createSpan({ cls: "attachment-name" });
|
||||
nameEl.textContent = file.name;
|
||||
|
||||
// Title or aliases (second line)
|
||||
if (file instanceof TFile) {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
if (cache?.frontmatter) {
|
||||
const titleField = this.plugin.fieldMapper.toUserField("title");
|
||||
const title = cache.frontmatter[titleField];
|
||||
|
||||
if (title) {
|
||||
const titleEl = container.createDiv({ cls: "attachment-title" });
|
||||
titleEl.textContent = title;
|
||||
} else {
|
||||
const aliases = parseFrontMatterAliases(cache.frontmatter);
|
||||
if (aliases && aliases.length > 0) {
|
||||
const aliasEl = container.createDiv({ cls: "attachment-aliases" });
|
||||
aliasEl.textContent = aliases.join(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File path (if different from name)
|
||||
if (file.path !== file.name) {
|
||||
const pathEl = container.createDiv({ cls: "attachment-path" });
|
||||
pathEl.textContent = file.path;
|
||||
}
|
||||
}
|
||||
|
||||
onChooseItem(file: TAbstractFile, evt: MouseEvent | KeyboardEvent) {
|
||||
this.onChoose(file);
|
||||
}
|
||||
}
|
||||
359
src/modals/FileSelectorModal.ts
Normal file
359
src/modals/FileSelectorModal.ts
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
import {
|
||||
App,
|
||||
SuggestModal,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder,
|
||||
parseFrontMatterAliases,
|
||||
Notice,
|
||||
} from "obsidian";
|
||||
import type TaskNotesPlugin from "../main";
|
||||
|
||||
export type FileSelectorResult =
|
||||
| { type: "selected"; file: TAbstractFile }
|
||||
| { type: "created"; file: TFile }
|
||||
| { type: "cancelled" };
|
||||
|
||||
export interface FileSelectorOptions {
|
||||
/** Callback when a file is selected or created */
|
||||
onResult: (result: FileSelectorResult) => void;
|
||||
/** Optional placeholder text */
|
||||
placeholder?: string;
|
||||
/** Optional title */
|
||||
title?: string;
|
||||
/** Filter to apply to files (default: markdown files only) */
|
||||
filter?: "markdown" | "all" | ((file: TAbstractFile) => boolean);
|
||||
/** Folder to create new files in (default: vault root) */
|
||||
newFileFolder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic file selector modal that allows users to:
|
||||
* 1. Select an existing file from a fuzzy search
|
||||
* 2. Create a new file by pressing Shift+Enter
|
||||
*/
|
||||
export class FileSelectorModal extends SuggestModal<TAbstractFile> {
|
||||
private plugin: TaskNotesPlugin;
|
||||
private options: FileSelectorOptions;
|
||||
private currentQuery: string = "";
|
||||
private resultHandled: boolean = false;
|
||||
private createFooterEl: HTMLElement | null = null;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: TaskNotesPlugin,
|
||||
options: FileSelectorOptions
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.options = options;
|
||||
|
||||
// Set placeholder
|
||||
this.setPlaceholder(
|
||||
options.placeholder || "Search files or type to create new..."
|
||||
);
|
||||
|
||||
// Set instructions
|
||||
this.setInstructions([
|
||||
{ command: "↑↓", purpose: "to navigate" },
|
||||
{ command: "↵", purpose: "to select" },
|
||||
{ command: "⇧↵", purpose: "to create new" },
|
||||
{ command: "esc", purpose: "to cancel" },
|
||||
]);
|
||||
|
||||
// Set modal title
|
||||
if (options.title) {
|
||||
this.titleEl.setText(options.title);
|
||||
}
|
||||
|
||||
// Add classes for styling
|
||||
this.containerEl.addClass("file-selector-modal");
|
||||
this.containerEl.addClass("tasknotes-plugin");
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
super.onOpen();
|
||||
|
||||
// Register Shift+Enter for creating new file
|
||||
this.scope.register(["Shift"], "Enter", (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.createNewFile();
|
||||
return false;
|
||||
});
|
||||
|
||||
// Track input changes for the footer preview
|
||||
this.inputEl.addEventListener("input", this.handleInputChange);
|
||||
|
||||
// Create footer after DOM is ready
|
||||
setTimeout(() => this.createFooter(), 0);
|
||||
}
|
||||
|
||||
private handleInputChange = (): void => {
|
||||
this.currentQuery = this.inputEl.value.trim();
|
||||
this.updateCreateFooter();
|
||||
};
|
||||
|
||||
private createFooter(): void {
|
||||
const promptEl = this.modalEl.querySelector(".prompt");
|
||||
if (!promptEl) return;
|
||||
|
||||
this.createFooterEl = promptEl.parentElement?.createDiv({
|
||||
cls: "file-selector-create-footer",
|
||||
}) || null;
|
||||
|
||||
if (this.createFooterEl) {
|
||||
this.createFooterEl.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
private updateCreateFooter(): void {
|
||||
if (!this.createFooterEl) return;
|
||||
|
||||
if (!this.currentQuery) {
|
||||
this.createFooterEl.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
this.createFooterEl.empty();
|
||||
this.createFooterEl.style.display = "flex";
|
||||
|
||||
// Content
|
||||
const contentDiv = this.createFooterEl.createDiv({
|
||||
cls: "file-selector-create-footer__content",
|
||||
});
|
||||
|
||||
// Title line
|
||||
const titleLine = contentDiv.createDiv({
|
||||
cls: "file-selector-create-footer__title-line",
|
||||
});
|
||||
|
||||
const shortcut = titleLine.createSpan({
|
||||
cls: "file-selector-create-footer__shortcut",
|
||||
text: "⇧↵",
|
||||
});
|
||||
|
||||
titleLine.createSpan({
|
||||
cls: "file-selector-create-footer__hint-text",
|
||||
text: " to create: ",
|
||||
});
|
||||
|
||||
titleLine.createSpan({
|
||||
cls: "file-selector-create-footer__filename",
|
||||
text: this.getNewFileName(),
|
||||
});
|
||||
}
|
||||
|
||||
private getNewFileName(): string {
|
||||
let name = this.currentQuery;
|
||||
// Remove .md extension if user typed it
|
||||
if (name.toLowerCase().endsWith(".md")) {
|
||||
name = name.slice(0, -3);
|
||||
}
|
||||
return name + ".md";
|
||||
}
|
||||
|
||||
private async createNewFile(): Promise<void> {
|
||||
if (!this.currentQuery) {
|
||||
new Notice("Please enter a file name");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let fileName = this.currentQuery;
|
||||
// Remove .md extension if user typed it (we'll add it back)
|
||||
if (fileName.toLowerCase().endsWith(".md")) {
|
||||
fileName = fileName.slice(0, -3);
|
||||
}
|
||||
|
||||
// Determine the folder
|
||||
const folderPath = this.options.newFileFolder || "";
|
||||
const filePath = folderPath
|
||||
? `${folderPath}/${fileName}.md`
|
||||
: `${fileName}.md`;
|
||||
|
||||
// Check if file already exists
|
||||
const existingFile = this.app.vault.getAbstractFileByPath(filePath);
|
||||
if (existingFile) {
|
||||
new Notice(`File "${filePath}" already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure folder exists
|
||||
if (folderPath) {
|
||||
const folder = this.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!folder) {
|
||||
await this.app.vault.createFolder(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the file
|
||||
const newFile = await this.app.vault.create(filePath, "");
|
||||
|
||||
this.resultHandled = true;
|
||||
this.close();
|
||||
this.options.onResult({ type: "created", file: newFile });
|
||||
} catch (error) {
|
||||
console.error("Error creating file:", error);
|
||||
new Notice("Failed to create file");
|
||||
}
|
||||
}
|
||||
|
||||
getSuggestions(query: string): TAbstractFile[] {
|
||||
this.currentQuery = query.trim();
|
||||
this.updateCreateFooter();
|
||||
|
||||
const allFiles = this.app.vault.getAllLoadedFiles();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
||||
// Apply filter
|
||||
let filtered: TAbstractFile[];
|
||||
const filter = this.options.filter || "markdown";
|
||||
|
||||
if (typeof filter === "function") {
|
||||
filtered = allFiles.filter(filter);
|
||||
} else if (filter === "markdown") {
|
||||
filtered = allFiles.filter(
|
||||
(file) =>
|
||||
file instanceof TFile &&
|
||||
file.extension === "md" &&
|
||||
!file.path.includes(".trash")
|
||||
);
|
||||
} else {
|
||||
filtered = allFiles.filter(
|
||||
(file) =>
|
||||
file instanceof TFile && !file.path.includes(".trash")
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by query
|
||||
if (!query) {
|
||||
return filtered.slice(0, 50); // Limit initial results
|
||||
}
|
||||
|
||||
return filtered
|
||||
.filter((file) => {
|
||||
const searchText = this.getSearchText(file).toLowerCase();
|
||||
return searchText.includes(lowerQuery);
|
||||
})
|
||||
.slice(0, 50);
|
||||
}
|
||||
|
||||
private getSearchText(file: TAbstractFile): string {
|
||||
let text = `${file.name} ${file.path}`;
|
||||
|
||||
if (file instanceof TFile) {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
if (cache?.frontmatter) {
|
||||
// Add title
|
||||
const titleField = this.plugin.fieldMapper.toUserField("title");
|
||||
const title = cache.frontmatter[titleField];
|
||||
if (title) {
|
||||
text += ` ${title}`;
|
||||
}
|
||||
|
||||
// Add aliases
|
||||
const aliases = parseFrontMatterAliases(cache.frontmatter);
|
||||
if (aliases && aliases.length > 0) {
|
||||
text += ` ${aliases.join(" ")}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
renderSuggestion(file: TAbstractFile, el: HTMLElement): void {
|
||||
const container = el.createDiv({ cls: "file-selector-suggestion" });
|
||||
|
||||
// File name
|
||||
container.createDiv({
|
||||
cls: "file-selector-suggestion__name",
|
||||
text: file.name,
|
||||
});
|
||||
|
||||
// Title or aliases (for markdown files)
|
||||
if (file instanceof TFile) {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
if (cache?.frontmatter) {
|
||||
const titleField = this.plugin.fieldMapper.toUserField("title");
|
||||
const title = cache.frontmatter[titleField];
|
||||
|
||||
if (title) {
|
||||
container.createDiv({
|
||||
cls: "file-selector-suggestion__title",
|
||||
text: title,
|
||||
});
|
||||
} else {
|
||||
const aliases = parseFrontMatterAliases(cache.frontmatter);
|
||||
if (aliases && aliases.length > 0) {
|
||||
container.createDiv({
|
||||
cls: "file-selector-suggestion__aliases",
|
||||
text: aliases.join(", "),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Path (if not in root)
|
||||
if (file.parent && file.parent.path !== "/") {
|
||||
container.createDiv({
|
||||
cls: "file-selector-suggestion__path",
|
||||
text: file.parent.path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onChooseSuggestion(file: TAbstractFile, evt: MouseEvent | KeyboardEvent): void {
|
||||
this.resultHandled = true;
|
||||
this.options.onResult({ type: "selected", file });
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.inputEl.removeEventListener("input", this.handleInputChange);
|
||||
|
||||
if (this.createFooterEl) {
|
||||
this.createFooterEl.remove();
|
||||
this.createFooterEl = null;
|
||||
}
|
||||
|
||||
// Defer cancelled check (same pattern as TaskSelectorWithCreateModal)
|
||||
setTimeout(() => {
|
||||
if (!this.resultHandled) {
|
||||
this.options.onResult({ type: "cancelled" });
|
||||
}
|
||||
}, 0);
|
||||
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to open a file selector modal
|
||||
*/
|
||||
export function openFileSelector(
|
||||
plugin: TaskNotesPlugin,
|
||||
onChoose: (file: TAbstractFile | null) => void,
|
||||
options?: {
|
||||
placeholder?: string;
|
||||
title?: string;
|
||||
filter?: "markdown" | "all" | ((file: TAbstractFile) => boolean);
|
||||
newFileFolder?: string;
|
||||
}
|
||||
): void {
|
||||
const modal = new FileSelectorModal(plugin.app, plugin, {
|
||||
placeholder: options?.placeholder,
|
||||
title: options?.title,
|
||||
filter: options?.filter,
|
||||
newFileFolder: options?.newFileFolder,
|
||||
onResult: (result) => {
|
||||
if (result.type === "selected" || result.type === "created") {
|
||||
onChoose(result.file);
|
||||
} else {
|
||||
onChoose(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
modal.open();
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import { App, Modal, Setting, Notice, TFile } from "obsidian";
|
|||
import TaskNotesPlugin from "../main";
|
||||
import { ICSEvent, TaskInfo, NoteInfo } from "../types";
|
||||
import { ICSNoteCreationModal } from "./ICSNoteCreationModal";
|
||||
import { ICSNoteLinkModal } from "./ICSNoteLinkModal";
|
||||
import { openFileSelector } from "./FileSelectorModal";
|
||||
import { SafeAsync } from "../utils/safeAsync";
|
||||
import { TranslationKey } from "../i18n";
|
||||
|
||||
|
|
@ -185,10 +185,11 @@ export class ICSEventInfoModal extends Modal {
|
|||
}
|
||||
|
||||
private async linkExistingNote(): Promise<void> {
|
||||
console.log("Link existing note button clicked");
|
||||
await SafeAsync.execute(
|
||||
async () => {
|
||||
const modal = new ICSNoteLinkModal(this.app, this.plugin, async (file) => {
|
||||
openFileSelector(this.plugin, async (file) => {
|
||||
if (!file) return;
|
||||
|
||||
await SafeAsync.execute(
|
||||
async () => {
|
||||
await this.plugin.icsNoteService.linkNoteToICS(
|
||||
|
|
@ -202,8 +203,10 @@ export class ICSEventInfoModal extends Modal {
|
|||
errorMessage: "Failed to link note",
|
||||
}
|
||||
);
|
||||
}, {
|
||||
placeholder: "Search notes to link...",
|
||||
filter: "markdown",
|
||||
});
|
||||
modal.open();
|
||||
},
|
||||
{
|
||||
errorMessage: "Failed to open note selection",
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
import {
|
||||
App,
|
||||
FuzzySuggestModal,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
SearchResult,
|
||||
parseFrontMatterAliases,
|
||||
} from "obsidian";
|
||||
import type TaskNotesPlugin from "../main";
|
||||
|
||||
/**
|
||||
* Modal for selecting notes to link to ICS events using fuzzy search
|
||||
* Based on the existing ProjectSelectModal pattern
|
||||
*/
|
||||
export class ICSNoteLinkModal extends FuzzySuggestModal<TAbstractFile> {
|
||||
private onChoose: (file: TAbstractFile) => void;
|
||||
private plugin: TaskNotesPlugin;
|
||||
|
||||
constructor(app: App, plugin: TaskNotesPlugin, onChoose: (file: TAbstractFile) => void) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.onChoose = onChoose;
|
||||
this.setPlaceholder("Type to search for notes to link...");
|
||||
this.setInstructions([
|
||||
{ command: "↑↓", purpose: "to navigate" },
|
||||
{ command: "↵", purpose: "to select" },
|
||||
{ command: "esc", purpose: "to cancel" },
|
||||
]);
|
||||
}
|
||||
|
||||
getItems(): TAbstractFile[] {
|
||||
return this.app.vault
|
||||
.getAllLoadedFiles()
|
||||
.filter(
|
||||
(file) =>
|
||||
file instanceof TFile &&
|
||||
file.extension === "md" &&
|
||||
!file.path.includes(".trash")
|
||||
);
|
||||
}
|
||||
|
||||
getItemText(file: TAbstractFile): string {
|
||||
let text = `${file.name} ${file.path}`;
|
||||
|
||||
// Add aliases to searchable text
|
||||
if (file instanceof TFile) {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
if (cache?.frontmatter) {
|
||||
const aliases = parseFrontMatterAliases(cache.frontmatter);
|
||||
if (aliases && aliases.length > 0) {
|
||||
text += ` ${aliases.join(" ")}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
renderSuggestion(value: { item: TAbstractFile; match: SearchResult }, el: HTMLElement) {
|
||||
const file = value.item;
|
||||
el.empty();
|
||||
|
||||
const container = el.createDiv({ cls: "ics-note-link-suggestion" });
|
||||
|
||||
// File name (main line)
|
||||
const nameEl = container.createSpan({ cls: "note-link-name" });
|
||||
nameEl.textContent = file.name;
|
||||
|
||||
// Title or aliases (second line)
|
||||
if (file instanceof TFile) {
|
||||
const cache = this.app.metadataCache.getFileCache(file);
|
||||
if (cache?.frontmatter) {
|
||||
const titleField = this.plugin.fieldMapper.toUserField("title");
|
||||
const title = cache.frontmatter[titleField];
|
||||
|
||||
if (title) {
|
||||
const titleEl = container.createDiv({ cls: "note-link-title" });
|
||||
titleEl.textContent = title;
|
||||
} else {
|
||||
const aliases = parseFrontMatterAliases(cache.frontmatter);
|
||||
if (aliases && aliases.length > 0) {
|
||||
const aliasEl = container.createDiv({ cls: "note-link-aliases" });
|
||||
aliasEl.textContent = aliases.join(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File path (if different from name)
|
||||
if (file.path !== file.name) {
|
||||
const pathEl = container.createDiv({ cls: "note-link-path" });
|
||||
pathEl.textContent = file.path;
|
||||
}
|
||||
}
|
||||
|
||||
onChooseItem(file: TAbstractFile, evt: MouseEvent | KeyboardEvent) {
|
||||
this.onChoose(file);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
import TaskNotesPlugin from "../main";
|
||||
import { TimeBlock, DailyNoteFrontmatter } from "../types";
|
||||
import { generateTimeblockId } from "../utils/helpers";
|
||||
import { AttachmentSelectModal } from "./AttachmentSelectModal";
|
||||
import { openFileSelector } from "./FileSelectorModal";
|
||||
import { parseDateAsLocal } from "../utils/dateUtils";
|
||||
import {
|
||||
createDailyNote,
|
||||
|
|
@ -148,10 +148,12 @@ export class TimeblockCreationModal extends Modal {
|
|||
.setButtonText(this.translate("modals.timeblockCreation.addAttachmentButton"))
|
||||
.setTooltip(this.translate("modals.timeblockCreation.addAttachmentTooltip"))
|
||||
.onClick(() => {
|
||||
const modal = new AttachmentSelectModal(this.app, this.plugin, (file) => {
|
||||
this.addAttachment(file);
|
||||
openFileSelector(this.plugin, (file) => {
|
||||
if (file) this.addAttachment(file);
|
||||
}, {
|
||||
placeholder: "Search files or type to create new...",
|
||||
filter: "all",
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
setTooltip,
|
||||
} from "obsidian";
|
||||
import TaskNotesPlugin from "../main";
|
||||
import { AttachmentSelectModal } from "./AttachmentSelectModal";
|
||||
import { openFileSelector } from "./FileSelectorModal";
|
||||
import {
|
||||
getDailyNote,
|
||||
getAllDailyNotes,
|
||||
|
|
@ -130,10 +130,12 @@ export class TimeblockInfoModal extends Modal {
|
|||
.setButtonText(this.translate("modals.timeblockInfo.addAttachmentButton"))
|
||||
.setTooltip(this.translate("modals.timeblockInfo.addAttachmentTooltip"))
|
||||
.onClick(() => {
|
||||
const modal = new AttachmentSelectModal(this.app, this.plugin, (file) => {
|
||||
this.addAttachment(file);
|
||||
openFileSelector(this.plugin, (file) => {
|
||||
if (file) this.addAttachment(file);
|
||||
}, {
|
||||
placeholder: "Search files or type to create new...",
|
||||
filter: "all",
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
92
styles/file-selector-modal.css
Normal file
92
styles/file-selector-modal.css
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/* ================================================
|
||||
FILE SELECTOR MODAL
|
||||
================================================ */
|
||||
|
||||
/* ================================================
|
||||
SUGGESTION STYLING
|
||||
================================================ */
|
||||
|
||||
.file-selector-suggestion {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: var(--tn-spacing-xs) 0;
|
||||
}
|
||||
|
||||
.file-selector-suggestion__name {
|
||||
font-weight: 500;
|
||||
font-size: var(--tn-font-size-md);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.file-selector-suggestion__title,
|
||||
.file-selector-suggestion__aliases {
|
||||
font-size: var(--tn-font-size-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-selector-suggestion__path {
|
||||
font-size: var(--tn-font-size-xs);
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
CREATE FOOTER
|
||||
================================================ */
|
||||
|
||||
.file-selector-create-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--tn-spacing-md);
|
||||
padding: var(--tn-spacing-md) var(--tn-spacing-lg);
|
||||
background: var(--background-secondary);
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.file-selector-create-footer__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-selector-create-footer__title-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--tn-spacing-xs);
|
||||
}
|
||||
|
||||
.file-selector-create-footer__shortcut {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--background-modifier-border);
|
||||
color: var(--text-faint);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
.file-selector-create-footer__hint-text {
|
||||
font-size: var(--tn-font-size-sm);
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.file-selector-create-footer__filename {
|
||||
font-size: var(--tn-font-size-sm);
|
||||
color: var(--text-normal);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
DARK THEME ADJUSTMENTS
|
||||
================================================ */
|
||||
|
||||
.theme-dark .file-selector-create-footer {
|
||||
background: var(--background-secondary-alt);
|
||||
}
|
||||
|
||||
.theme-dark .file-selector-create-footer__shortcut {
|
||||
background-color: var(--background-modifier-form-field);
|
||||
}
|
||||
|
|
@ -42,11 +42,11 @@
|
|||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--tn-spacing-md);
|
||||
padding: var(--tn-spacing-sm) var(--tn-spacing-md);
|
||||
padding: var(--tn-spacing-md) var(--tn-spacing-lg);
|
||||
background: var(--background-secondary);
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
margin-top: auto;
|
||||
min-height: 50px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.task-selector-create-footer__icon {
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--tn-spacing-xs);
|
||||
gap: var(--tn-spacing-sm);
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
|
|
@ -100,15 +100,16 @@
|
|||
.task-selector-create-footer__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: var(--tn-spacing-xs);
|
||||
}
|
||||
|
||||
.task-selector-create-footer__chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: var(--tn-font-size-xs);
|
||||
font-weight: 500;
|
||||
|
|
|
|||
Loading…
Reference in a new issue