mirror of
https://github.com/OFFIS-ESC/constellation-analyzer
synced 2026-01-27 07:43:41 +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 { useDocumentHistory } from "./hooks/useDocumentHistory";
|
||||||
import { useWorkspaceStore } from "./stores/workspaceStore";
|
import { useWorkspaceStore } from "./stores/workspaceStore";
|
||||||
import { usePanelStore } from "./stores/panelStore";
|
import { usePanelStore } from "./stores/panelStore";
|
||||||
|
import { useCreateDocument } from "./hooks/useCreateDocument";
|
||||||
import type { Actor, Relation } from "./types";
|
import type { Actor, Relation } from "./types";
|
||||||
import type { ExportOptions } from "./utils/graphExport";
|
import type { ExportOptions } from "./utils/graphExport";
|
||||||
|
|
||||||
|
|
@ -41,6 +42,7 @@ function AppContent() {
|
||||||
const { undo, redo } = useDocumentHistory();
|
const { undo, redo } = useDocumentHistory();
|
||||||
const { activeDocumentId } = useWorkspaceStore();
|
const { activeDocumentId } = useWorkspaceStore();
|
||||||
const { leftPanelVisible, rightPanelVisible } = usePanelStore();
|
const { leftPanelVisible, rightPanelVisible } = usePanelStore();
|
||||||
|
const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
||||||
const [showDocumentManager, setShowDocumentManager] = useState(false);
|
const [showDocumentManager, setShowDocumentManager] = useState(false);
|
||||||
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
|
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false);
|
||||||
const [selectedNode, setSelectedNode] = useState<Actor | null>(null);
|
const [selectedNode, setSelectedNode] = useState<Actor | null>(null);
|
||||||
|
|
@ -80,6 +82,7 @@ function AppContent() {
|
||||||
useGlobalShortcuts({
|
useGlobalShortcuts({
|
||||||
onUndo: undo,
|
onUndo: undo,
|
||||||
onRedo: redo,
|
onRedo: redo,
|
||||||
|
onNewDocument: handleNewDocument,
|
||||||
onOpenDocumentManager: () => setShowDocumentManager(true),
|
onOpenDocumentManager: () => setShowDocumentManager(true),
|
||||||
onOpenHelp: () => setShowKeyboardHelp(true),
|
onOpenHelp: () => setShowKeyboardHelp(true),
|
||||||
onFitView: handleFitView,
|
onFitView: handleFitView,
|
||||||
|
|
@ -212,6 +215,9 @@ function AppContent() {
|
||||||
|
|
||||||
{/* Toast Notifications */}
|
{/* Toast Notifications */}
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
|
{/* New Document Dialog */}
|
||||||
|
{NewDocumentDialog}
|
||||||
</div>
|
</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 { useEditorStore } from "../../stores/editorStore";
|
||||||
import { useActiveDocument } from "../../stores/workspace/useActiveDocument";
|
import { useActiveDocument } from "../../stores/workspace/useActiveDocument";
|
||||||
import { useWorkspaceStore } from "../../stores/workspaceStore";
|
import { useWorkspaceStore } from "../../stores/workspaceStore";
|
||||||
|
import { useCreateDocument } from "../../hooks/useCreateDocument";
|
||||||
import CustomNode from "../Nodes/CustomNode";
|
import CustomNode from "../Nodes/CustomNode";
|
||||||
import CustomEdge from "../Edges/CustomEdge";
|
import CustomEdge from "../Edges/CustomEdge";
|
||||||
import ContextMenu from "./ContextMenu";
|
import ContextMenu from "./ContextMenu";
|
||||||
|
|
@ -63,7 +64,8 @@ interface GraphEditorProps {
|
||||||
const GraphEditor = ({ onNodeSelect, onEdgeSelect, onAddNodeRequest, onExportRequest }: GraphEditorProps) => {
|
const GraphEditor = ({ onNodeSelect, onEdgeSelect, onAddNodeRequest, onExportRequest }: GraphEditorProps) => {
|
||||||
// Sync with workspace active document
|
// Sync with workspace active document
|
||||||
const { activeDocumentId } = useActiveDocument();
|
const { activeDocumentId } = useActiveDocument();
|
||||||
const { saveViewport, getViewport, createDocument } = useWorkspaceStore();
|
const { saveViewport, getViewport } = useWorkspaceStore();
|
||||||
|
const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
||||||
|
|
||||||
// Graph export functionality
|
// Graph export functionality
|
||||||
const { exportPNG, exportSVG } = useGraphExport();
|
const { exportPNG, exportSVG } = useGraphExport();
|
||||||
|
|
@ -555,14 +557,17 @@ const GraphEditor = ({ onNodeSelect, onEdgeSelect, onAddNodeRequest, onExportReq
|
||||||
// Show empty state when no document is active
|
// Show empty state when no document is active
|
||||||
if (!activeDocumentId) {
|
if (!activeDocumentId) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
onNewDocument={() => createDocument()}
|
onNewDocument={handleNewDocument}
|
||||||
onOpenDocumentManager={() => {
|
onOpenDocumentManager={() => {
|
||||||
// This will be handled by the parent component
|
// This will be handled by the parent component
|
||||||
// We'll trigger it via a custom event
|
// We'll trigger it via a custom event
|
||||||
window.dispatchEvent(new CustomEvent("openDocumentManager"));
|
window.dispatchEvent(new CustomEvent("openDocumentManager"));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{NewDocumentDialog}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useGraphWithHistory } from '../../hooks/useGraphWithHistory';
|
||||||
import DocumentManager from '../Workspace/DocumentManager';
|
import DocumentManager from '../Workspace/DocumentManager';
|
||||||
import NodeTypeConfigModal from '../Config/NodeTypeConfig';
|
import NodeTypeConfigModal from '../Config/NodeTypeConfig';
|
||||||
import EdgeTypeConfigModal from '../Config/EdgeTypeConfig';
|
import EdgeTypeConfigModal from '../Config/EdgeTypeConfig';
|
||||||
|
import InputDialog from '../Common/InputDialog';
|
||||||
import { useConfirm } from '../../hooks/useConfirm';
|
import { useConfirm } from '../../hooks/useConfirm';
|
||||||
import { useShortcutLabels } from '../../hooks/useShortcutLabels';
|
import { useShortcutLabels } from '../../hooks/useShortcutLabels';
|
||||||
import type { ExportOptions } from '../../utils/graphExport';
|
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 [showDocumentManager, setShowDocumentManager] = useState(false);
|
||||||
const [showNodeConfig, setShowNodeConfig] = useState(false);
|
const [showNodeConfig, setShowNodeConfig] = useState(false);
|
||||||
const [showEdgeConfig, setShowEdgeConfig] = useState(false);
|
const [showEdgeConfig, setShowEdgeConfig] = useState(false);
|
||||||
|
const [showNewDocDialog, setShowNewDocDialog] = useState(false);
|
||||||
|
const [showNewFromTemplateDialog, setShowNewFromTemplateDialog] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const { confirm, ConfirmDialogComponent } = useConfirm();
|
const { confirm, ConfirmDialogComponent } = useConfirm();
|
||||||
const { getShortcutLabel } = useShortcutLabels();
|
const { getShortcutLabel } = useShortcutLabels();
|
||||||
|
|
@ -72,9 +75,14 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleNewDocument = useCallback(() => {
|
const handleNewDocument = useCallback(() => {
|
||||||
createDocument();
|
setShowNewDocDialog(true);
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}, [createDocument, closeMenu]);
|
}, [closeMenu]);
|
||||||
|
|
||||||
|
const handleConfirmNewDocument = useCallback((title: string) => {
|
||||||
|
createDocument(title);
|
||||||
|
setShowNewDocDialog(false);
|
||||||
|
}, [createDocument]);
|
||||||
|
|
||||||
const handleNewDocumentFromTemplate = useCallback(() => {
|
const handleNewDocumentFromTemplate = useCallback(() => {
|
||||||
if (!activeDocumentId) {
|
if (!activeDocumentId) {
|
||||||
|
|
@ -82,12 +90,18 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
||||||
closeMenu();
|
closeMenu();
|
||||||
return;
|
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) {
|
if (newDocId) {
|
||||||
switchToDocument(newDocId);
|
switchToDocument(newDocId);
|
||||||
}
|
}
|
||||||
closeMenu();
|
setShowNewFromTemplateDialog(false);
|
||||||
}, [createDocumentFromTemplate, activeDocumentId, switchToDocument, closeMenu]);
|
}, [createDocumentFromTemplate, activeDocumentId, switchToDocument]);
|
||||||
|
|
||||||
const handleOpenDocumentManager = useCallback(() => {
|
const handleOpenDocumentManager = useCallback(() => {
|
||||||
setShowDocumentManager(true);
|
setShowDocumentManager(true);
|
||||||
|
|
@ -404,6 +418,36 @@ const MenuBar: React.FC<MenuBarProps> = ({ onOpenHelp, onFitView, onSelectAll, o
|
||||||
onClose={() => setShowEdgeConfig(false)}
|
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 */}
|
{/* Confirmation Dialog */}
|
||||||
{ConfirmDialogComponent}
|
{ConfirmDialogComponent}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import FolderZipIcon from '@mui/icons-material/FolderZip';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
||||||
import { useConfirm } from '../../hooks/useConfirm';
|
import { useConfirm } from '../../hooks/useConfirm';
|
||||||
|
import { useCreateDocument } from '../../hooks/useCreateDocument';
|
||||||
import DocumentCard from './DocumentCard';
|
import DocumentCard from './DocumentCard';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -29,7 +30,6 @@ const DocumentManager = ({ isOpen, onClose }: DocumentManagerProps) => {
|
||||||
const {
|
const {
|
||||||
documentMetadata,
|
documentMetadata,
|
||||||
documentOrder,
|
documentOrder,
|
||||||
createDocument,
|
|
||||||
switchToDocument,
|
switchToDocument,
|
||||||
duplicateDocument,
|
duplicateDocument,
|
||||||
exportDocument,
|
exportDocument,
|
||||||
|
|
@ -41,6 +41,7 @@ const DocumentManager = ({ isOpen, onClose }: DocumentManagerProps) => {
|
||||||
} = useWorkspaceStore();
|
} = useWorkspaceStore();
|
||||||
|
|
||||||
const { confirm, ConfirmDialogComponent } = useConfirm();
|
const { confirm, ConfirmDialogComponent } = useConfirm();
|
||||||
|
const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
// Get all document IDs from metadata (includes both open and closed documents)
|
// Get all document IDs from metadata (includes both open and closed documents)
|
||||||
|
|
@ -68,11 +69,10 @@ const DocumentManager = ({ isOpen, onClose }: DocumentManagerProps) => {
|
||||||
});
|
});
|
||||||
}, [allDocumentIds, documentMetadata, searchQuery]);
|
}, [allDocumentIds, documentMetadata, searchQuery]);
|
||||||
|
|
||||||
const handleNewDocument = useCallback(() => {
|
const handleNewDocumentClick = useCallback(() => {
|
||||||
const newDocId = createDocument();
|
handleNewDocument();
|
||||||
switchToDocument(newDocId);
|
|
||||||
onClose();
|
onClose();
|
||||||
}, [createDocument, switchToDocument, onClose]);
|
}, [handleNewDocument, onClose]);
|
||||||
|
|
||||||
const handleImportDocument = useCallback(async () => {
|
const handleImportDocument = useCallback(async () => {
|
||||||
const newDocId = await importDocumentFromFile();
|
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 flex-col gap-3 px-6 py-4 border-b border-gray-200 bg-gray-50">
|
||||||
<div className="flex gap-3 flex-wrap">
|
<div className="flex gap-3 flex-wrap">
|
||||||
<button
|
<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"
|
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 }} />
|
<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">
|
<div className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||||
<p className="text-lg mb-4">No documents yet</p>
|
<p className="text-lg mb-4">No documents yet</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleNewDocument}
|
onClick={handleNewDocumentClick}
|
||||||
className="px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
className="px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Create your first document
|
Create your first document
|
||||||
|
|
@ -273,6 +273,9 @@ const DocumentManager = ({ isOpen, onClose }: DocumentManagerProps) => {
|
||||||
|
|
||||||
{/* Confirmation Dialog */}
|
{/* Confirmation Dialog */}
|
||||||
{ConfirmDialogComponent}
|
{ConfirmDialogComponent}
|
||||||
|
|
||||||
|
{/* New Document Dialog */}
|
||||||
|
{NewDocumentDialog}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
import { useWorkspaceStore } from '../../stores/workspaceStore';
|
||||||
|
import { useCreateDocument } from '../../hooks/useCreateDocument';
|
||||||
import Tab from './Tab';
|
import Tab from './Tab';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
|
|
@ -29,13 +30,14 @@ const DocumentTabs = () => {
|
||||||
switchToDocument,
|
switchToDocument,
|
||||||
closeDocument,
|
closeDocument,
|
||||||
renameDocument,
|
renameDocument,
|
||||||
createDocument,
|
|
||||||
reorderDocuments,
|
reorderDocuments,
|
||||||
duplicateDocument,
|
duplicateDocument,
|
||||||
exportDocument,
|
exportDocument,
|
||||||
deleteDocument,
|
deleteDocument,
|
||||||
} = useWorkspaceStore();
|
} = useWorkspaceStore();
|
||||||
|
|
||||||
|
const { handleNewDocument, NewDocumentDialog } = useCreateDocument();
|
||||||
|
|
||||||
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
||||||
const [contextMenu, setContextMenu] = useState<{
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
x: number;
|
x: number;
|
||||||
|
|
@ -67,10 +69,6 @@ const DocumentTabs = () => {
|
||||||
[renameDocument]
|
[renameDocument]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNewDocument = useCallback(() => {
|
|
||||||
createDocument();
|
|
||||||
}, [createDocument]);
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((index: number) => {
|
const handleDragStart = useCallback((index: number) => {
|
||||||
setDraggedIndex(index);
|
setDraggedIndex(index);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -220,6 +218,9 @@ const DocumentTabs = () => {
|
||||||
onClose={() => setContextMenu(null)}
|
onClose={() => setContextMenu(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* New Document Dialog */}
|
||||||
|
{NewDocumentDialog}
|
||||||
</div>
|
</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 {
|
interface UseGlobalShortcutsOptions {
|
||||||
|
onNewDocument?: () => void;
|
||||||
onOpenDocumentManager?: () => void;
|
onOpenDocumentManager?: () => void;
|
||||||
onUndo?: () => void;
|
onUndo?: () => void;
|
||||||
onRedo?: () => void;
|
onRedo?: () => void;
|
||||||
|
|
@ -26,7 +27,6 @@ export function useGlobalShortcuts(options: UseGlobalShortcutsOptions = {}) {
|
||||||
activeDocumentId,
|
activeDocumentId,
|
||||||
switchToDocument,
|
switchToDocument,
|
||||||
closeDocument,
|
closeDocument,
|
||||||
createDocument,
|
|
||||||
saveDocument,
|
saveDocument,
|
||||||
} = useWorkspaceStore();
|
} = useWorkspaceStore();
|
||||||
|
|
||||||
|
|
@ -38,8 +38,9 @@ export function useGlobalShortcuts(options: UseGlobalShortcutsOptions = {}) {
|
||||||
description: "New Document",
|
description: "New Document",
|
||||||
key: "n",
|
key: "n",
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
handler: () => createDocument(),
|
handler: () => options.onNewDocument?.(),
|
||||||
category: "Document Management",
|
category: "Document Management",
|
||||||
|
enabled: !!options.onNewDocument,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "open-document-manager",
|
id: "open-document-manager",
|
||||||
|
|
@ -201,7 +202,6 @@ export function useGlobalShortcuts(options: UseGlobalShortcutsOptions = {}) {
|
||||||
activeDocumentId,
|
activeDocumentId,
|
||||||
switchToDocument,
|
switchToDocument,
|
||||||
closeDocument,
|
closeDocument,
|
||||||
createDocument,
|
|
||||||
saveDocument,
|
saveDocument,
|
||||||
options,
|
options,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue