mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 12:30:24 +00:00
380 lines
9.4 KiB
TypeScript
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;
|
|
}
|
|
}
|