mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
refactor: update import/export functions to handle datasets alongside snippets
This commit is contained in:
@@ -26,8 +26,8 @@
|
|||||||
<span class="header-title">Astrolabe</span>
|
<span class="header-title">Astrolabe</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-links">
|
<div class="header-links">
|
||||||
<span class="header-link" id="import-link" title="Import snippets from JSON file">Import</span>
|
<span class="header-link" id="import-link" title="Import snippets and datasets">Import</span>
|
||||||
<span class="header-link" id="export-link" title="Export all snippets to JSON file">Export</span>
|
<span class="header-link" id="export-link" title="Export all snippets and datasets">Export</span>
|
||||||
<span class="header-link" id="datasets-link" title="Open dataset manager (Cmd/Ctrl+K)">Datasets</span>
|
<span class="header-link" id="datasets-link" title="Open dataset manager (Cmd/Ctrl+K)">Datasets</span>
|
||||||
<span class="header-link" id="settings-link" title="Open settings (Cmd/Ctrl+,)">Settings 🛠️</span>
|
<span class="header-link" id="settings-link" title="Open settings (Cmd/Ctrl+,)">Settings 🛠️</span>
|
||||||
<span class="header-link" id="help-link" title="View keyboard shortcuts and help">About & Privacy</span>
|
<span class="header-link" id="help-link" title="View keyboard shortcuts and help">About & Privacy</span>
|
||||||
|
|||||||
@@ -1184,8 +1184,8 @@ function updateStorageMonitor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export all snippets to JSON file
|
// Export all snippets and datasets to JSON file
|
||||||
function exportSnippets() {
|
async function exportSnippets() {
|
||||||
const snippets = SnippetStorage.loadSnippets();
|
const snippets = SnippetStorage.loadSnippets();
|
||||||
|
|
||||||
if (snippets.length === 0) {
|
if (snippets.length === 0) {
|
||||||
@@ -1193,15 +1193,27 @@ function exportSnippets() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get ALL datasets for complete backup
|
||||||
|
const datasets = await DatasetStorage.listDatasets();
|
||||||
|
|
||||||
|
// Create unified export format
|
||||||
|
const exportData = {
|
||||||
|
version: "1.0",
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
exportedBy: "Astrolabe",
|
||||||
|
snippets: snippets,
|
||||||
|
datasets: datasets
|
||||||
|
};
|
||||||
|
|
||||||
// Create JSON blob
|
// Create JSON blob
|
||||||
const jsonString = JSON.stringify(snippets, null, 2);
|
const jsonString = JSON.stringify(exportData, null, 2);
|
||||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||||
|
|
||||||
// Create download link
|
// Create download link
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `astrolabe-snippets-${new Date().toISOString().slice(0, 10)}.json`;
|
link.download = `astrolabe-project-${new Date().toISOString().slice(0, 10)}.json`;
|
||||||
|
|
||||||
// Trigger download
|
// Trigger download
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
@@ -1210,10 +1222,13 @@ function exportSnippets() {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
Toast.success(`Exported ${snippets.length} snippet${snippets.length !== 1 ? 's' : ''}`);
|
const count = datasets.length > 0
|
||||||
|
? `${snippets.length} snippet${snippets.length !== 1 ? 's' : ''} and ${datasets.length} dataset${datasets.length !== 1 ? 's' : ''}`
|
||||||
|
: `${snippets.length} snippet${snippets.length !== 1 ? 's' : ''}`;
|
||||||
|
Toast.success(`Exported ${count}`);
|
||||||
|
|
||||||
// Track event
|
// Track event
|
||||||
Analytics.track('snippets-export', `Export ${snippets.length} snippets`);
|
Analytics.track('project-export', `Export ${snippets.length} snippets, ${datasets.length} datasets`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize external snippet format to Astrolabe format
|
// Normalize external snippet format to Astrolabe format
|
||||||
@@ -1280,29 +1295,79 @@ function estimateImportFit(existingSnippets, newSnippets) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import snippets from JSON file
|
// Import snippets and datasets from JSON file
|
||||||
function importSnippets(fileInput) {
|
function importSnippets(fileInput) {
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = async function(e) {
|
||||||
try {
|
try {
|
||||||
const importedData = JSON.parse(e.target.result);
|
const importedData = JSON.parse(e.target.result);
|
||||||
|
|
||||||
// Handle both single snippet and array of snippets
|
// Detect format: legacy (array) or unified (object with version)
|
||||||
const snippetsToImport = Array.isArray(importedData) ? importedData : [importedData];
|
let snippetsToImport = [];
|
||||||
|
let datasetsToImport = [];
|
||||||
|
|
||||||
|
if (Array.isArray(importedData)) {
|
||||||
|
// Legacy format: array of snippets only
|
||||||
|
snippetsToImport = importedData;
|
||||||
|
} else if (importedData.version && importedData.snippets) {
|
||||||
|
// New unified format
|
||||||
|
snippetsToImport = importedData.snippets || [];
|
||||||
|
datasetsToImport = importedData.datasets || [];
|
||||||
|
} else {
|
||||||
|
// Single snippet object
|
||||||
|
snippetsToImport = [importedData];
|
||||||
|
}
|
||||||
|
|
||||||
if (snippetsToImport.length === 0) {
|
if (snippetsToImport.length === 0) {
|
||||||
Toast.info('No snippets found in file');
|
Toast.info('No snippets found in file');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize and merge with existing snippets
|
// Import datasets first (if any)
|
||||||
|
let datasetsImported = 0;
|
||||||
|
const renamedDatasets = []; // Track renamed datasets for warning
|
||||||
|
|
||||||
|
for (const datasetData of datasetsToImport) {
|
||||||
|
try {
|
||||||
|
let datasetName = datasetData.name;
|
||||||
|
const originalName = datasetName;
|
||||||
|
|
||||||
|
// Handle name conflicts by renaming
|
||||||
|
if (await DatasetStorage.nameExists(datasetName)) {
|
||||||
|
const timestamp = Date.now().toString().slice(-6);
|
||||||
|
datasetName = `${originalName}_${timestamp}`;
|
||||||
|
|
||||||
|
// Unlikely, but ensure uniqueness
|
||||||
|
let counter = 1;
|
||||||
|
while (await DatasetStorage.nameExists(datasetName)) {
|
||||||
|
datasetName = `${originalName}_${timestamp}_${counter}`;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
renamedDatasets.push({ from: originalName, to: datasetName });
|
||||||
|
}
|
||||||
|
|
||||||
|
await DatasetStorage.createDataset(
|
||||||
|
datasetName,
|
||||||
|
datasetData.data,
|
||||||
|
datasetData.format,
|
||||||
|
datasetData.source,
|
||||||
|
datasetData.comment || ''
|
||||||
|
);
|
||||||
|
datasetsImported++;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to import dataset ${datasetData.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import snippets (existing normalization logic)
|
||||||
const existingSnippets = SnippetStorage.loadSnippets();
|
const existingSnippets = SnippetStorage.loadSnippets();
|
||||||
const existingIds = new Set(existingSnippets.map(s => s.id));
|
const existingIds = new Set(existingSnippets.map(s => s.id));
|
||||||
|
|
||||||
let importedCount = 0;
|
let snippetsImported = 0;
|
||||||
const normalizedSnippets = [];
|
const normalizedSnippets = [];
|
||||||
|
|
||||||
snippetsToImport.forEach(snippet => {
|
snippetsToImport.forEach(snippet => {
|
||||||
@@ -1315,48 +1380,55 @@ function importSnippets(fileInput) {
|
|||||||
|
|
||||||
normalizedSnippets.push(normalized);
|
normalizedSnippets.push(normalized);
|
||||||
existingIds.add(normalized.id);
|
existingIds.add(normalized.id);
|
||||||
importedCount++;
|
snippetsImported++;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Estimate fit before attempting save
|
// Estimate storage fit
|
||||||
const fit = estimateImportFit(existingSnippets, normalizedSnippets);
|
const fit = estimateImportFit(existingSnippets, normalizedSnippets);
|
||||||
|
|
||||||
if (!fit.willFit) {
|
if (!fit.willFit) {
|
||||||
// Still try to load - let user decide if they want to proceed
|
|
||||||
Toast.warning(
|
Toast.warning(
|
||||||
`⚠️ Import is ${formatBytes(fit.overageBytes)} over the 5 MB limit. Attempting to load...`,
|
`⚠️ Import is ${formatBytes(fit.overageBytes)} over the 5 MB limit. Attempting to load...`,
|
||||||
5000
|
5000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge snippets
|
// Save snippets
|
||||||
const allSnippets = existingSnippets.concat(normalizedSnippets);
|
const allSnippets = existingSnippets.concat(normalizedSnippets);
|
||||||
|
|
||||||
// Attempt to save
|
|
||||||
if (SnippetStorage.saveSnippets(allSnippets)) {
|
if (SnippetStorage.saveSnippets(allSnippets)) {
|
||||||
const message = fit.willFit
|
let message = `Imported ${snippetsImported} snippet${snippetsImported !== 1 ? 's' : ''}`;
|
||||||
? `Successfully imported ${importedCount} snippet${importedCount !== 1 ? 's' : ''}`
|
if (datasetsImported > 0) {
|
||||||
: `Imported ${importedCount} snippet${importedCount !== 1 ? 's' : ''} (Storage: ${formatBytes(fit.totalSize)} / 5 MB)`;
|
message += ` and ${datasetsImported} dataset${datasetsImported !== 1 ? 's' : ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn about renamed datasets
|
||||||
|
if (renamedDatasets.length > 0) {
|
||||||
|
const renameList = renamedDatasets.map(r => `"${r.from}" → "${r.to}"`).join(', ');
|
||||||
|
Toast.warning(
|
||||||
|
`${message}. Some datasets were renamed due to conflicts: ${renameList}. You may need to update dataset references in affected snippets.`,
|
||||||
|
8000
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Toast.success(message);
|
||||||
|
}
|
||||||
|
|
||||||
Toast.success(message);
|
|
||||||
renderSnippetList();
|
renderSnippetList();
|
||||||
updateStorageMonitor();
|
updateStorageMonitor();
|
||||||
|
|
||||||
// Track event
|
// Track event
|
||||||
Analytics.track('snippets-import', `Import ${importedCount} snippets`);
|
Analytics.track('project-import', `Import ${snippetsImported} snippets, ${datasetsImported} datasets`);
|
||||||
} else {
|
} else {
|
||||||
// Save failed - show detailed error
|
|
||||||
const overageBytes = fit.overageBytes > 0 ? fit.overageBytes : calculateDataSize(allSnippets) - STORAGE_LIMIT_BYTES;
|
const overageBytes = fit.overageBytes > 0 ? fit.overageBytes : calculateDataSize(allSnippets) - STORAGE_LIMIT_BYTES;
|
||||||
const overageSize = formatBytes(overageBytes);
|
|
||||||
Toast.error(
|
Toast.error(
|
||||||
`Storage quota exceeded by ${overageSize}. Please delete some snippets and try again.`,
|
`Storage quota exceeded by ${formatBytes(overageBytes)}. Please delete some snippets and try again.`,
|
||||||
6000
|
6000
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Import error:', error);
|
console.error('Import error:', error);
|
||||||
Toast.error('Failed to import snippets. Please check that the file is valid JSON.');
|
Toast.error('Failed to import. Please check that the file is valid JSON.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear file input
|
// Clear file input
|
||||||
|
|||||||
Reference in New Issue
Block a user