devonthesofa_obsidian-note-.../integrations/views/status-dashboard-view.tsx

380 lines
9.4 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";
import { getAllFrontmatterKeys } from "@/core/statusKeyHelpers";
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 "Dashboard";
}
getIcon() {
return "bar-chart-2";
}
private calculateVaultStats = (): VaultStats => {
const files = this.app.vault.getFiles();
const availableStatuses =
BaseNoteStatusService.getAllAvailableStatuses();
const statusMetadataKeys = getAllFrontmatterKeys();
let notesWithStatus = 0;
const statusDistribution: Record<string, number> = {};
const tagDistribution: Record<string, number> = {};
// Initialize distribution using full scoped identifiers
availableStatuses.forEach((status) => {
const scopedIdentifier = status.templateId
? `${status.templateId}:${status.name}`
: status.name;
statusDistribution[scopedIdentifier] = 0;
});
statusMetadataKeys.forEach((tag) => {
tagDistribution[tag] = 0;
});
files.forEach((file) => {
const noteStatusService = new NoteStatusService(file);
noteStatusService.populateStatuses();
let hasAnyStatus = false;
statusMetadataKeys.forEach((key) => {
const statuses = noteStatusService.getStatusesForKey(key);
if (!statuses.length) return;
hasAnyStatus = true;
tagDistribution[key]++;
statuses.forEach((status) => {
const scopedIdentifier = status.templateId
? `${status.templateId}:${status.name}`
: status.name;
if (!statusDistribution.hasOwnProperty(scopedIdentifier)) {
statusDistribution[scopedIdentifier] = 0;
}
statusDistribution[scopedIdentifier]++;
});
});
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();
const statusMetadataKeys = getAllFrontmatterKeys();
const statusesByKey: Record<string, NoteStatus[]> = {};
statusMetadataKeys.forEach((key) => {
const statuses = noteStatusService.getStatusesForKey(key);
if (statuses.length) {
statusesByKey[key] = statuses;
}
});
this.currentNote = {
file: activeFile,
statuses: statusesByKey,
lastModified: activeFile.stat.mtime,
};
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 "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 statusKeys = getAllFrontmatterKeys();
const filesWithoutStatus = files.filter((file) => {
const cachedMetadata = this.app.metadataCache.getFileCache(file);
const frontmatter = cachedMetadata?.frontmatter;
if (!frontmatter) return true;
return !statusKeys.some((key) => {
const value = frontmatter[key];
if (Array.isArray(value)) {
return value.length > 0;
}
return Boolean(value);
});
});
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 query = statusKeys.map((key) => `-[${key}:]`).join(" ");
// @ts-ignore
this.app.internalPlugins.plugins[
"global-search"
].instance.openGlobalSearch(query);
}
private searchBySpecificStatus(statusName: string) {
const keys = getAllFrontmatterKeys();
const query = keys
.map((key) => `[${key}:"${statusName}"]`)
.join(" OR ");
// @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");
// Check if vault exceeds the size limit
const files = this.app.vault.getFiles();
const vaultSizeLimit = settingsService.settings.vaultSizeLimit || 15000;
if (vaultSizeLimit > 0 && files.length > vaultSizeLimit) {
// Show disabled message
container.createEl("div", {
cls: "status-dashboard-disabled",
text: `Dashboard disabled: Vault has ${files.length} files (limit: ${vaultSizeLimit}). You can adjust this limit in plugin settings.`,
});
return;
}
this.root = createRoot(container);
// Set up event listeners
const handleFileChange = () => {
this.updateCurrentNote();
};
const handleVaultChange = () => {
this.loadData();
};
eventBus.subscribe(
"status-changed",
handleVaultChange,
"status-dashboard-vault-subscription",
);
eventBus.subscribe(
"active-file-change",
handleFileChange,
"status-dashboard-file-subscription",
);
eventBus.subscribe(
"plugin-settings-changed",
({ key }) => {
if (
key === "tagPrefix" ||
key === "enabledTemplates" ||
key === "useCustomStatusesOnly" ||
key === "customStatuses" ||
key === "useMultipleStatuses" ||
key === "strictStatuses" ||
key === "statusFrontmatterMappings" ||
key === "vaultSizeLimit"
) {
handleVaultChange();
handleFileChange();
}
},
"status-dashboard-settings-subscription",
);
this.app.workspace.on("active-leaf-change", handleFileChange);
// Initial load
this.loadData();
}
async onClose() {
// Clean up event listeners
eventBus.unsubscribe(
"status-changed",
"status-dashboard-vault-subscription",
);
eventBus.unsubscribe(
"active-file-change",
"status-dashboard-file-subscription",
);
eventBus.unsubscribe(
"plugin-settings-changed",
"status-dashboard-settings-subscription",
);
this.root?.unmount();
this.root = null;
}
}