/** * GoingsOn - Import Module * * Imports tasks, projects, or events from a CSV/TSV file. The entity type is * auto-detected from the header columns by the native importer (Rust). CSV is * GoingsOn's import interchange format; convert other tools' exports to CSV * with a separate converter, then bring them in here. */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttrVal = GoingsOn.utils.escapeAttrValue; // ============ State ============ let selectedFilePath = null; let previewData = null; // ============ Import Wizard ============ /** * Opens the import modal (choose file, then preview and confirm). */ function openImportModal() { selectedFilePath = null; previewData = null; const content = `

1. Choose a CSV or TSV file

Columns are matched by name (description, due, priority, project, tags for tasks; start/end for events; name/type for projects).

`; GoingsOn.ui.openModal('Import from CSV', content); } /** * Opens the file dialog, then loads a preview. */ async function selectFile() { try { const { open } = window.__TAURI__.dialog; const filePath = await open({ multiple: false, filters: [{ name: 'CSV / TSV', extensions: ['csv', 'tsv'] }], }); if (!filePath) return; // User cancelled selectedFilePath = filePath; document.getElementById('selected-file-name').textContent = getFileName(filePath); document.getElementById('import-step-preview').classList.remove('hidden'); await loadPreview(); } catch (err) { GoingsOn.ui.showToast('Failed to select file: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } /** * Gets the filename from a full path. */ function getFileName(path) { return path.split(/[/\\]/).pop() || path; } /** * Loads and displays the preview of the import data. */ async function loadPreview() { const container = document.getElementById('import-preview-container'); container.innerHTML = '
Parsing file...
'; try { previewData = await GoingsOn.api.import.preview(selectedFilePath); if (!previewData.items || previewData.items.length === 0) { const warnings = previewData.warnings || []; container.innerHTML = `

No items found in file.

`; if (warnings.length > 0) { container.insertAdjacentHTML('beforeend', renderWarnings(warnings)); } document.getElementById('import-confirm-btn').disabled = true; return; } container.innerHTML = renderPreviewTable(previewData); document.getElementById('import-confirm-btn').disabled = false; if (previewData.warnings && previewData.warnings.length > 0) { container.insertAdjacentHTML('beforeend', renderWarnings(previewData.warnings)); } } catch (err) { container.innerHTML = `

Failed to parse file: ${esc(GoingsOn.utils.getErrorMessage(err))}

`; document.getElementById('import-confirm-btn').disabled = true; } } function renderWarnings(warnings) { return `
Warnings:
`; } /** * Renders the preview table. */ function renderPreviewTable(data) { const entityType = data.entityType || 'item'; const items = data.items || []; const maxPreview = 25; const displayItems = items.slice(0, maxPreview); const columns = getColumnsForEntityType(entityType); return `

${items.length} ${esc(entityType)}${items.length !== 1 ? 's' : ''} found

${columns.map(c => ``).join('')} ${displayItems.map(item => renderPreviewRow(item, columns)).join('')}
${esc(c.label)}
${items.length > maxPreview ? `

...and ${items.length - maxPreview} more

` : ''} `; } /** * Columns to display per entity type. Keys are the camelCase fields the * native importer emits on each item's `data` object. */ function getColumnsForEntityType(entityType) { switch (entityType) { case 'task': return [ { key: 'description', label: 'Description' }, { key: 'projectName', label: 'Project' }, { key: 'priority', label: 'Priority' }, { key: 'due', label: 'Due' }, ]; case 'project': return [ { key: 'name', label: 'Name' }, { key: 'description', label: 'Description' }, { key: 'projectType', label: 'Type' }, { key: 'status', label: 'Status' }, ]; case 'event': return [ { key: 'title', label: 'Title' }, { key: 'start', label: 'Start' }, { key: 'end', label: 'End' }, { key: 'location', label: 'Location' }, ]; default: return [{ key: 'description', label: 'Description' }]; } } /** * Renders a preview table row. */ function renderPreviewRow(item, columns) { const data = item.data || {}; return ` ${columns.map(c => { const value = data[c.key] || ''; return `${esc(truncate(value, 50))}`; }).join('')} `; } function truncate(str, maxLen) { if (!str) return ''; str = String(str); return str.length > maxLen ? str.substring(0, maxLen) + '...' : str; } /** * Executes the import. */ async function executeImport() { if (!selectedFilePath || !previewData) return; const btn = document.getElementById('import-confirm-btn'); btn.disabled = true; btn.textContent = 'Importing...'; try { const result = await GoingsOn.api.import.execute(selectedFilePath); GoingsOn.ui.closeModal(); const entityType = previewData.entityType || 'item'; if (result.failedCount > 0) { GoingsOn.ui.showToast(`Imported ${result.importedCount} ${entityType}(s), ${result.failedCount} failed`, 'warning'); } else { GoingsOn.ui.showToast(`Successfully imported ${result.importedCount} ${entityType}(s)!`, 'success'); } if (typeof GoingsOn.navigation !== 'undefined' && GoingsOn.navigation.reloadCurrentView) { GoingsOn.navigation.reloadCurrentView(); } } catch (err) { GoingsOn.ui.showToast('Import failed: ' + GoingsOn.utils.getErrorMessage(err), 'error'); btn.disabled = false; btn.textContent = 'Import'; } } // ============ Populate Namespace ============ GoingsOn.import = { openModal: openImportModal, selectFile, executeImport, }; })();