Skip to content

Commit 9a97834

Browse files
committed
Improve type safety and reduce code duplication
- Replace 'any' casts with proper types (TFile, FileExplorerItem, FileExplorerView) - Add FileExplorerItem interface for file explorer wrapper objects - Consolidate duplicate ensureFolderExists into single import from folder-ops - Extract template button handlers into reusable generateDefaultTemplateHandler - Add DEFAULT_TEMPLATES_FOLDER constant - Add comments documenting intentional internal API usage
1 parent 932b965 commit 9a97834

4 files changed

Lines changed: 88 additions & 89 deletions

File tree

src/folder-ops.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* These functions interact with the Obsidian vault to manage folders.
44
*/
55

6-
import { App, TAbstractFile, TFolder, normalizePath } from "obsidian";
6+
import { App, TAbstractFile, TFile, TFolder, normalizePath } from "obsidian";
77

88
/**
99
* Ensure a folder exists, creating it and any parent directories if necessary.
@@ -122,8 +122,8 @@ export function getFolderLastModifiedTime(folder: TFolder): number {
122122
if (child instanceof TFolder) {
123123
traverse(child);
124124
} else {
125-
// TFile has mtime property
126-
const file = child as any;
125+
// child must be a TFile (already verified by the if check above)
126+
const file = child as TFile;
127127
if (typeof file.stat?.mtime === "number") {
128128
maxMtime = Math.max(maxMtime, file.stat.mtime);
129129
}

src/main.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from "./utils";
1616
import { ArchiveConfirmModal, NameInputModal } from "./modals";
1717
import { ensureFolderExists, getExistingPaths, focusFolder, getFolderLastModifiedTime } from "./folder-ops";
18-
import type { FileExplorerView } from "./obsidian-internals";
18+
import type { FileExplorerView, FileExplorerItem } from "./obsidian-internals";
1919

2020
declare global {
2121
interface Window {
@@ -239,6 +239,8 @@ export default class ParaManagerPlugin extends Plugin {
239239
*/
240240
private isTemplaterAvailable(): boolean {
241241
try {
242+
// INTERNAL API: app.plugins is not in Obsidian's public type definitions
243+
// This is necessary to detect the Templater plugin
242244
return !!(this.app as any).plugins?.plugins?.["templater-obsidian"];
243245
} catch {
244246
return false;
@@ -251,6 +253,8 @@ export default class ParaManagerPlugin extends Plugin {
251253
*/
252254
private isCoreTemplatesAvailable(): boolean {
253255
try {
256+
// INTERNAL API: app.internalPlugins is not in Obsidian's public type definitions
257+
// This is necessary to detect the core Templates plugin
254258
return !!(this.app as any).internalPlugins?.plugins?.["templates"];
255259
} catch {
256260
return false;
@@ -287,6 +291,8 @@ export default class ParaManagerPlugin extends Plugin {
287291
try {
288292
// Prefer Templater if available
289293
if (this.isTemplaterAvailable()) {
294+
// INTERNAL API: app.plugins is not in Obsidian's public type definitions
295+
// We've already verified Templater is available via isTemplaterAvailable()
290296
const templater = ((this.app as any).plugins?.plugins?.["templater-obsidian"] as TemplaterPlugin)?.templater;
291297
if (templater) {
292298
const result = await templater.parse_template({ template_file: templateFile, target_file: file });
@@ -516,7 +522,7 @@ export default class ParaManagerPlugin extends Plugin {
516522
* @param items - The wrapper items to sort
517523
* @returns The sorted wrapper items
518524
*/
519-
private sortProjectItems(items: any[]): any[] {
525+
private sortProjectItems(items: FileExplorerItem[]): FileExplorerItem[] {
520526
const sorted = [...items];
521527

522528
if (this.settings.projectSortOrder === "lastModified") {

src/obsidian-internals.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import { View, WorkspaceLeaf, TFolder, TAbstractFile } from 'obsidian';
22

3+
/**
4+
* Wrapper object for items displayed in the file explorer UI.
5+
* The actual file or folder is accessible via the .file property.
6+
* This is an undocumented Obsidian internal type.
7+
*/
8+
export interface FileExplorerItem {
9+
file: TAbstractFile;
10+
}
11+
312
export interface FileExplorerView extends View {
413
getSortedFolderItems(folder: TFolder): TAbstractFile[];
514
requestSort(): void;

src/settings.ts

Lines changed: 68 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
import { AbstractInputSuggest, App, Notice, PluginSettingTab, Setting, TextComponent, TFolder, normalizePath } from "obsidian";
1+
import { AbstractInputSuggest, App, Notice, PluginSettingTab, Setting, TextComponent, TFile, TFolder, normalizePath } from "obsidian";
22
import type ParaManagerPlugin from "./main";
33
import { validateParaFolderPath, type ParaFolderField } from "./utils";
4+
import { ensureFolderExists } from "./folder-ops";
5+
import type { FileExplorerView } from "./obsidian-internals";
46

5-
/**
6-
* Ensure a folder exists in the vault, creating it if necessary.
7-
*/
8-
async function ensureFolderExistsVault(app: App, folderPath: string): Promise<void> {
9-
const normalized = normalizePath(folderPath);
10-
const existing = app.vault.getAbstractFileByPath(normalized);
11-
if (!existing) {
12-
await app.vault.createFolder(normalized);
13-
}
14-
}
7+
/** Default folder for storing templates */
8+
const DEFAULT_TEMPLATES_FOLDER = "Templates";
159

1610
/**
1711
* Generate a default template for a PARA item type.
@@ -63,6 +57,44 @@ A resource is a topic or tool you want to reference in the future.
6357
return descriptions[itemType] || `# {{name}}\n`;
6458
}
6559

60+
/**
61+
* Handle the "Generate Default" button click for template settings.
62+
* Creates a default template file, updates the corresponding setting, and refreshes the UI.
63+
*
64+
* @param plugin - The ParaManagerPlugin instance
65+
* @param itemType - The PARA item type (Project, Area, or Resource)
66+
* @param settingsKey - The settings key to update (projectTemplatePath, areaTemplatePath, or resourceTemplatePath)
67+
* @param refreshDisplay - Function to refresh the settings display UI
68+
*/
69+
async function generateDefaultTemplateHandler(
70+
plugin: ParaManagerPlugin,
71+
itemType: "Project" | "Area" | "Resource",
72+
settingsKey: "projectTemplatePath" | "areaTemplatePath" | "resourceTemplatePath",
73+
refreshDisplay: () => void
74+
): Promise<void> {
75+
const defaultContent = generateDefaultTemplate(itemType);
76+
await ensureFolderExists(plugin.app, DEFAULT_TEMPLATES_FOLDER);
77+
const templatePath = `${DEFAULT_TEMPLATES_FOLDER}/${itemType}.md`;
78+
79+
// Check if template already exists
80+
const existing = plugin.app.vault.getAbstractFileByPath(templatePath);
81+
if (existing) {
82+
new Notice("Template already exists at " + templatePath);
83+
return;
84+
}
85+
86+
try {
87+
await plugin.app.vault.create(templatePath, defaultContent);
88+
plugin.settings[settingsKey] = templatePath;
89+
await plugin.saveSettings();
90+
refreshDisplay(); // Refresh UI
91+
new Notice(`Created ${itemType.toLowerCase()} template at ${templatePath}`);
92+
} catch (error) {
93+
const message = error instanceof Error ? error.message : "Unknown error";
94+
new Notice("Failed to create template: " + message);
95+
}
96+
}
97+
6698
/**
6799
* Provides folder autocomplete suggestions for text inputs.
68100
* Uses Obsidian's AbstractInputSuggest to show a dropdown of matching folders.
@@ -148,15 +180,15 @@ function setupTemplatePathInput(
148180
const inputEl = text.inputEl;
149181

150182
// Create a custom suggest for markdown files
151-
class FileInputSuggest extends AbstractInputSuggest<any> {
183+
class FileInputSuggest extends AbstractInputSuggest<TFile> {
152184
constructor(app: App, inputEl: HTMLInputElement) {
153185
super(app, inputEl);
154186
}
155187

156-
getSuggestions(inputStr: string): any[] {
188+
getSuggestions(inputStr: string): TFile[] {
157189
const files = plugin.app.vault.getAllLoadedFiles()
158-
.filter((f): f is any => {
159-
return typeof f.name === "string" && f.name.endsWith(".md");
190+
.filter((f): f is TFile => {
191+
return f instanceof TFile && f.name.endsWith(".md");
160192
});
161193

162194
if (!inputStr) return files.slice(0, 50); // Limit suggestions
@@ -167,11 +199,11 @@ function setupTemplatePathInput(
167199
).slice(0, 20);
168200
}
169201

170-
renderSuggestion(file: any, el: HTMLElement): void {
202+
renderSuggestion(file: TFile, el: HTMLElement): void {
171203
el.setText(file.path);
172204
}
173205

174-
selectSuggestion(file: any): void {
206+
selectSuggestion(file: TFile): void {
175207
this.setValue(file.path);
176208
this.close();
177209
}
@@ -449,7 +481,7 @@ export class ParaManagerSettingTab extends PluginSettingTab {
449481
this.plugin.installSortingPatch();
450482
const fileExplorer = this.plugin.app.workspace.getLeavesOfType("file-explorer")[0];
451483
if (fileExplorer) {
452-
(fileExplorer.view as any).requestSort?.();
484+
(fileExplorer.view as FileExplorerView).requestSort?.();
453485
}
454486
})
455487
);
@@ -479,28 +511,12 @@ export class ParaManagerSettingTab extends PluginSettingTab {
479511
button
480512
.setButtonText("Generate Default")
481513
.onClick(async () => {
482-
const defaultContent = generateDefaultTemplate("Project");
483-
const templatesFolder = "Templates";
484-
await ensureFolderExistsVault(this.plugin.app, templatesFolder);
485-
const templatePath = `${templatesFolder}/Project.md`;
486-
487-
// Check if template already exists
488-
const existing = this.plugin.app.vault.getAbstractFileByPath(templatePath);
489-
if (existing) {
490-
new Notice("Template already exists at " + templatePath);
491-
return;
492-
}
493-
494-
try {
495-
await this.plugin.app.vault.create(templatePath, defaultContent);
496-
this.plugin.settings.projectTemplatePath = templatePath;
497-
await this.plugin.saveSettings();
498-
this.display(); // Refresh UI
499-
new Notice("Created project template at " + templatePath);
500-
} catch (error) {
501-
const message = error instanceof Error ? error.message : "Unknown error";
502-
new Notice("Failed to create template: " + message);
503-
}
514+
await generateDefaultTemplateHandler(
515+
this.plugin,
516+
"Project",
517+
"projectTemplatePath",
518+
() => this.display()
519+
);
504520
})
505521
);
506522

@@ -526,28 +542,12 @@ export class ParaManagerSettingTab extends PluginSettingTab {
526542
button
527543
.setButtonText("Generate Default")
528544
.onClick(async () => {
529-
const defaultContent = generateDefaultTemplate("Area");
530-
const templatesFolder = "Templates";
531-
await ensureFolderExistsVault(this.plugin.app, templatesFolder);
532-
const templatePath = `${templatesFolder}/Area.md`;
533-
534-
// Check if template already exists
535-
const existing = this.plugin.app.vault.getAbstractFileByPath(templatePath);
536-
if (existing) {
537-
new Notice("Template already exists at " + templatePath);
538-
return;
539-
}
540-
541-
try {
542-
await this.plugin.app.vault.create(templatePath, defaultContent);
543-
this.plugin.settings.areaTemplatePath = templatePath;
544-
await this.plugin.saveSettings();
545-
this.display(); // Refresh UI
546-
new Notice("Created area template at " + templatePath);
547-
} catch (error) {
548-
const message = error instanceof Error ? error.message : "Unknown error";
549-
new Notice("Failed to create template: " + message);
550-
}
545+
await generateDefaultTemplateHandler(
546+
this.plugin,
547+
"Area",
548+
"areaTemplatePath",
549+
() => this.display()
550+
);
551551
})
552552
);
553553

@@ -573,28 +573,12 @@ export class ParaManagerSettingTab extends PluginSettingTab {
573573
button
574574
.setButtonText("Generate Default")
575575
.onClick(async () => {
576-
const defaultContent = generateDefaultTemplate("Resource");
577-
const templatesFolder = "Templates";
578-
await ensureFolderExistsVault(this.plugin.app, templatesFolder);
579-
const templatePath = `${templatesFolder}/Resource.md`;
580-
581-
// Check if template already exists
582-
const existing = this.plugin.app.vault.getAbstractFileByPath(templatePath);
583-
if (existing) {
584-
new Notice("Template already exists at " + templatePath);
585-
return;
586-
}
587-
588-
try {
589-
await this.plugin.app.vault.create(templatePath, defaultContent);
590-
this.plugin.settings.resourceTemplatePath = templatePath;
591-
await this.plugin.saveSettings();
592-
this.display(); // Refresh UI
593-
new Notice("Created resource template at " + templatePath);
594-
} catch (error) {
595-
const message = error instanceof Error ? error.message : "Unknown error";
596-
new Notice("Failed to create template: " + message);
597-
}
576+
await generateDefaultTemplateHandler(
577+
this.plugin,
578+
"Resource",
579+
"resourceTemplatePath",
580+
() => this.display()
581+
);
598582
})
599583
);
600584
}

0 commit comments

Comments
 (0)