mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
feat: add dashboard actions
This commit is contained in:
parent
46de2b1c29
commit
541df4e17b
5 changed files with 346 additions and 56 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue