mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-26 23:43:40 +00:00
feat: add document naming dialog before creation
Implements a user-friendly dialog that prompts for document names before creating new documents, replacing the default "Untitled Analysis" behavior. Features: - InputDialog component for text input with validation - useCreateDocument hook to centralize naming logic - Pre-filled default value "Untitled Analysis" - Validation to prevent empty document names - Helpful placeholder text with examples - Keyboard shortcuts (Enter/Escape) - Auto-focus and select input field Updated all document creation entry points: - Menu Bar: "New Document" and "New from Template" - Document Tabs: "+" button - Document Manager: "New Document" button - Empty State: "New Document" button - Keyboard shortcut: Ctrl+N This provides a consistent UX across the application and reduces code duplication by using a single reusable hook. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3a64d37f02
commit
c1a2d926cd
9 changed files with 343 additions and 29 deletions
|
|
@ -14,6 +14,7 @@ import { useGlobalShortcuts } from "./hooks/useGlobalShortcuts";
|
|||
import { useDocumentHistory } from "./hooks/useDocumentHistory";
|
||||
import { useWorkspaceStore } from "./stores/workspaceStore";
|
||||
import { usePanelStore } from "./stores/panelStore";
|
||||
import { useCreateDocument } from "./hooks/useCreateDocument";
|
||||
import type { Actor, Relation } from "./types";
|
||||
import type { ExportOptions } from "./utils/graphExport";
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ function AppContent() {
|
|||
const { undo, redo } = useDocumentHistory();
|
||||
const { activeDocumentId } = useWorkspaceStore();
|
||||
const { leftPanelVisible, rightPanelVisible } = usePanelStore();
|
||||
const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
||||
const [showDocumentManager, setShowDocumentManager] = useState(false);
|
||||
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
|
||||
const [selectedNode, setSelectedNode] = useState<Actor | null>(null);
|
||||
|
|
@ -80,6 +82,7 @@ function AppContent() {
|
|||
useGlobalShortcuts({
|
||||
onUndo: undo,
|
||||
onRedo: redo,
|
||||
onNewDocument: handleNewDocument,
|
||||
onOpenDocumentManager: () => setShowDocumentManager(true),
|
||||
onOpenHelp: () => setShowKeyboardHelp(true),
|
||||
onFitView: handleFitView,
|
||||
|
|
@ -212,6 +215,9 @@ function AppContent() {
|
|||
|
||||
{/* Toast Notifications */}
|
||||
<ToastContainer />
|
||||
|
||||
{/* New Document Dialog */}
|
||||
{NewDocumentDialog}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
190
src/components/Common/InputDialog.tsx
Normal file
190
src/components/Common/InputDialog.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import { useEffect, useState, useRef } from 'react';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
|
||||
/**
|
||||
* InputDialog - Input prompt dialog
|
||||
*
|
||||
* A modal dialog component for getting text input from the user.
|
||||
*
|
||||
* Features:
|
||||
* - Customizable title and message
|
||||
* - Optional placeholder text
|
||||
* - Input validation
|
||||
* - Keyboard support (Enter to confirm, Escape to cancel)
|
||||
* - Auto-focus on input field
|
||||
* - Backdrop click to cancel
|
||||
*/
|
||||
|
||||
export type InputDialogSeverity = 'info' | 'error';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message?: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
severity?: InputDialogSeverity;
|
||||
validateInput?: (value: string) => string | null; // Returns error message or null if valid
|
||||
onConfirm: (value: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const InputDialog = ({
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
placeholder = '',
|
||||
defaultValue = '',
|
||||
confirmLabel = 'OK',
|
||||
cancelLabel = 'Cancel',
|
||||
severity = 'info',
|
||||
validateInput,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: Props) => {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Reset value and error when dialog opens/closes
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setValue(defaultValue);
|
||||
setError(null);
|
||||
// Focus input field after a short delay to ensure it's rendered
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, 50);
|
||||
}
|
||||
}, [isOpen, defaultValue]);
|
||||
|
||||
const handleConfirm = () => {
|
||||
// Validate input if validator provided
|
||||
if (validateInput) {
|
||||
const validationError = validateInput(value);
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onConfirm(value);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setError(null);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleConfirm();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, value]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Severity-based styling
|
||||
const severityConfig = {
|
||||
info: {
|
||||
icon: <InfoIcon className="text-blue-600" sx={{ fontSize: 48 }} />,
|
||||
confirmClass: 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-500',
|
||||
},
|
||||
error: {
|
||||
icon: <ErrorIcon className="text-red-600" sx={{ fontSize: 48 }} />,
|
||||
confirmClass: 'bg-red-600 hover:bg-red-700 focus:ring-red-500',
|
||||
},
|
||||
};
|
||||
|
||||
const config = severityConfig[severity];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Content */}
|
||||
<div className="p-6">
|
||||
<div className="flex items-start space-x-4">
|
||||
{/* Icon */}
|
||||
<div className="flex-shrink-0">{config.icon}</div>
|
||||
|
||||
{/* Text Content */}
|
||||
<div className="flex-1 pt-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
{title}
|
||||
</h3>
|
||||
{message && (
|
||||
<p className="text-sm text-gray-600 mb-3 whitespace-pre-wrap">
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Input Field */}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
if (error) setError(null); // Clear error on change
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
className={`w-full px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 ${
|
||||
error
|
||||
? 'border-red-300 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:ring-blue-500'
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<p className="mt-2 text-sm text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="px-6 py-4 bg-gray-50 rounded-b-lg flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="px-4 py-2 bg-white border border-gray-300 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-50 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
className={`px-4 py-2 text-white text-sm font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 ${config.confirmClass}`}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputDialog;
|
||||
|
|
@ -26,6 +26,7 @@ import { useDocumentHistory } from "../../hooks/useDocumentHistory";
|
|||
import { useEditorStore } from "../../stores/editorStore";
|
||||
import { useActiveDocument } from "../../stores/workspace/useActiveDocument";
|
||||
import { useWorkspaceStore } from "../../stores/workspaceStore";
|
||||
import { useCreateDocument } from "../../hooks/useCreateDocument";
|
||||
import CustomNode from "../Nodes/CustomNode";
|
||||
import CustomEdge from "../Edges/CustomEdge";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
|
|
@ -63,7 +64,8 @@ interface GraphEditorProps {
|
|||
const GraphEditor = ({ onNodeSelect, onEdgeSelect, onAddNodeRequest, onExportRequest }: GraphEditorProps) => {
|
||||
// Sync with workspace active document
|
||||
const { activeDocumentId } = useActiveDocument();
|
||||
const { saveViewport, getViewport, createDocument } = useWorkspaceStore();
|
||||
const { saveViewport, getViewport } = useWorkspaceStore();
|
||||
const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
||||
|
||||
// Graph export functionality
|
||||
const { exportPNG, exportSVG } = useGraphExport();
|
||||
|
|
@ -555,14 +557,17 @@ const GraphEditor = ({ onNodeSelect, onEdgeSelect, onAddNodeRequest, onExportReq
|
|||
// Show empty state when no document is active
|
||||
if (!activeDocumentId) {
|
||||
return (
|
||||
<EmptyState
|
||||
onNewDocument={() => createDocument()}
|
||||
onOpenDocumentManager={() => {
|
||||
// This will be handled by the parent component
|
||||
// We'll trigger it via a custom event
|
||||
window.dispatchEvent(new CustomEvent("openDocumentManager"));
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<EmptyState
|
||||
onNewDocument={handleNewDocument}
|
||||
onOpenDocumentManager={() => {
|
||||
// This will be handled by the parent component
|
||||
// We'll trigger it via a custom event
|
||||
window.dispatchEvent(new CustomEvent("openDocumentManager"));
|
||||
}}
|
||||
/>
|
||||
{NewDocumentDialog}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useGraphWithHistory } from '../../hooks/useGraphWithHistory';
|
|||
import DocumentManager from '../Workspace/DocumentManager';
|
||||
import NodeTypeConfigModal from '../Config/NodeTypeConfig';
|
||||
import EdgeTypeConfigModal from '../Config/EdgeTypeConfig';
|
||||
import InputDialog from '../Common/InputDialog';
|
||||
import { useConfirm } from '../../hooks/useConfirm';
|
||||
import { useShortcutLabels } from '../../hooks/useShortcutLabels';
|
||||
import type { ExportOptions } from '../../utils/graphExport';
|
||||
|
|
@ -31,6 +32,8 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
|||
const [showDocumentManager, setShowDocumentManager] = useState(false);
|
||||
const [showNodeConfig, setShowNodeConfig] = useState(false);
|
||||
const [showEdgeConfig, setShowEdgeConfig] = useState(false);
|
||||
const [showNewDocDialog, setShowNewDocDialog] = useState(false);
|
||||
const [showNewFromTemplateDialog, setShowNewFromTemplateDialog] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { confirm, ConfirmDialogComponent } = useConfirm();
|
||||
const { getShortcutLabel } = useShortcutLabels();
|
||||
|
|
@ -72,9 +75,14 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
|||
}, []);
|
||||
|
||||
const handleNewDocument = useCallback(() => {
|
||||
createDocument();
|
||||
setShowNewDocDialog(true);
|
||||
closeMenu();
|
||||
}, [createDocument, closeMenu]);
|
||||
}, [closeMenu]);
|
||||
|
||||
const handleConfirmNewDocument = useCallback((title: string) => {
|
||||
createDocument(title);
|
||||
setShowNewDocDialog(false);
|
||||
}, [createDocument]);
|
||||
|
||||
const handleNewDocumentFromTemplate = useCallback(() => {
|
||||
if (!activeDocumentId) {
|
||||
|
|
@ -82,12 +90,18 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
|||
closeMenu();
|
||||
return;
|
||||
}
|
||||
const newDocId = createDocumentFromTemplate(activeDocumentId);
|
||||
setShowNewFromTemplateDialog(true);
|
||||
closeMenu();
|
||||
}, [activeDocumentId, closeMenu]);
|
||||
|
||||
const handleConfirmNewFromTemplate = useCallback((title: string) => {
|
||||
if (!activeDocumentId) return;
|
||||
const newDocId = createDocumentFromTemplate(activeDocumentId, title);
|
||||
if (newDocId) {
|
||||
switchToDocument(newDocId);
|
||||
}
|
||||
closeMenu();
|
||||
}, [createDocumentFromTemplate, activeDocumentId, switchToDocument, closeMenu]);
|
||||
setShowNewFromTemplateDialog(false);
|
||||
}, [createDocumentFromTemplate, activeDocumentId, switchToDocument]);
|
||||
|
||||
const handleOpenDocumentManager = useCallback(() => {
|
||||
setShowDocumentManager(true);
|
||||
|
|
@ -404,6 +418,36 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
|||
onClose={() => setShowEdgeConfig(false)}
|
||||
/>
|
||||
|
||||
{/* Input Dialogs */}
|
||||
<InputDialog
|
||||
isOpen={showNewDocDialog}
|
||||
title="New Document"
|
||||
message="Enter a name for the new document:"
|
||||
placeholder="e.g., Team Analysis, Project Relationships..."
|
||||
defaultValue="Untitled Analysis"
|
||||
confirmLabel="Create"
|
||||
onConfirm={handleConfirmNewDocument}
|
||||
onCancel={() => setShowNewDocDialog(false)}
|
||||
validateInput={(value) => {
|
||||
if (!value.trim()) return 'Document name cannot be empty';
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
<InputDialog
|
||||
isOpen={showNewFromTemplateDialog}
|
||||
title="New Document from Template"
|
||||
message="Enter a name for the new document:"
|
||||
placeholder="e.g., Team Analysis, Project Relationships..."
|
||||
defaultValue="Untitled Analysis"
|
||||
confirmLabel="Create"
|
||||
onConfirm={handleConfirmNewFromTemplate}
|
||||
onCancel={() => setShowNewFromTemplateDialog(false)}
|
||||
validateInput={(value) => {
|
||||
if (!value.trim()) return 'Document name cannot be empty';
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
{ConfirmDialogComponent}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import FolderZipIcon from '@mui/icons-material/FolderZip';
|
|||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
||||
import { useConfirm } from '../../hooks/useConfirm';
|
||||
import { useCreateDocument } from '../../hooks/useCreateDocument';
|
||||
import DocumentCard from './DocumentCard';
|
||||
|
||||
/**
|
||||
|
|
@ -29,7 +30,6 @@ const DocumentManager = ({ isOpen, onClose }: DocumentManagerProps) => {
|
|||
const {
|
||||
documentMetadata,
|
||||
documentOrder,
|
||||
createDocument,
|
||||
switchToDocument,
|
||||
duplicateDocument,
|
||||
exportDocument,
|
||||
|
|
@ -41,6 +41,7 @@ const DocumentManager = ({ isOpen, onClose }: DocumentManagerProps) => {
|
|||
} = useWorkspaceStore();
|
||||
|
||||
const { confirm, ConfirmDialogComponent } = useConfirm();
|
||||
const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
// Get all document IDs from metadata (includes both open and closed documents)
|
||||
|
|
@ -68,11 +69,10 @@ const DocumentManager = ({ isOpen, onClose }: DocumentManagerProps) => {
|
|||
});
|
||||
}, [allDocumentIds, documentMetadata, searchQuery]);
|
||||
|
||||
const handleNewDocument = useCallback(() => {
|
||||
const newDocId = createDocument();
|
||||
switchToDocument(newDocId);
|
||||
const handleNewDocumentClick = useCallback(() => {
|
||||
handleNewDocument();
|
||||
onClose();
|
||||
}, [createDocument, switchToDocument, onClose]);
|
||||
}, [handleNewDocument, onClose]);
|
||||
|
||||
const handleImportDocument = useCallback(async () => {
|
||||
const newDocId = await importDocumentFromFile();
|
||||
|
|
@ -158,7 +158,7 @@ const DocumentManager = ({ isOpen, onClose }: DocumentManagerProps) => {
|
|||
<div className="flex flex-col gap-3 px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
<button
|
||||
onClick={handleNewDocument}
|
||||
onClick={handleNewDocumentClick}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<AddIcon sx={{ fontSize: 20 }} />
|
||||
|
|
@ -219,7 +219,7 @@ const DocumentManager = ({ isOpen, onClose }: DocumentManagerProps) => {
|
|||
<div className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||
<p className="text-lg mb-4">No documents yet</p>
|
||||
<button
|
||||
onClick={handleNewDocument}
|
||||
onClick={handleNewDocumentClick}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Create your first document
|
||||
|
|
@ -273,6 +273,9 @@ const DocumentManager = ({ isOpen, onClose }: DocumentManagerProps) => {
|
|||
|
||||
{/* Confirmation Dialog */}
|
||||
{ConfirmDialogComponent}
|
||||
|
||||
{/* New Document Dialog */}
|
||||
{NewDocumentDialog}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
||||
import { useCreateDocument } from '../../hooks/useCreateDocument';
|
||||
import Tab from './Tab';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
|
|
@ -29,13 +30,14 @@ const DocumentTabs = () => {
|
|||
switchToDocument,
|
||||
closeDocument,
|
||||
renameDocument,
|
||||
createDocument,
|
||||
reorderDocuments,
|
||||
duplicateDocument,
|
||||
exportDocument,
|
||||
deleteDocument,
|
||||
} = useWorkspaceStore();
|
||||
|
||||
const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
||||
|
||||
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
x: number;
|
||||
|
|
@ -67,10 +69,6 @@ const DocumentTabs = () => {
|
|||
[renameDocument]
|
||||
);
|
||||
|
||||
const handleNewDocument = useCallback(() => {
|
||||
createDocument();
|
||||
}, [createDocument]);
|
||||
|
||||
const handleDragStart = useCallback((index: number) => {
|
||||
setDraggedIndex(index);
|
||||
}, []);
|
||||
|
|
@ -220,6 +218,9 @@ const DocumentTabs = () => {
|
|||
onClose={() => setContextMenu(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* New Document Dialog */}
|
||||
{NewDocumentDialog}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
65
src/hooks/useCreateDocument.tsx
Normal file
65
src/hooks/useCreateDocument.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { useWorkspaceStore } from '../stores/workspaceStore';
|
||||
import InputDialog from '../components/Common/InputDialog';
|
||||
|
||||
/**
|
||||
* useCreateDocument Hook
|
||||
*
|
||||
* Provides a consistent document creation flow with naming dialog
|
||||
* across the application. Returns both the handler function and
|
||||
* the dialog component to render.
|
||||
*
|
||||
* Usage:
|
||||
* ```tsx
|
||||
* const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
||||
*
|
||||
* return (
|
||||
* <>
|
||||
* <button onClick={handleNewDocument}>New Document</button>
|
||||
* {NewDocumentDialog}
|
||||
* </>
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function useCreateDocument() {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const { createDocument } = useWorkspaceStore();
|
||||
|
||||
const handleNewDocument = useCallback(() => {
|
||||
setShowDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirm = useCallback(
|
||||
(title: string) => {
|
||||
createDocument(title);
|
||||
setShowDialog(false);
|
||||
},
|
||||
[createDocument]
|
||||
);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setShowDialog(false);
|
||||
}, []);
|
||||
|
||||
const NewDocumentDialog = (
|
||||
<InputDialog
|
||||
isOpen={showDialog}
|
||||
title="New Document"
|
||||
message="Enter a name for the new document:"
|
||||
placeholder="e.g., Team Analysis, Project Relationships..."
|
||||
defaultValue="Untitled Analysis"
|
||||
confirmLabel="Create"
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
validateInput={(value) => {
|
||||
if (!value.trim()) return 'Document name cannot be empty';
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return {
|
||||
handleNewDocument,
|
||||
NewDocumentDialog,
|
||||
};
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import type { KeyboardShortcut } from "./useKeyboardShortcutManager";
|
|||
*/
|
||||
|
||||
interface UseGlobalShortcutsOptions {
|
||||
onNewDocument?: () => void;
|
||||
onOpenDocumentManager?: () => void;
|
||||
onUndo?: () => void;
|
||||
onRedo?: () => void;
|
||||
|
|
@ -26,7 +27,6 @@ export function useGlobalShortcuts(options: UseGlobalShortcutsOptions = {}) {
|
|||
activeDocumentId,
|
||||
switchToDocument,
|
||||
closeDocument,
|
||||
createDocument,
|
||||
saveDocument,
|
||||
} = useWorkspaceStore();
|
||||
|
||||
|
|
@ -38,8 +38,9 @@ export function useGlobalShortcuts(options: UseGlobalShortcutsOptions = {}) {
|
|||
description: "New Document",
|
||||
key: "n",
|
||||
ctrl: true,
|
||||
handler: () => createDocument(),
|
||||
handler: () => options.onNewDocument?.(),
|
||||
category: "Document Management",
|
||||
enabled: !!options.onNewDocument,
|
||||
},
|
||||
{
|
||||
id: "open-document-manager",
|
||||
|
|
@ -201,7 +202,6 @@ export function useGlobalShortcuts(options: UseGlobalShortcutsOptions = {}) {
|
|||
activeDocumentId,
|
||||
switchToDocument,
|
||||
closeDocument,
|
||||
createDocument,
|
||||
saveDocument,
|
||||
options,
|
||||
]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue