mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
refactor: split StatusDashboard component
This commit is contained in:
parent
77442bec67
commit
5b8bccd6c4
8 changed files with 428 additions and 370 deletions
77
components/StatusDashboard/CurrentNoteSection.tsx
Normal file
77
components/StatusDashboard/CurrentNoteSection.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { TFile } from "obsidian";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { StatusBadge } from "@/components/atoms/StatusBadge";
|
||||
import { GroupLabel } from "@/components/atoms/GroupLabel";
|
||||
|
||||
interface CurrentNoteInfo {
|
||||
file: TFile | null;
|
||||
statuses: Record<string, NoteStatus[]>;
|
||||
lastModified: number;
|
||||
}
|
||||
|
||||
interface CurrentNoteSectionProps {
|
||||
currentNote: CurrentNoteInfo;
|
||||
}
|
||||
|
||||
export const CurrentNoteSection = ({
|
||||
currentNote,
|
||||
}: CurrentNoteSectionProps) => {
|
||||
return (
|
||||
<div className="status-dashboard-section">
|
||||
<div className="status-dashboard-section-header">
|
||||
<h3>Current Note</h3>
|
||||
</div>
|
||||
<div className="status-dashboard-current-note">
|
||||
{currentNote.file ? (
|
||||
<>
|
||||
<div className="current-note-info">
|
||||
<div className="current-note-name">
|
||||
{currentNote.file.basename}
|
||||
</div>
|
||||
<div className="current-note-path">
|
||||
{currentNote.file.path}
|
||||
</div>
|
||||
<div className="current-note-modified">
|
||||
Last modified:{" "}
|
||||
{new Date(
|
||||
currentNote.lastModified,
|
||||
).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="current-note-statuses">
|
||||
{Object.entries(currentNote.statuses).map(
|
||||
([tag, statuses]) => (
|
||||
<div
|
||||
key={tag}
|
||||
className="current-note-tag-group"
|
||||
>
|
||||
<GroupLabel name={tag} />
|
||||
<div className="current-note-status-list">
|
||||
{statuses.length > 0 ? (
|
||||
statuses.map((status) => (
|
||||
<StatusBadge
|
||||
key={status.name}
|
||||
status={status}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<span className="current-note-no-status">
|
||||
No status assigned
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="current-note-empty">
|
||||
No note currently active
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
59
components/StatusDashboard/QuickActionsPanel.tsx
Normal file
59
components/StatusDashboard/QuickActionsPanel.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { BaseNoteStatusService } from "@/core/noteStatusService";
|
||||
import settingsService from "@/core/settingsService";
|
||||
|
||||
interface QuickActionsPanelProps {
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
export const QuickActionsPanel = ({ onRefresh }: QuickActionsPanelProps) => {
|
||||
return (
|
||||
<div className="status-dashboard-section">
|
||||
<div className="status-dashboard-section-header">
|
||||
<h3>Quick Actions</h3>
|
||||
</div>
|
||||
<div className="quick-actions">
|
||||
<button
|
||||
className="quick-action-btn"
|
||||
onClick={() => {
|
||||
const leaf =
|
||||
BaseNoteStatusService.app.workspace.getLeaf();
|
||||
leaf.setViewState({
|
||||
type: "grouped-status-view",
|
||||
active: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
📊 View Grouped Statuses
|
||||
</button>
|
||||
<button
|
||||
className="quick-action-btn"
|
||||
onClick={() => {
|
||||
const files =
|
||||
BaseNoteStatusService.app.vault.getMarkdownFiles();
|
||||
const filesWithoutStatus = files.filter((file) => {
|
||||
const cachedMetadata =
|
||||
BaseNoteStatusService.app.metadataCache.getFileCache(
|
||||
file,
|
||||
);
|
||||
const frontmatter = cachedMetadata?.frontmatter;
|
||||
return (
|
||||
!frontmatter ||
|
||||
!frontmatter[settingsService.settings.tagPrefix]
|
||||
);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Found ${filesWithoutStatus.length} notes without status:`,
|
||||
filesWithoutStatus.map((f) => f.path),
|
||||
);
|
||||
}}
|
||||
>
|
||||
🔍 Find Notes Without Status
|
||||
</button>
|
||||
<button className="quick-action-btn" onClick={onRefresh}>
|
||||
🔄 Refresh Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,137 +1,27 @@
|
|||
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { TFile } from "obsidian";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import {
|
||||
BaseNoteStatusService,
|
||||
NoteStatusService,
|
||||
} from "@/core/noteStatusService";
|
||||
import { StatusBadge } from "@/components/atoms/StatusBadge";
|
||||
import { GroupLabel } from "@/components/atoms/GroupLabel";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { BaseNoteStatusService } from "@/core/noteStatusService";
|
||||
import eventBus from "@/core/eventBus";
|
||||
import settingsService from "@/core/settingsService";
|
||||
|
||||
interface VaultStats {
|
||||
totalNotes: number;
|
||||
notesWithStatus: number;
|
||||
statusDistribution: Record<string, number>;
|
||||
tagDistribution: Record<string, number>;
|
||||
recentChanges: Array<{
|
||||
file: TFile;
|
||||
status: NoteStatus;
|
||||
timestamp: number;
|
||||
action: "added" | "removed";
|
||||
}>;
|
||||
}
|
||||
|
||||
interface CurrentNoteInfo {
|
||||
file: TFile | null;
|
||||
statuses: Record<string, NoteStatus[]>;
|
||||
lastModified: number;
|
||||
}
|
||||
import { VaultStatsCard } from "./VaultStatsCard";
|
||||
import { CurrentNoteSection } from "./CurrentNoteSection";
|
||||
import { StatusDistributionChart } from "./StatusDistributionChart";
|
||||
import { QuickActionsPanel } from "./QuickActionsPanel";
|
||||
import { useVaultStats } from "./useVaultStats";
|
||||
import { useCurrentNote } from "./useCurrentNote";
|
||||
|
||||
export const StatusDashboard = () => {
|
||||
const [vaultStats, setVaultStats] = useState<VaultStats>({
|
||||
totalNotes: 0,
|
||||
notesWithStatus: 0,
|
||||
statusDistribution: {},
|
||||
tagDistribution: {},
|
||||
recentChanges: [],
|
||||
});
|
||||
|
||||
const [currentNote, setCurrentNote] = useState<CurrentNoteInfo>({
|
||||
file: null,
|
||||
statuses: {},
|
||||
lastModified: 0,
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const calculateVaultStats = useCallback((): VaultStats => {
|
||||
const files = BaseNoteStatusService.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> = {};
|
||||
|
||||
// Initialize counters
|
||||
availableStatuses.forEach((status) => {
|
||||
statusDistribution[status.name] = 0;
|
||||
});
|
||||
|
||||
statusMetadataKeys.forEach((tag) => {
|
||||
tagDistribution[tag] = 0;
|
||||
});
|
||||
|
||||
files.forEach((file) => {
|
||||
const cachedMetadata =
|
||||
BaseNoteStatusService.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: [], // TODO: Implement recent changes tracking
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateCurrentNote = useCallback(() => {
|
||||
const activeFile = BaseNoteStatusService.app.workspace.getActiveFile();
|
||||
|
||||
if (!activeFile) {
|
||||
setCurrentNote({ file: null, statuses: {}, lastModified: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
const noteStatusService = new NoteStatusService(activeFile);
|
||||
noteStatusService.populateStatuses();
|
||||
|
||||
setCurrentNote({
|
||||
file: activeFile,
|
||||
statuses: noteStatusService.statuses,
|
||||
lastModified: activeFile.stat.mtime,
|
||||
});
|
||||
}, []);
|
||||
const { vaultStats, updateVaultStats } = useVaultStats();
|
||||
const { currentNote, updateCurrentNote } = useCurrentNote();
|
||||
|
||||
const loadData = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const stats = calculateVaultStats();
|
||||
setVaultStats(stats);
|
||||
updateVaultStats();
|
||||
updateCurrentNote();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [calculateVaultStats, updateCurrentNote]);
|
||||
}, [updateVaultStats, updateCurrentNote]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
|
|
@ -173,26 +63,6 @@ export const StatusDashboard = () => {
|
|||
};
|
||||
}, [loadData, updateCurrentNote]);
|
||||
|
||||
const statusChart = useMemo(() => {
|
||||
const total = Object.values(vaultStats.statusDistribution).reduce(
|
||||
(sum, count) => sum + count,
|
||||
0,
|
||||
);
|
||||
if (total === 0) return [];
|
||||
|
||||
return Object.entries(vaultStats.statusDistribution)
|
||||
.filter(([_, count]) => count > 0)
|
||||
.map(([statusName, count]) => ({
|
||||
name: statusName,
|
||||
count,
|
||||
percentage: Math.round((count / total) * 100),
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}, [vaultStats.statusDistribution]);
|
||||
|
||||
const availableStatuses = BaseNoteStatusService.getAllAvailableStatuses();
|
||||
const statusMap = new Map(availableStatuses.map((s) => [s.name, s]));
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="status-dashboard">
|
||||
|
|
@ -217,227 +87,10 @@ export const StatusDashboard = () => {
|
|||
</div>
|
||||
|
||||
<div className="status-dashboard-content">
|
||||
{/* Current Note Section */}
|
||||
<div className="status-dashboard-section">
|
||||
<div className="status-dashboard-section-header">
|
||||
<h3>Current Note</h3>
|
||||
</div>
|
||||
<div className="status-dashboard-current-note">
|
||||
{currentNote.file ? (
|
||||
<>
|
||||
<div className="current-note-info">
|
||||
<div className="current-note-name">
|
||||
{currentNote.file.basename}
|
||||
</div>
|
||||
<div className="current-note-path">
|
||||
{currentNote.file.path}
|
||||
</div>
|
||||
<div className="current-note-modified">
|
||||
Last modified:{" "}
|
||||
{new Date(
|
||||
currentNote.lastModified,
|
||||
).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="current-note-statuses">
|
||||
{Object.entries(currentNote.statuses).map(
|
||||
([tag, statuses]) => (
|
||||
<div
|
||||
key={tag}
|
||||
className="current-note-tag-group"
|
||||
>
|
||||
<GroupLabel name={tag} />
|
||||
<div className="current-note-status-list">
|
||||
{statuses.length > 0 ? (
|
||||
statuses.map(
|
||||
(status) => (
|
||||
<StatusBadge
|
||||
key={
|
||||
status.name
|
||||
}
|
||||
status={
|
||||
status
|
||||
}
|
||||
/>
|
||||
),
|
||||
)
|
||||
) : (
|
||||
<span className="current-note-no-status">
|
||||
No status assigned
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="current-note-empty">
|
||||
No note currently active
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vault Overview Section */}
|
||||
<div className="status-dashboard-section">
|
||||
<div className="status-dashboard-section-header">
|
||||
<h3>Vault Overview</h3>
|
||||
</div>
|
||||
<div className="vault-overview">
|
||||
<div className="vault-stats-grid">
|
||||
<div className="vault-stat-card">
|
||||
<div className="vault-stat-number">
|
||||
{vaultStats.totalNotes}
|
||||
</div>
|
||||
<div className="vault-stat-label">
|
||||
Total Notes
|
||||
</div>
|
||||
</div>
|
||||
<div className="vault-stat-card">
|
||||
<div className="vault-stat-number">
|
||||
{vaultStats.notesWithStatus}
|
||||
</div>
|
||||
<div className="vault-stat-label">
|
||||
Notes with Status
|
||||
</div>
|
||||
</div>
|
||||
<div className="vault-stat-card">
|
||||
<div className="vault-stat-number">
|
||||
{vaultStats.totalNotes > 0
|
||||
? Math.round(
|
||||
(vaultStats.notesWithStatus /
|
||||
vaultStats.totalNotes) *
|
||||
100,
|
||||
)
|
||||
: 0}
|
||||
%
|
||||
</div>
|
||||
<div className="vault-stat-label">Coverage</div>
|
||||
</div>
|
||||
<div className="vault-stat-card">
|
||||
<div className="vault-stat-number">
|
||||
{Object.values(
|
||||
vaultStats.statusDistribution,
|
||||
).reduce((sum, count) => sum + count, 0)}
|
||||
</div>
|
||||
<div className="vault-stat-label">
|
||||
Total Statuses
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Distribution Section */}
|
||||
<div className="status-dashboard-section">
|
||||
<div className="status-dashboard-section-header">
|
||||
<h3>Status Distribution</h3>
|
||||
</div>
|
||||
<div className="status-distribution">
|
||||
{statusChart.length > 0 ? (
|
||||
<div className="status-chart">
|
||||
{statusChart.map(
|
||||
({ name, count, percentage }) => {
|
||||
const status = statusMap.get(name);
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className="status-chart-item"
|
||||
>
|
||||
<div className="status-chart-info">
|
||||
{status && (
|
||||
<StatusBadge
|
||||
status={status}
|
||||
/>
|
||||
)}
|
||||
<span className="status-chart-count">
|
||||
{count} notes (
|
||||
{percentage}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="status-chart-bar">
|
||||
<div
|
||||
className="status-chart-fill"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
backgroundColor:
|
||||
status?.color ||
|
||||
"var(--interactive-accent)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="status-distribution-empty">
|
||||
No status data available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions Section */}
|
||||
<div className="status-dashboard-section">
|
||||
<div className="status-dashboard-section-header">
|
||||
<h3>Quick Actions</h3>
|
||||
</div>
|
||||
<div className="quick-actions">
|
||||
<button
|
||||
className="quick-action-btn"
|
||||
onClick={() => {
|
||||
const leaf =
|
||||
BaseNoteStatusService.app.workspace.getLeaf();
|
||||
leaf.setViewState({
|
||||
type: "grouped-status-view",
|
||||
active: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
📊 View Grouped Statuses
|
||||
</button>
|
||||
<button
|
||||
className="quick-action-btn"
|
||||
onClick={() => {
|
||||
// Find notes without status
|
||||
const files =
|
||||
BaseNoteStatusService.app.vault.getMarkdownFiles();
|
||||
const filesWithoutStatus = files.filter(
|
||||
(file) => {
|
||||
const cachedMetadata =
|
||||
BaseNoteStatusService.app.metadataCache.getFileCache(
|
||||
file,
|
||||
);
|
||||
const frontmatter =
|
||||
cachedMetadata?.frontmatter;
|
||||
return (
|
||||
!frontmatter ||
|
||||
!frontmatter[
|
||||
settingsService.settings
|
||||
.tagPrefix
|
||||
]
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Found ${filesWithoutStatus.length} notes without status:`,
|
||||
filesWithoutStatus.map((f) => f.path),
|
||||
);
|
||||
}}
|
||||
>
|
||||
🔍 Find Notes Without Status
|
||||
</button>
|
||||
<button className="quick-action-btn" onClick={loadData}>
|
||||
🔄 Refresh Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<CurrentNoteSection currentNote={currentNote} />
|
||||
<VaultStatsCard vaultStats={vaultStats} />
|
||||
<StatusDistributionChart vaultStats={vaultStats} />
|
||||
<QuickActionsPanel onRefresh={loadData} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
76
components/StatusDashboard/StatusDistributionChart.tsx
Normal file
76
components/StatusDashboard/StatusDistributionChart.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { useMemo } from "react";
|
||||
import { BaseNoteStatusService } from "@/core/noteStatusService";
|
||||
import { StatusBadge } from "@/components/atoms/StatusBadge";
|
||||
import { VaultStats } from "./useVaultStats";
|
||||
|
||||
interface StatusDistributionChartProps {
|
||||
vaultStats: VaultStats;
|
||||
}
|
||||
|
||||
export const StatusDistributionChart = ({
|
||||
vaultStats,
|
||||
}: StatusDistributionChartProps) => {
|
||||
const statusChart = useMemo(() => {
|
||||
const total = Object.values(vaultStats.statusDistribution).reduce(
|
||||
(sum, count) => sum + count,
|
||||
0,
|
||||
);
|
||||
if (total === 0) return [];
|
||||
|
||||
return Object.entries(vaultStats.statusDistribution)
|
||||
.filter(([_, count]) => count > 0)
|
||||
.map(([statusName, count]) => ({
|
||||
name: statusName,
|
||||
count,
|
||||
percentage: Math.round((count / total) * 100),
|
||||
}))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}, [vaultStats.statusDistribution]);
|
||||
|
||||
const availableStatuses = BaseNoteStatusService.getAllAvailableStatuses();
|
||||
const statusMap = new Map(availableStatuses.map((s) => [s.name, s]));
|
||||
|
||||
return (
|
||||
<div className="status-dashboard-section">
|
||||
<div className="status-dashboard-section-header">
|
||||
<h3>Status Distribution</h3>
|
||||
</div>
|
||||
<div className="status-distribution">
|
||||
{statusChart.length > 0 ? (
|
||||
<div className="status-chart">
|
||||
{statusChart.map(({ name, count, percentage }) => {
|
||||
const status = statusMap.get(name);
|
||||
return (
|
||||
<div key={name} className="status-chart-item">
|
||||
<div className="status-chart-info">
|
||||
{status && (
|
||||
<StatusBadge status={status} />
|
||||
)}
|
||||
<span className="status-chart-count">
|
||||
{count} notes ({percentage}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="status-chart-bar">
|
||||
<div
|
||||
className="status-chart-fill"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
backgroundColor:
|
||||
status?.color ||
|
||||
"var(--interactive-accent)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="status-distribution-empty">
|
||||
No status data available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
54
components/StatusDashboard/VaultStatsCard.tsx
Normal file
54
components/StatusDashboard/VaultStatsCard.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { VaultStats } from "./useVaultStats";
|
||||
|
||||
interface VaultStatsCardProps {
|
||||
vaultStats: VaultStats;
|
||||
}
|
||||
|
||||
export const VaultStatsCard = ({ vaultStats }: VaultStatsCardProps) => {
|
||||
return (
|
||||
<div className="status-dashboard-section">
|
||||
<div className="status-dashboard-section-header">
|
||||
<h3>Vault Overview</h3>
|
||||
</div>
|
||||
<div className="vault-overview">
|
||||
<div className="vault-stats-grid">
|
||||
<div className="vault-stat-card">
|
||||
<div className="vault-stat-number">
|
||||
{vaultStats.totalNotes}
|
||||
</div>
|
||||
<div className="vault-stat-label">Total Notes</div>
|
||||
</div>
|
||||
<div className="vault-stat-card">
|
||||
<div className="vault-stat-number">
|
||||
{vaultStats.notesWithStatus}
|
||||
</div>
|
||||
<div className="vault-stat-label">
|
||||
Notes with Status
|
||||
</div>
|
||||
</div>
|
||||
<div className="vault-stat-card">
|
||||
<div className="vault-stat-number">
|
||||
{vaultStats.totalNotes > 0
|
||||
? Math.round(
|
||||
(vaultStats.notesWithStatus /
|
||||
vaultStats.totalNotes) *
|
||||
100,
|
||||
)
|
||||
: 0}
|
||||
%
|
||||
</div>
|
||||
<div className="vault-stat-label">Coverage</div>
|
||||
</div>
|
||||
<div className="vault-stat-card">
|
||||
<div className="vault-stat-number">
|
||||
{Object.values(
|
||||
vaultStats.statusDistribution,
|
||||
).reduce((sum, count) => sum + count, 0)}
|
||||
</div>
|
||||
<div className="vault-stat-label">Total Statuses</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
44
components/StatusDashboard/useCurrentNote.tsx
Normal file
44
components/StatusDashboard/useCurrentNote.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { useState, useCallback } from "react";
|
||||
import { TFile } from "obsidian";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import {
|
||||
BaseNoteStatusService,
|
||||
NoteStatusService,
|
||||
} from "@/core/noteStatusService";
|
||||
|
||||
interface CurrentNoteInfo {
|
||||
file: TFile | null;
|
||||
statuses: Record<string, NoteStatus[]>;
|
||||
lastModified: number;
|
||||
}
|
||||
|
||||
export const useCurrentNote = () => {
|
||||
const [currentNote, setCurrentNote] = useState<CurrentNoteInfo>({
|
||||
file: null,
|
||||
statuses: {},
|
||||
lastModified: 0,
|
||||
});
|
||||
|
||||
const updateCurrentNote = useCallback(() => {
|
||||
const activeFile = BaseNoteStatusService.app.workspace.getActiveFile();
|
||||
|
||||
if (!activeFile) {
|
||||
setCurrentNote({ file: null, statuses: {}, lastModified: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
const noteStatusService = new NoteStatusService(activeFile);
|
||||
noteStatusService.populateStatuses();
|
||||
|
||||
setCurrentNote({
|
||||
file: activeFile,
|
||||
statuses: noteStatusService.statuses,
|
||||
lastModified: activeFile.stat.mtime,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
currentNote,
|
||||
updateCurrentNote,
|
||||
};
|
||||
};
|
||||
95
components/StatusDashboard/useVaultStats.tsx
Normal file
95
components/StatusDashboard/useVaultStats.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { useState, useCallback } from "react";
|
||||
import { TFile } from "obsidian";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { BaseNoteStatusService } from "@/core/noteStatusService";
|
||||
import settingsService from "@/core/settingsService";
|
||||
|
||||
export interface VaultStats {
|
||||
totalNotes: number;
|
||||
notesWithStatus: number;
|
||||
statusDistribution: Record<string, number>;
|
||||
tagDistribution: Record<string, number>;
|
||||
recentChanges: Array<{
|
||||
file: TFile;
|
||||
status: NoteStatus;
|
||||
timestamp: number;
|
||||
action: "added" | "removed";
|
||||
}>;
|
||||
}
|
||||
|
||||
export const useVaultStats = () => {
|
||||
const [vaultStats, setVaultStats] = useState<VaultStats>({
|
||||
totalNotes: 0,
|
||||
notesWithStatus: 0,
|
||||
statusDistribution: {},
|
||||
tagDistribution: {},
|
||||
recentChanges: [],
|
||||
});
|
||||
|
||||
const calculateVaultStats = useCallback((): VaultStats => {
|
||||
const files = BaseNoteStatusService.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 =
|
||||
BaseNoteStatusService.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: [],
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateVaultStats = useCallback(() => {
|
||||
const stats = calculateVaultStats();
|
||||
setVaultStats(stats);
|
||||
}, [calculateVaultStats]);
|
||||
|
||||
return {
|
||||
vaultStats,
|
||||
updateVaultStats,
|
||||
};
|
||||
};
|
||||
14
styles.css
14
styles.css
|
|
@ -532,33 +532,33 @@
|
|||
hyphens: auto;
|
||||
}
|
||||
|
||||
/* styles/components/groupped-status-view.css */
|
||||
.groupped-status-view-container {
|
||||
/* styles/components/grouped-status-view.css */
|
||||
.grouped-status-view-container {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.groupped-status-view {
|
||||
.grouped-status-view {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
.groupped-status-header {
|
||||
.grouped-status-header {
|
||||
padding: var(--size-4-2) var(--size-4-3);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.groupped-status-filters {
|
||||
.grouped-status-filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
.groupped-status-note-filter {
|
||||
.grouped-status-note-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.groupped-status-note-input {
|
||||
.grouped-status-note-input {
|
||||
width: 100%;
|
||||
padding: var(--size-2-1) var(--size-2-3);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
|
|
|
|||
Loading…
Reference in a new issue