From e739bb995c49d31a78eb45453ca5bfb114eafb6d Mon Sep 17 00:00:00 2001 From: Jordan Dalby Date: Thu, 14 Nov 2024 19:08:51 +0000 Subject: [PATCH 1/3] Added import and export buttons to the settings menu --- .../src/components/settings/SettingsModal.tsx | 211 +++++++++++++++++- .../snippets/view/SnippetStorage.tsx | 20 +- client/src/hooks/useSnippets.ts | 6 +- 3 files changed, 230 insertions(+), 7 deletions(-) diff --git a/client/src/components/settings/SettingsModal.tsx b/client/src/components/settings/SettingsModal.tsx index d6d785e..744cc0e 100644 --- a/client/src/components/settings/SettingsModal.tsx +++ b/client/src/components/settings/SettingsModal.tsx @@ -1,13 +1,29 @@ -import React, { useState } from 'react'; -import { BookOpen, Clock } from 'lucide-react'; +import React, { useRef, useState } from 'react'; +import { AlertCircle, BookOpen, Clock, Download, Upload } from 'lucide-react'; import Modal from '../common/modals/Modal'; import ChangelogModal from '../common/modals/ChangelogModal'; +import { useToast } from '../../hooks/useToast'; +import { Snippet } from '../../types/snippets'; const GITHUB_URL = "https://github.com/jordan-dalby/ByteStash"; const DOCKER_URL = "https://github.com/jordan-dalby/ByteStash/pkgs/container/bytestash"; const REDDIT_URL = "https://www.reddit.com/r/selfhosted/comments/1gb1ail/selfhosted_code_snippet_manager/"; const WIKI_URL = "https://github.com/jordan-dalby/ByteStash/wiki"; +interface ImportProgress { + total: number; + current: number; + succeeded: number; + failed: number; + errors: { title: string; error: string }[]; +} + +interface ImportData { + version: string; + exported_at: string; + snippets: Omit[]; +} + export interface SettingsModalProps { isOpen: boolean; onClose: () => void; @@ -21,9 +37,20 @@ export interface SettingsModalProps { showLineNumbers: boolean; }; onSettingsChange: (newSettings: SettingsModalProps['settings']) => void; + snippets: Snippet[]; + addSnippet: (snippet: Omit, toast: boolean) => Promise; + reloadSnippets: () => void; } -const SettingsModal: React.FC = ({ isOpen, onClose, settings, onSettingsChange }) => { +const SettingsModal: React.FC = ({ + isOpen, + onClose, + settings, + onSettingsChange, + snippets, + addSnippet, + reloadSnippets +}) => { const [compactView, setCompactView] = useState(settings.compactView); const [showCodePreview, setShowCodePreview] = useState(settings.showCodePreview); const [previewLines, setPreviewLines] = useState(settings.previewLines); @@ -32,6 +59,10 @@ const SettingsModal: React.FC = ({ isOpen, onClose, settings const [expandCategories, setExpandCategories] = useState(settings.expandCategories); const [showLineNumbers, setShowLineNumbers] = useState(settings.showLineNumbers); const [showChangelog, setShowChangelog] = useState(false); + const [importing, setImporting] = useState(false); + const [importProgress, setImportProgress] = useState(null); + const fileInputRef = useRef(null); + const { addToast } = useToast(); const handleSave = () => { onSettingsChange({ @@ -46,6 +77,112 @@ const SettingsModal: React.FC = ({ isOpen, onClose, settings onClose(); }; + const resetImportState = () => { + setImporting(false); + setImportProgress(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const validateImportData = (data: any): data is ImportData => { + if (!data || typeof data !== 'object') return false; + if (typeof data.version !== 'string') return false; + if (!Array.isArray(data.snippets)) return false; + + return data.snippets.every((snippet: Snippet) => + typeof snippet === 'object' && + typeof snippet.title === 'string' && + Array.isArray(snippet.fragments) && + Array.isArray(snippet.categories) + ); + }; + + const handleImportFile = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + try { + setImporting(true); + const content = await file.text(); + const importData = JSON.parse(content); + + if (!validateImportData(importData)) { + throw new Error('Invalid import file format'); + } + + const progress: ImportProgress = { + total: importData.snippets.length, + current: 0, + succeeded: 0, + failed: 0, + errors: [] + }; + + setImportProgress(progress); + + for (const snippet of importData.snippets) { + try { + await addSnippet(snippet, false); + progress.succeeded += 1; + } catch (error) { + progress.failed += 1; + progress.errors.push({ + title: snippet.title, + error: error instanceof Error ? error.message : 'Unknown error' + }); + console.error(`Failed to import snippet "${snippet.title}":`, error); + } + + progress.current += 1; + setImportProgress({ ...progress }); + } + + if (progress.failed === 0) { + addToast(`Successfully imported ${progress.succeeded} snippets`, 'success'); + reloadSnippets(); + } else { + addToast( + `Imported ${progress.succeeded} snippets, ${progress.failed} failed. Check console for details.`, + 'warning' + ); + } + } catch (error) { + console.error('Import error:', error); + addToast( + error instanceof Error ? error.message : 'Failed to import snippets', + 'error' + ); + } finally { + resetImportState(); + } + }; + + const handleExport = () => { + try { + const exportData = { + version: '1.0', + exported_at: new Date().toISOString(), + snippets: snippets + }; + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `bytestash-export-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + addToast('Snippets exported successfully', 'success'); + } catch (error) { + console.error('Export error:', error); + addToast('Failed to export snippets', 'error'); + } + }; + return ( = ({ isOpen, onClose, settings /> + {/* Export/Import Section */} +
+
+ + +
+ + {/* Import Progress */} + {importProgress && ( +
+
+ Importing snippets... + + {importProgress.current} / {importProgress.total} + +
+ + {/* Progress Bar */} +
+
+
+ + {/* Error Summary (if any) */} + {importProgress.errors.length > 0 && ( +
+
+ + {importProgress.errors.length} errors occurred +
+
+ {importProgress.errors.map((error, index) => ( +
+ Failed to import "{error.title}": {error.error} +
+ ))} +
+
+ )} +
+ )} +
+
+ ); + return ( = ({ >
-
- - setCompactView(e.target.checked)} - className="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500" - /> -
-
- - setShowCodePreview(e.target.checked)} - className="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500" - /> -
- {showCodePreview && ( -
- - setPreviewLines(Math.max(1, Math.min(20, parseInt(e.target.value) || 1)))} - min="1" - max="20" - className="form-input w-20 rounded-md bg-gray-700 border border-gray-600 text-white p-2 text-sm" + {/* View Settings */} + + + + + +
+ + + + + {showCodePreview && ( + + setPreviewLines(Math.max(1, Math.min(20, parseInt(e.target.value) || 1)))} + min="1" + max="20" + className="form-input w-20 rounded-md bg-gray-700 border border-gray-600 text-white p-1 text-sm" + /> + + )}
- )} -
- - setIncludeCodeInSearch(e.target.checked)} - className="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500" - /> -
-
- - setShowCategories(e.target.checked)} - className="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500" - /> -
- - {showCategories && ( -
- - setExpandCategories(e.target.checked)} - className="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500" + + + -
- )} -
- - setShowLineNumbers(e.target.checked)} - className="form-checkbox h-5 w-5 text-blue-600 rounded focus:ring-blue-500" - /> -
+ +
- {/* Export/Import Section */} -
+ {/* Category Settings */} + + + + + + {showCategories && ( + + + + )} + + + {/* Search Settings */} + + + + + + + {/* Data Management */} +
- {/* Import Progress */} {importProgress && ( -
+
Importing snippets... - - {importProgress.current} / {importProgress.total} - + {importProgress.current} / {importProgress.total}
- {/* Progress Bar */}
= ({ />
- {/* Error Summary (if any) */} {importProgress.errors.length > 0 && (
@@ -335,8 +414,9 @@ const SettingsModal: React.FC = ({ )}
)} -
+ + {/* Links Section */}