Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/components/CodeEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@
align-items: center;
}

.save-status {
font-size: 12px;
margin-left: 6px;
margin-right: 2px;
}

.save-status.unsaved {
color: #8c8c8c; /* VS Code gray */
}

.title-click-indicator {
font-size: 14px;
color: #0066cc;
Expand All @@ -80,6 +90,11 @@
margin-top: 4px;
}

.unsaved-indicator {
color: #ffc107;
font-weight: 500;
}

/* Markdown syntax highlighting */
.cm-header {
color: #0066cc;
Expand Down
142 changes: 128 additions & 14 deletions src/components/CodeEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const CodeEditor = () => {
const [documents, setDocuments] = useState([]);
const [currentDocument, setCurrentDocument] = useState(null);
const [content, setContent] = useState('');
const [isSaved, setIsSaved] = useState(true); // Track save status

// Initialize documents from database
useEffect(() => {
Expand All @@ -27,31 +28,130 @@ const CodeEditor = () => {
if (allDocs.length > 0) {
setCurrentDocument(allDocs[0]);
setContent(allDocs[0].content);
setIsSaved(true); // Initially loaded document is saved
}
}

loadDocuments();
}, []);
// Save current document content when it changes

// Listen for document updates (e.g., after conflict resolution)
useEffect(() => {
if (currentDocument && content !== currentDocument.content) {
const updatedDoc = {
...currentDocument,
content,
updatedAt: new Date().toISOString()
};
const handleDocumentsUpdate = async (event) => {
// If we have specific document IDs that were updated, only refresh those
const updatedDocIds = event?.detail?.documentIds;

async function saveDocument() {
const savedDoc = await DocumentManager.saveDocument(updatedDoc);
setCurrentDocument(savedDoc);
if (updatedDocIds && currentDocument) {
// Check if current document was updated
const currentDocId = currentDocument.id || currentDocument._id;
if (updatedDocIds.includes(currentDocId)) {
console.log('Current document was updated remotely, refreshing editor');
try {
const updatedCurrentDoc = await DocumentManager.getDocument(currentDocId);
if (updatedCurrentDoc && updatedCurrentDoc.updatedAt !== currentDocument.updatedAt) {
setCurrentDocument(updatedCurrentDoc);
setContent(updatedCurrentDoc.content);
setIsSaved(true); // Document updated from remote is considered saved

// Update the editor view with the new content
if (editorView) {
editorView.dispatch({
changes: {
from: 0,
to: editorView.state.doc.length,
insert: updatedCurrentDoc.content
}
});
}
}
} catch (error) {
console.error('Error refreshing current document:', error);
}
}

// Update document list to reflect the change
// Only reload full document list if we need to (for the command palette)
if (isCommandPaletteOpen) {
const allDocs = await DocumentManager.getAllDocuments();
setDocuments(allDocs);
}
} else {
// Fallback: full refresh only if we don't have specific IDs
const allDocs = await DocumentManager.getAllDocuments();
setDocuments(allDocs);

// Check if the current document was updated
if (currentDocument) {
const updatedCurrentDoc = allDocs.find(doc => doc.id === currentDocument.id || doc._id === currentDocument.id);
if (updatedCurrentDoc && updatedCurrentDoc.updatedAt !== currentDocument.updatedAt) {
console.log('Current document was updated remotely, refreshing editor');
setCurrentDocument(updatedCurrentDoc);
setContent(updatedCurrentDoc.content);
setIsSaved(true); // Document updated from remote is considered saved

// Update the editor view with the new content
if (editorView) {
editorView.dispatch({
changes: {
from: 0,
to: editorView.state.doc.length,
insert: updatedCurrentDoc.content
}
});
}
}
}
}
};

window.addEventListener('documentsUpdated', handleDocumentsUpdate);

return () => {
window.removeEventListener('documentsUpdated', handleDocumentsUpdate);
};
}, [currentDocument, editorView, isCommandPaletteOpen]);

// Refresh documents list when command palette opens (lazy loading)
useEffect(() => {
if (isCommandPaletteOpen) {
async function refreshDocumentsList() {
const allDocs = await DocumentManager.getAllDocuments();
setDocuments(allDocs);
}
refreshDocumentsList();
}
}, [isCommandPaletteOpen]);

// Debounced save - wait for user to stop typing before saving
useEffect(() => {
if (currentDocument && content !== currentDocument.content) {
// Mark as unsaved when content changes
setIsSaved(false);

// Clear any existing timeout
const timeoutId = setTimeout(async () => {
const updatedDoc = {
...currentDocument,
content,
updatedAt: new Date().toISOString()
};

try {
console.log('Auto-saving document after typing pause...');
const savedDoc = await DocumentManager.saveDocument(updatedDoc);
setCurrentDocument(savedDoc);
setIsSaved(true); // Mark as saved after successful save

// Update document list to reflect the change
const allDocs = await DocumentManager.getAllDocuments();
setDocuments(allDocs);
} catch (error) {
console.error('Error auto-saving document:', error);
// Keep isSaved as false if save failed
}
}, 2000); // Wait 2 seconds after user stops typing

saveDocument();
// Cleanup function to clear timeout if component unmounts or content changes again
return () => clearTimeout(timeoutId);
}
}, [content, currentDocument]);

Expand All @@ -77,6 +177,7 @@ const CodeEditor = () => {
// Switch to the selected document
setCurrentDocument(document);
setContent(document.content);
setIsSaved(true); // Reset save status for new document

// Update editor content
if (editorView) {
Expand Down Expand Up @@ -104,6 +205,7 @@ const CodeEditor = () => {
// Switch to the new document
setCurrentDocument(newDoc);
setContent(newDoc.content);
setIsSaved(true); // New document is considered saved

// Update editor content
if (editorView) {
Expand Down Expand Up @@ -147,10 +249,13 @@ const CodeEditor = () => {
// Use async/await in an IIFE
(async () => {
try {
console.log('Manual save triggered (Ctrl+S)');
const savedDoc = await DocumentManager.saveDocument(updatedDoc);
setCurrentDocument(savedDoc);
setIsSaved(true); // Mark as saved after successful save
const allDocs = await DocumentManager.getAllDocuments();
setDocuments(allDocs);
console.log('Document saved successfully');
} catch (error) {
console.error('Error saving document with keyboard shortcut:', error);
}
Expand Down Expand Up @@ -218,10 +323,13 @@ const CodeEditor = () => {
// Use an IIFE to handle async calls
(async () => {
try {
console.log('Manual save triggered (Ctrl+S in editor)');
const savedDoc = await DocumentManager.saveDocument(updatedDoc);
setCurrentDocument(savedDoc);
setIsSaved(true); // Mark as saved after successful save
const allDocs = await DocumentManager.getAllDocuments();
setDocuments(allDocs);
console.log('Document saved successfully');
} catch (error) {
console.error('Error saving document in CodeMirror keybinding:', error);
}
Expand Down Expand Up @@ -257,7 +365,13 @@ const CodeEditor = () => {
title="Click to open document list"
>
<h1 className="document-title">
{currentDocument.title} <span className="title-click-indicator">⌘</span>
{currentDocument.title}
{!isSaved && (
<span className="save-status unsaved">
</span>
)}
<span className="title-click-indicator">⌘</span>
</h1>
<div className="document-meta">
Last updated: {new Date(currentDocument.updatedAt).toLocaleString()}
Expand Down
65 changes: 65 additions & 0 deletions src/components/ConflictResolver.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
.conflict-resolver-simple {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
padding: 12px 16px;
margin: 12px 0;
border-left: 4px solid #f39c12;
}

.conflict-notification {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
}

.conflict-icon {
font-size: 1.2em;
}

.conflict-message {
flex: 1;
color: #856404;
font-weight: 500;
}

.auto-resolve-button {
background: #f39c12;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 0.9em;
cursor: pointer;
transition: background-color 0.2s ease;
}

.auto-resolve-button:hover:not(:disabled) {
background: #e67e22;
}

.auto-resolve-button:disabled {
background: #bdc3c7;
cursor: not-allowed;
}

.conflict-help {
color: #6c757d;
font-size: 0.85em;
line-height: 1.4;
}

/* Mobile responsive */
@media (max-width: 768px) {
.conflict-notification {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}

.auto-resolve-button {
align-self: stretch;
text-align: center;
}
}
53 changes: 53 additions & 0 deletions src/components/ConflictResolver.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, { useState } from 'react';
import { DatabaseService } from '../services/DatabaseService';
import './ConflictResolver.css';

const ConflictResolver = ({ conflictCount, onRefresh }) => {
const [isResolving, setIsResolving] = useState(false);

const handleAutoResolve = async () => {
try {
setIsResolving(true);
const resolved = await DatabaseService.autoResolveConflicts();

if (resolved > 0) {
console.log(`Auto-resolved ${resolved} conflict(s)`);
// Refresh the conflicts list and document list
if (onRefresh) {
onRefresh();
}
}
} catch (error) {
console.error('Error auto-resolving conflicts:', error);
} finally {
setIsResolving(false);
}
};

if (conflictCount === 0) {
return null;
}

return (
<div className="conflict-resolver-simple">
<div className="conflict-notification">
<span className="conflict-icon">⚠️</span>
<span className="conflict-message">
{conflictCount} document conflict{conflictCount > 1 ? 's' : ''} detected
</span>
<button
onClick={handleAutoResolve}
disabled={isResolving}
className="auto-resolve-button"
>
{isResolving ? 'Resolving...' : 'Auto-Resolve'}
</button>
</div>
<div className="conflict-help">
Conflicts are usually resolved automatically. Click "Auto-Resolve" to merge changes or create conflict markers.
</div>
</div>
);
};

export default ConflictResolver;
Loading
Loading