feat: add dashboard actions

This commit is contained in:
Aleix Soler 2025-07-19 16:29:42 +02:00
parent 46de2b1c29
commit 541df4e17b
5 changed files with 346 additions and 56 deletions

View file

@ -1,58 +1,167 @@
import { BaseNoteStatusService } from "@/core/noteStatusService";
import settingsService from "@/core/settingsService";
export type DashboardAction =
| "refresh"
| "open-grouped-view"
| "find-unassigned"
| "change-status"
| "insert-metadata"
| "cycle-status"
| "clear-status"
| "copy-status"
| "paste-status"
| "search-by-status"
| "toggle-multiple-mode"
| "set-quick-status";
interface QuickActionsPanelProps {
onRefresh: () => void;
hasCurrentFile: boolean;
useMultipleStatuses: boolean;
quickStatusCommands: Array<{ name: string; command: string }>;
availableStatuses: Array<{ name: string }>;
onAction: (action: DashboardAction, value?: string) => void;
}
export const QuickActionsPanel = ({ onRefresh }: QuickActionsPanelProps) => {
export const QuickActionsPanel = ({
hasCurrentFile,
useMultipleStatuses,
quickStatusCommands,
availableStatuses,
onAction,
}: 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]
);
});
{/* Views Group */}
<div className="quick-actions-group">
<div className="quick-actions-group-title">📊 Views</div>
<button
className="quick-action-btn"
onClick={() => onAction("open-grouped-view")}
title="Open grouped status view"
>
📊 Grouped Statuses
</button>
<button
className="quick-action-btn"
onClick={() => onAction("find-unassigned")}
title="Find notes without status"
>
🔍 Find Unassigned Notes
</button>
</div>
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>
{/* Quick Status Group - Only show when not in multi-status mode and has quick statuses */}
{!useMultipleStatuses && quickStatusCommands.length > 0 && (
<div className="quick-actions-group">
<div className="quick-actions-group-title">
Quick Status
</div>
{quickStatusCommands.map((status) => (
<button
key={status.name}
className="quick-action-btn"
onClick={() =>
onAction("set-quick-status", status.name)
}
disabled={!hasCurrentFile}
title={`Set status to ${status.name}`}
>
{status.name}
</button>
))}
</div>
)}
{/* Current Note Group */}
<div className="quick-actions-group">
<div className="quick-actions-group-title">
📝 Current Note
</div>
<button
className="quick-action-btn"
onClick={() => onAction("change-status")}
disabled={!hasCurrentFile}
title="Change status of current note"
>
📝 Change Status
</button>
<button
className="quick-action-btn"
onClick={() => onAction("insert-metadata")}
disabled={!hasCurrentFile}
title="Insert status metadata in editor"
>
📝 Insert Metadata
</button>
<button
className="quick-action-btn"
onClick={() => onAction("cycle-status")}
disabled={!hasCurrentFile || useMultipleStatuses}
title="Cycle through available statuses"
>
📝 Cycle Status
</button>
<button
className="quick-action-btn"
onClick={() => onAction("clear-status")}
disabled={!hasCurrentFile}
title="Clear status from current note"
>
📝 Clear Status
</button>
</div>
{/* Clipboard Group */}
<div className="quick-actions-group">
<div className="quick-actions-group-title">
📋 Clipboard
</div>
<button
className="quick-action-btn"
onClick={() => onAction("copy-status")}
disabled={!hasCurrentFile}
title="Copy status to clipboard"
>
📋 Copy Status
</button>
<button
className="quick-action-btn"
onClick={() => onAction("paste-status")}
disabled={!hasCurrentFile}
title="Paste status from clipboard"
>
📋 Paste Status
</button>
</div>
{/* Tools Group */}
<div className="quick-actions-group">
<div className="quick-actions-group-title"> Tools</div>
<button
className="quick-action-btn"
onClick={() => onAction("search-by-status")}
disabled={!hasCurrentFile}
title="Search for notes with same status"
>
Search by Status
</button>
<button
className="quick-action-btn"
onClick={() => onAction("toggle-multiple-mode")}
title="Toggle multiple statuses mode"
>
Toggle Multi-Status
</button>
<button
className="quick-action-btn"
onClick={() => onAction("refresh")}
title="Refresh dashboard data"
>
Refresh Data
</button>
</div>
</div>
</div>
);

View file

