mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
333 lines
8 KiB
TypeScript
333 lines
8 KiB
TypeScript
import { ItemView, WorkspaceLeaf, Notice, App, TFile } from "obsidian";
|
|
import { Root, createRoot } from "react-dom/client";
|
|
import { StatusDashboard } from "@/components/StatusDashboard/StatusDashboard";
|
|
import { DashboardAction } from "@/components/StatusDashboard/QuickActionsPanel";
|
|
import {
|
|
BaseNoteStatusService,
|
|
NoteStatusService,
|
|
} from "@/core/noteStatusService";
|
|
import settingsService from "@/core/settingsService";
|
|
import eventBus from "@/core/eventBus";
|
|
import { VaultStats } from "@/components/StatusDashboard/useVaultStats";
|
|
import { NoteStatus } from "@/types/noteStatus";
|
|
|
|
interface AppWithCommands extends App {
|
|
commands: {
|
|
executeCommandById(commandId: string): boolean;
|
|
};
|
|
}
|
|
|
|
interface CurrentNoteInfo {
|
|
file: TFile | null;
|
|
statuses: Record<string, NoteStatus[]>;
|
|
lastModified: number;
|
|
}
|
|
|
|
export const VIEW_TYPE_STATUS_DASHBOARD = "status-dashboard-view";
|
|
|
|
export class StatusDashboardView extends ItemView {
|
|
root: Root | null = null;
|
|
private vaultStats: VaultStats = {
|
|
totalNotes: 0,
|
|
notesWithStatus: 0,
|
|
statusDistribution: {},
|
|
tagDistribution: {},
|
|
recentChanges: [],
|
|
};
|
|
private currentNote: CurrentNoteInfo = {
|
|
file: null,
|
|
statuses: {},
|
|
lastModified: 0,
|
|
};
|
|
private isLoading: boolean = true;
|
|
|
|
constructor(leaf: WorkspaceLeaf) {
|
|
super(leaf);
|
|
}
|
|
|
|
getViewType() {
|
|
return VIEW_TYPE_STATUS_DASHBOARD;
|
|
}
|
|
|
|
getDisplayText() {
|
|
return "Status Dashboard";
|
|
}
|
|
|
|
getIcon() {
|
|
return "bar-chart-2";
|
|
}
|
|
|
|
private calculateVaultStats = (): VaultStats => {
|
|
const files = this.app.vault.getMarkdownFiles();
|
|
const availableStatuses =
|
|
BaseNoteStatusService.getAllAvailableStatuses();
|
|
const statusMetadataKeys = [settingsService.settings.tagPrefix];
|
|
|
|
let notesWithStatus = 0;
|
|
const statusDistribution: Record<string, number> = {};
|
|
const tagDistribution: Record<string, number> = {};
|
|
|
|
availableStatuses.forEach((status) => {
|
|
statusDistribution[status.name] = 0;
|
|
});
|
|
|
|
statusMetadataKeys.forEach((tag) => {
|
|
tagDistribution[tag] = 0;
|
|
});
|
|
|
|
files.forEach((file) => {
|
|
const cachedMetadata = this.app.metadataCache.getFileCache(file);
|
|
const frontmatter = cachedMetadata?.frontmatter;
|
|
|
|
if (!frontmatter) return;
|
|
|
|
let hasAnyStatus = false;
|
|
|
|
statusMetadataKeys.forEach((key) => {
|
|
const value = frontmatter[key];
|
|
if (value) {
|
|
hasAnyStatus = true;
|
|
tagDistribution[key]++;
|
|
|
|
const statusNames = Array.isArray(value) ? value : [value];
|
|
statusNames.forEach((statusName) => {
|
|
const statusStr = statusName.toString();
|
|
if (statusDistribution.hasOwnProperty(statusStr)) {
|
|
statusDistribution[statusStr]++;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
if (hasAnyStatus) {
|
|
notesWithStatus++;
|
|
}
|
|
});
|
|
|
|
return {
|
|
totalNotes: files.length,
|
|
notesWithStatus,
|
|
statusDistribution,
|
|
tagDistribution,
|
|
recentChanges: [],
|
|
};
|
|
};
|
|
|
|
private updateCurrentNote = () => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
|
|
if (!activeFile) {
|
|
this.currentNote = { file: null, statuses: {}, lastModified: 0 };
|
|
this.renderDashboard();
|
|
return;
|
|
}
|
|
|
|
const noteStatusService = new NoteStatusService(activeFile);
|
|
noteStatusService.populateStatuses();
|
|
|
|
this.currentNote = {
|
|
file: activeFile,
|
|
statuses: noteStatusService.statuses,
|
|
lastModified: activeFile.stat.mtime,
|
|
};
|
|
this.renderDashboard();
|
|
};
|
|
|
|
private updateVaultStats = () => {
|
|
this.vaultStats = this.calculateVaultStats();
|
|
this.renderDashboard();
|
|
};
|
|
|
|
private loadData = () => {
|
|
this.isLoading = true;
|
|
this.renderDashboard();
|
|
|
|
try {
|
|
this.vaultStats = this.calculateVaultStats();
|
|
this.updateCurrentNote();
|
|
} finally {
|
|
this.isLoading = false;
|
|
this.renderDashboard();
|
|
}
|
|
};
|
|
|
|
private handleAction = (action: DashboardAction, value?: string) => {
|
|
const appWithCommands = this.app as AppWithCommands;
|
|
|
|
switch (action) {
|
|
case "refresh":
|
|
this.loadData();
|
|
break;
|
|
case "open-grouped-view":
|
|
this.openGroupedView();
|
|
break;
|
|
case "find-unassigned":
|
|
this.findUnassignedNotes();
|
|
break;
|
|
case "change-status":
|
|
appWithCommands.commands.executeCommandById(
|
|
"note-status:change-status",
|
|
);
|
|
break;
|
|
case "insert-metadata":
|
|
appWithCommands.commands.executeCommandById(
|
|
"note-status:insert-status-metadata",
|
|
);
|
|
break;
|
|
case "cycle-status":
|
|
appWithCommands.commands.executeCommandById(
|
|
"note-status:cycle-status",
|
|
);
|
|
break;
|
|
case "clear-status":
|
|
appWithCommands.commands.executeCommandById(
|
|
"note-status:clear-status",
|
|
);
|
|
break;
|
|
case "copy-status":
|
|
appWithCommands.commands.executeCommandById(
|
|
"note-status:copy-status",
|
|
);
|
|
break;
|
|
case "paste-status":
|
|
appWithCommands.commands.executeCommandById(
|
|
"note-status:paste-status",
|
|
);
|
|
break;
|
|
case "search-by-status":
|
|
appWithCommands.commands.executeCommandById(
|
|
"note-status:search-by-status",
|
|
);
|
|
break;
|
|
case "toggle-multiple-mode":
|
|
appWithCommands.commands.executeCommandById(
|
|
"note-status:toggle-multiple-statuses",
|
|
);
|
|
break;
|
|
case "set-quick-status":
|
|
if (value) {
|
|
const commandId = `note-status:set-status-${value}`;
|
|
const result =
|
|
appWithCommands.commands.executeCommandById(commandId);
|
|
if (!result) {
|
|
new Notice(
|
|
`Quick status command failed: ${value}. Make sure the status exists and multiple statuses mode is disabled.`,
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
case "search-by-specific-status":
|
|
if (value) {
|
|
this.searchBySpecificStatus(value);
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
private openGroupedView() {
|
|
const leaf = this.app.workspace.getLeaf();
|
|
leaf.setViewState({ type: "grouped-status-view", active: true });
|
|
}
|
|
|
|
private findUnassignedNotes() {
|
|
const files = this.app.vault.getMarkdownFiles();
|
|
const filesWithoutStatus = files.filter((file) => {
|
|
const cachedMetadata = this.app.metadataCache.getFileCache(file);
|
|
const frontmatter = cachedMetadata?.frontmatter;
|
|
return (
|
|
!frontmatter || !frontmatter[settingsService.settings.tagPrefix]
|
|
);
|
|
});
|
|
|
|
if (filesWithoutStatus.length === 0) {
|
|
new Notice("All notes have status assigned!");
|
|
return;
|
|
}
|
|
|
|
new Notice(
|
|
`Found ${filesWithoutStatus.length} notes without status. Opening search...`,
|
|
);
|
|
|
|
// Create a search query to find files without the status tag
|
|
const tagPrefix = settingsService.settings.tagPrefix;
|
|
const query = `-[${tagPrefix}:]`;
|
|
|
|
// @ts-ignore
|
|
this.app.internalPlugins.plugins[
|
|
"global-search"
|
|
].instance.openGlobalSearch(query);
|
|
}
|
|
|
|
private searchBySpecificStatus(statusName: string) {
|
|
const tagPrefix = settingsService.settings.tagPrefix;
|
|
const query = `[${tagPrefix}:"${statusName}"]`;
|
|
|
|
// @ts-ignore
|
|
this.app.internalPlugins.plugins[
|
|
"global-search"
|
|
].instance.openGlobalSearch(query);
|
|
}
|
|
|
|
private renderDashboard() {
|
|
if (!this.root) return;
|
|
|
|
this.root.render(
|
|
<StatusDashboard
|
|
onAction={this.handleAction}
|
|
settings={settingsService.settings}
|
|
vaultStats={this.vaultStats}
|
|
currentNote={this.currentNote}
|
|
isLoading={this.isLoading}
|
|
availableStatuses={BaseNoteStatusService.getAllAvailableStatuses()}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
async onOpen() {
|
|
const container = this.containerEl.children[1];
|
|
container.empty();
|
|
container.addClass("status-dashboard-view-container");
|
|
|
|
this.root = createRoot(container);
|
|
|
|
// Set up event listeners
|
|
const handleFileChange = () => {
|
|
this.updateCurrentNote();
|
|
};
|
|
|
|
const handleVaultChange = () => {
|
|
this.loadData();
|
|
};
|
|
|
|
eventBus.subscribe(
|
|
"frontmatter-manually-changed",
|
|
handleVaultChange,
|
|
"status-dashboard-vault-subscription",
|
|
);
|
|
eventBus.subscribe(
|
|
"active-file-change",
|
|
handleFileChange,
|
|
"status-dashboard-file-subscription",
|
|
);
|
|
|
|
this.app.workspace.on("active-leaf-change", handleFileChange);
|
|
|
|
// Initial load
|
|
this.loadData();
|
|
}
|
|
|
|
async onClose() {
|
|
// Clean up event listeners
|
|
eventBus.unsubscribe(
|
|
"frontmatter-manually-changed",
|
|
"status-dashboard-vault-subscription",
|
|
);
|
|
eventBus.unsubscribe(
|
|
"active-file-change",
|
|
"status-dashboard-file-subscription",
|
|
);
|
|
|
|
this.root?.unmount();
|
|
this.root = null;
|
|
}
|
|
}
|