@ -4,11 +4,20 @@ import eventBus from "@/core/eventBus";
import { VaultStatsCard } from "./VaultStatsCard";
import { CurrentNoteSection } from "./CurrentNoteSection";
import { StatusDistributionChart } from "./StatusDistributionChart";
import { QuickActionsPanel } from "./QuickActionsPanel";
import { QuickActionsPanel, DashboardAction } from "./QuickActionsPanel";
import { useVaultStats } from "./useVaultStats";
import { useCurrentNote } from "./useCurrentNote";
import { PluginSettings } from "@/types/pluginSettings";
export const StatusDashboard = () => {
interface StatusDashboardProps {
onAction: (action: DashboardAction, value?: string) => void;
settings: PluginSettings;
}
export const StatusDashboard = ({
onAction,
settings,
}: StatusDashboardProps) => {
const [isLoading, setIsLoading] = useState(true);
const { vaultStats, updateVaultStats } = useVaultStats();
const { currentNote, updateCurrentNote } = useCurrentNote();
@ -23,6 +32,17 @@ export const StatusDashboard = () => {
}
}, [updateVaultStats, updateCurrentNote]);
const handleAction = useCallback(
(action: DashboardAction, value?: string) => {
if (action === "refresh") {
loadData();
} else {
onAction(action, value);
}
},
[loadData, onAction],
);
useEffect(() => {
loadData();
@ -90,7 +110,18 @@ export const StatusDashboard = () => {
<CurrentNoteSection currentNote={currentNote} />
<VaultStatsCard vaultStats={vaultStats} />
<StatusDistributionChart vaultStats={vaultStats} />
<QuickActionsPanel onRefresh={loadData} />
<QuickActionsPanel
hasCurrentFile={!!currentNote}
useMultipleStatuses={settings.useMultipleStatuses}
quickStatusCommands={settings.quickStatusCommands.map(
(name) => ({
name,
command: `note-status:set-status-${name}`,
}),
)}
availableStatuses={BaseNoteStatusService.getAllAvailableStatuses()}
onAction={handleAction}
/>
</div>
</div>
);

View file

@ -9,6 +9,8 @@ interface BaseInputProps {
placeholder?: string;
className?: string;
style?: React.CSSProperties;
onFocus?: () => void;
onBlur?: () => void;
}
interface TextInputProps extends BaseInputProps {
@ -57,7 +59,19 @@ const getInputStyles = (variant: InputVariant): React.CSSProperties => {
};
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ variant, value, onChange, placeholder, className, style }, ref) => {
(
{
variant,
value,
onChange,
placeholder,
className,
style,
onFocus,
onBlur,
},
ref,
) => {
const inputStyles = getInputStyles(variant);
const combinedStyles = { ...inputStyles, ...style };
@ -70,6 +84,8 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
placeholder={placeholder}
className={className}
style={combinedStyles}
onFocus={onFocus}
onBlur={onBlur}
/>
);
},

View file

@ -1,6 +1,15 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import { ItemView, WorkspaceLeaf, Notice, App } from "obsidian";
import { Root, createRoot } from "react-dom/client";
import { StatusDashboard } from "@/components/StatusDashboard/StatusDashboard";
import { DashboardAction } from "@/components/StatusDashboard/QuickActionsPanel";
import { BaseNoteStatusService } from "@/core/noteStatusService";
import settingsService from "@/core/settingsService";
interface AppWithCommands extends App {
commands: {
executeCommandById(commandId: string): boolean;
};
}
export const VIEW_TYPE_STATUS_DASHBOARD = "status-dashboard-view";
@ -23,13 +32,105 @@ export class StatusDashboardView extends ItemView {
return "bar-chart-2";
}
private handleAction = (action: DashboardAction, value?: string) => {
const appWithCommands = this.app as AppWithCommands;
switch (action) {
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;
}
};
private openGroupedView() {
const leaf = BaseNoteStatusService.app.workspace.getLeaf();
leaf.setViewState({ type: "grouped-status-view", active: true });
}
private findUnassignedNotes() {
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]
);
});
new Notice(`Found ${filesWithoutStatus.length} notes without status`);
console.log(
"Notes without status:",
filesWithoutStatus.map((f) => f.path),
);
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
container.addClass("status-dashboard-view-container");
this.root = createRoot(container);
this.root.render(<StatusDashboard />);
this.root.render(
<StatusDashboard
onAction={this.handleAction}
settings={settingsService.settings}
/>,
);
}
async onClose() {

View file

@ -229,14 +229,30 @@
/* Quick Actions */
.quick-actions {
padding: var(--size-4-3);
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--size-4-3);
}
.quick-actions-group {
display: flex;
flex-wrap: wrap;
gap: var(--size-4-2);
flex-direction: column;
gap: var(--size-2-2);
}
.quick-actions-group-title {
font-size: var(--font-ui-small);
font-weight: var(--font-semibold);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
padding-bottom: var(--size-2-1);
border-bottom: 1px solid var(--background-modifier-border);
margin-bottom: var(--size-2-1);
}
.quick-action-btn {
flex: 1;
min-width: 200px;
width: 100%;
padding: var(--size-2-3) var(--size-4-2);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-m);
@ -245,16 +261,33 @@
cursor: pointer;
font-size: var(--font-ui-medium);
font-weight: var(--font-medium);
text-align: center;
text-align: left;
display: flex;
align-items: center;
gap: var(--size-2-2);
transition: all var(--anim-duration-fast) ease;
}
.quick-action-btn:hover {
.quick-action-btn:hover:not(:disabled) {
background: var(--interactive-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-s);
}
.quick-action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background: var(--background-modifier-border);
color: var(--text-faint);
}
/* Mobile responsive */
@media (max-width: 768px) {
.quick-actions {
grid-template-columns: 1fr;
}
}
/* States */
.status-dashboard-loading {
display: flex;