diff --git a/CLAUDE.md b/CLAUDE.md
index 17222db..d114daf 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -67,3 +67,4 @@ When implementing changes:
- Pay attention to the existing code base style and approaches and try to adhere to the existing style instead of bringing your own vision.
- When updating documentation, do not record intermediate changes - write them always as a matter-of-fact information.
- When working on the code, if you notice any opportunities to better bring the project to the state above - bring this to user's attention and ask for approval to implement the suggested changes.
+- Testing: The user always tests changes manually. Do not start local servers or attempt to run the application.
diff --git a/index.html b/index.html
index 8526b1e..c3e1b3c 100644
--- a/index.html
+++ b/index.html
@@ -200,6 +200,7 @@
+
diff --git a/src/js/app.js b/src/js/app.js
index f2cb0a9..8a72f0e 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -216,6 +216,7 @@ document.addEventListener('DOMContentLoaded', function () {
const datasetsLink = document.getElementById('datasets-link');
const toggleDatasetsBtn = document.getElementById('toggle-datasets');
const newDatasetBtn = document.getElementById('new-dataset-btn');
+ const editDatasetBtn = document.getElementById('edit-dataset-btn');
const cancelDatasetBtn = document.getElementById('cancel-dataset-btn');
const saveDatasetBtn = document.getElementById('save-dataset-btn');
const deleteDatasetBtn = document.getElementById('delete-dataset-btn');
@@ -234,6 +235,15 @@ document.addEventListener('DOMContentLoaded', function () {
newDatasetBtn.addEventListener('click', showNewDatasetForm);
}
+ // Edit dataset button
+ if (editDatasetBtn) {
+ editDatasetBtn.addEventListener('click', async function() {
+ if (window.currentDatasetId) {
+ await showEditDatasetForm(window.currentDatasetId);
+ }
+ });
+ }
+
// Import dataset button and file input
const importDatasetBtn = document.getElementById('import-dataset-btn');
const importDatasetFile = document.getElementById('import-dataset-file');
@@ -375,10 +385,18 @@ function handleURLStateChange() {
if (state.datasetId === 'new') {
// Show new dataset form
showNewDatasetForm(false);
+ } else if (state.datasetId && state.datasetId.startsWith('edit-')) {
+ // Show edit dataset form - extract numeric ID from "edit-123456"
+ const numericId = parseFloat(state.datasetId.replace('edit-', ''));
+ if (!isNaN(numericId)) {
+ showEditDatasetForm(numericId, false);
+ }
} else if (state.datasetId) {
// Extract numeric ID from "dataset-123456"
const numericId = parseFloat(state.datasetId.replace('dataset-', ''));
- selectDataset(numericId, false);
+ if (!isNaN(numericId)) {
+ selectDataset(numericId, false);
+ }
}
} else if (state.snippetId) {
// Close dataset modal if open
diff --git a/src/js/dataset-manager.js b/src/js/dataset-manager.js
index 66cf953..852ae42 100644
--- a/src/js/dataset-manager.js
+++ b/src/js/dataset-manager.js
@@ -997,10 +997,31 @@ function showDetectionConfirmation(detection, originalInput) {
detectedConfidenceEl.className = `detected-confidence ${confidenceClass}`;
detectedConfidenceEl.textContent = `${detection.confidence} confidence`;
- // Show preview
+ // Calculate metadata for the detected data
+ let metadata = null;
+ if (detection.source === 'url' && detection.content) {
+ metadata = calculateDatasetStats(
+ detection.parsed || detection.content,
+ detection.format,
+ 'inline'
+ );
+ } else if (detection.source === 'inline') {
+ metadata = calculateDatasetStats(
+ detection.parsed || originalInput,
+ detection.format,
+ 'inline'
+ );
+ }
+
+ // Show preview with metadata
let previewText = '';
+
if (detection.source === 'url') {
previewText = `URL: ${originalInput}\n\n`;
+ if (metadata && metadata.columns && metadata.columns.length > 0) {
+ previewText += `Columns (${metadata.columnCount}): ${metadata.columns.join(', ')}\n`;
+ previewText += `Rows: ${metadata.rowCount}\n\n`;
+ }
if (detection.content) {
const lines = detection.content.split('\n');
previewText += `Preview (first 10 lines):\n${lines.slice(0, 10).join('\n')}`;
@@ -1009,8 +1030,12 @@ function showDetectionConfirmation(detection, originalInput) {
}
}
} else {
+ if (metadata && metadata.columns && metadata.columns.length > 0) {
+ previewText = `Columns (${metadata.columnCount}): ${metadata.columns.join(', ')}\n`;
+ previewText += `Rows: ${metadata.rowCount}\n\n`;
+ }
const lines = originalInput.split('\n');
- previewText = lines.slice(0, 15).join('\n');
+ previewText += lines.slice(0, 15).join('\n');
if (lines.length > 15) {
previewText += `\n... (${lines.length - 15} more lines)`;
}
@@ -1020,7 +1045,8 @@ function showDetectionConfirmation(detection, originalInput) {
// Store detection data for later use
window.currentDetection = {
...detection,
- originalInput
+ originalInput,
+ metadata
};
}
@@ -1031,8 +1057,80 @@ function hideDetectionConfirmation() {
window.currentDetection = null;
}
+// Setup input handler for dataset form (handles both create and edit)
+function setupDatasetInputHandler() {
+ const inputEl = document.getElementById('dataset-form-input');
+
+ // Remove existing listener if any
+ if (inputEl._datasetInputHandler) {
+ inputEl.removeEventListener('input', inputEl._datasetInputHandler);
+ }
+
+ // Create new handler
+ const handler = async function () {
+ const text = this.value.trim();
+ if (!text) {
+ hideDetectionConfirmation();
+ hideSchemaWarning();
+ return;
+ }
+
+ const errorEl = document.getElementById('dataset-form-error');
+ errorEl.textContent = '';
+
+ // Check if it's a URL
+ if (isURL(text)) {
+ errorEl.textContent = 'Fetching and analyzing URL...';
+
+ try {
+ const detection = await fetchAndDetectURL(text);
+ errorEl.textContent = '';
+
+ if (detection.format) {
+ showDetectionConfirmation(detection, text);
+ checkSchemaChanges();
+ } else {
+ errorEl.textContent = 'Could not detect data format from URL. Please check the URL or try pasting the data directly.';
+ hideDetectionConfirmation();
+ hideSchemaWarning();
+ }
+ } catch (error) {
+ errorEl.textContent = error.message;
+ hideDetectionConfirmation();
+ hideSchemaWarning();
+ }
+ } else {
+ // Inline data - detect format
+ const detection = detectDataFormat(text);
+
+ if (detection.format) {
+ showDetectionConfirmation({
+ ...detection,
+ source: 'inline'
+ }, text);
+ checkSchemaChanges();
+ } else {
+ errorEl.textContent = 'Could not detect data format. Please ensure your data is valid JSON, CSV, or TSV.';
+ hideDetectionConfirmation();
+ hideSchemaWarning();
+ }
+ }
+ };
+
+ // Store reference to handler for cleanup
+ inputEl._datasetInputHandler = handler;
+
+ // Attach listener
+ inputEl.addEventListener('input', handler);
+}
+
// Show new dataset form
function showNewDatasetForm(updateURL = true) {
+ // Set mode to create
+ window.datasetFormMode = 'create';
+ window.editingDatasetId = null;
+ window.originalSchema = null;
+
document.getElementById('dataset-list-view').style.display = 'none';
document.getElementById('dataset-form-view').style.display = 'block';
document.getElementById('dataset-form-name').value = '';
@@ -1040,6 +1138,12 @@ function showNewDatasetForm(updateURL = true) {
document.getElementById('dataset-form-comment').value = '';
document.getElementById('dataset-form-error').textContent = '';
+ // Update form header
+ document.querySelector('.dataset-form-header').textContent = 'Create New Dataset';
+
+ // Hide schema warning
+ hideSchemaWarning();
+
// Hide detection confirmation
hideDetectionConfirmation();
@@ -1048,56 +1152,156 @@ function showNewDatasetForm(updateURL = true) {
URLState.update({ view: 'datasets', snippetId: null, datasetId: 'new' });
}
- // Add paste handler if not already added
- if (!window.datasetListenersAdded) {
- const inputEl = document.getElementById('dataset-form-input');
+ // Setup input handler for detection
+ setupDatasetInputHandler();
+}
- // Handle paste/input with auto-detection
- inputEl.addEventListener('input', async function () {
- const text = this.value.trim();
- if (!text) {
- hideDetectionConfirmation();
- return;
- }
+// Show edit dataset form
+async function showEditDatasetForm(datasetId, updateURL = true) {
+ const dataset = await DatasetStorage.getDataset(datasetId);
+ if (!dataset) return;
- const errorEl = document.getElementById('dataset-form-error');
- errorEl.textContent = '';
+ // Set mode to edit
+ window.datasetFormMode = 'edit';
+ window.editingDatasetId = datasetId;
- // Check if it's a URL
- if (isURL(text)) {
- errorEl.textContent = 'Fetching and analyzing URL...';
+ // Store original schema for comparison
+ window.originalSchema = dataset.columns ? [...dataset.columns] : [];
- try {
- const detection = await fetchAndDetectURL(text);
- errorEl.textContent = '';
+ document.getElementById('dataset-list-view').style.display = 'none';
+ document.getElementById('dataset-form-view').style.display = 'block';
- if (detection.format) {
- showDetectionConfirmation(detection, text);
- } else {
- errorEl.textContent = 'Could not detect data format from URL. Please check the URL or try pasting the data directly.';
- hideDetectionConfirmation();
- }
- } catch (error) {
- errorEl.textContent = error.message;
- hideDetectionConfirmation();
- }
- } else {
- // Inline data - detect format
- const detection = detectDataFormat(text);
+ // Populate form with current values
+ document.getElementById('dataset-form-name').value = dataset.name;
+ document.getElementById('dataset-form-comment').value = dataset.comment || '';
- if (detection.format) {
- showDetectionConfirmation({
- ...detection,
- source: 'inline'
- }, text);
- } else {
- errorEl.textContent = 'Could not detect data format. Please ensure your data is valid JSON, CSV, or TSV.';
- hideDetectionConfirmation();
- }
- }
- });
+ // Populate data input based on source
+ let inputValue;
+ if (dataset.source === 'url') {
+ inputValue = dataset.data; // The URL string
+ } else if (dataset.format === 'json' || dataset.format === 'topojson') {
+ inputValue = JSON.stringify(dataset.data, null, 2);
+ } else if (dataset.format === 'csv' || dataset.format === 'tsv') {
+ inputValue = dataset.data;
+ }
+ document.getElementById('dataset-form-input').value = inputValue;
- window.datasetListenersAdded = true;
+ // Trigger detection to show current state
+ const inputEl = document.getElementById('dataset-form-input');
+ // Use setTimeout to ensure the value is set before triggering
+ setTimeout(() => {
+ inputEl.dispatchEvent(new Event('input', { bubbles: true }));
+ }, 0);
+
+ document.getElementById('dataset-form-error').textContent = '';
+
+ // Update form header
+ document.querySelector('.dataset-form-header').textContent = 'Edit Dataset';
+
+ // Hide schema warning initially
+ hideSchemaWarning();
+
+ // Update URL state
+ if (updateURL) {
+ URLState.update({ view: 'datasets', snippetId: null, datasetId: `edit-${datasetId}` });
+ }
+
+ // Setup input handler for detection
+ setupDatasetInputHandler();
+}
+
+// Hide schema warning
+function hideSchemaWarning() {
+ const warningEl = document.getElementById('dataset-schema-warning');
+ if (warningEl) {
+ warningEl.style.display = 'none';
+ }
+}
+
+// Show schema warning with details
+function showSchemaWarning(oldSchema, newSchema) {
+ let warningEl = document.getElementById('dataset-schema-warning');
+
+ // Create warning element if it doesn't exist
+ if (!warningEl) {
+ const confirmEl = document.getElementById('dataset-detection-confirm');
+ warningEl = document.createElement('div');
+ warningEl.id = 'dataset-schema-warning';
+ warningEl.className = 'dataset-schema-warning';
+ confirmEl.parentNode.insertBefore(warningEl, confirmEl.nextSibling);
+ }
+
+ // Determine what changed
+ const added = newSchema.filter(col => !oldSchema.includes(col));
+ const removed = oldSchema.filter(col => !newSchema.includes(col));
+
+ let message = '⚠️ Schema Change Detected ';
+
+ if (removed.length > 0) {
+ message += `Removed columns: ${removed.join(', ')} `;
+ }
+ if (added.length > 0) {
+ message += `Added columns: ${added.join(', ')} `;
+ }
+
+ message += 'Saving will update the dataset schema. Linked snippets may be affected.';
+
+ warningEl.innerHTML = message;
+ warningEl.style.display = 'block';
+}
+
+// Check for schema changes
+function checkSchemaChanges() {
+ // Only check in edit mode
+ if (window.datasetFormMode !== 'edit' || !window.originalSchema) {
+ return;
+ }
+
+ // Get current detection
+ const detection = window.currentDetection;
+ if (!detection || !detection.format) {
+ hideSchemaWarning();
+ return;
+ }
+
+ // Extract new schema from detected data
+ let newSchema = [];
+
+ if (detection.source === 'url' && detection.content) {
+ // Parse content to get schema
+ const tempStats = calculateDatasetStats(
+ detection.parsed || detection.content,
+ detection.format,
+ 'inline'
+ );
+ newSchema = tempStats.columns || [];
+ } else if (detection.source === 'inline') {
+ const tempStats = calculateDatasetStats(
+ detection.parsed || detection.originalInput,
+ detection.format,
+ 'inline'
+ );
+ newSchema = tempStats.columns || [];
+ }
+
+ // Compare schemas
+ const originalSchema = window.originalSchema || [];
+
+ if (newSchema.length === 0 && originalSchema.length === 0) {
+ hideSchemaWarning();
+ return;
+ }
+
+ // Check if schemas are different
+ const schemaChanged =
+ originalSchema.length !== newSchema.length ||
+ !originalSchema.every(col => newSchema.includes(col)) ||
+ !newSchema.every(col => originalSchema.includes(col));
+
+ if (schemaChanged) {
+ showSchemaWarning(originalSchema, newSchema);
+ } else {
+ hideSchemaWarning();
}
}
@@ -1105,9 +1309,13 @@ function showNewDatasetForm(updateURL = true) {
function hideNewDatasetForm() {
document.getElementById('dataset-list-view').style.display = 'block';
document.getElementById('dataset-form-view').style.display = 'none';
+ window.datasetFormMode = null;
+ window.editingDatasetId = null;
+ window.originalSchema = null;
+ hideSchemaWarning();
}
-// Save new dataset
+// Save new dataset (handles both create and edit modes)
async function saveNewDataset() {
const name = document.getElementById('dataset-form-name').value.trim();
const comment = document.getElementById('dataset-form-comment').value.trim();
@@ -1173,29 +1381,65 @@ async function saveNewDataset() {
}
}
- // Check if name already exists
- if (await DatasetStorage.nameExists(name)) {
- errorEl.textContent = 'A dataset with this name already exists';
- return;
- }
-
- // Create dataset
try {
- const dataset = await DatasetStorage.createDataset(name, data, format, source, comment);
+ if (window.datasetFormMode === 'edit') {
+ // Edit mode - update existing dataset
+ const datasetId = window.editingDatasetId;
- // If we have metadata from URL fetch, update the dataset
- if (metadata) {
- await DatasetStorage.updateDataset(dataset.id, {
- data: data,
- ...metadata
- });
+ // Check if name already exists (excluding current dataset)
+ if (await DatasetStorage.nameExists(name, datasetId)) {
+ errorEl.textContent = 'A dataset with this name already exists';
+ return;
+ }
+
+ // Update dataset
+ const updates = {
+ name,
+ data,
+ format,
+ source,
+ comment
+ };
+
+ // Add metadata if available (for URL sources)
+ if (metadata) {
+ Object.assign(updates, metadata);
+ }
+
+ await DatasetStorage.updateDataset(datasetId, updates);
+
+ hideNewDatasetForm();
+ await renderDatasetList();
+ await selectDataset(datasetId);
+
+ Toast.success('Dataset updated successfully');
+
+ // Track event
+ Analytics.track('dataset-update', `Update dataset (${source})`);
+ } else {
+ // Create mode - create new dataset
+ // Check if name already exists
+ if (await DatasetStorage.nameExists(name)) {
+ errorEl.textContent = 'A dataset with this name already exists';
+ return;
+ }
+
+ const dataset = await DatasetStorage.createDataset(name, data, format, source, comment);
+
+ // If we have metadata from URL fetch, update the dataset
+ if (metadata) {
+ await DatasetStorage.updateDataset(dataset.id, {
+ data: data,
+ ...metadata
+ });
+ }
+
+ hideNewDatasetForm();
+ await renderDatasetList();
+
+ // Track event
+ Analytics.track('dataset-create', `Create dataset (${source})`);
}
-
- hideNewDatasetForm();
- await renderDatasetList();
-
- // Track event
- Analytics.track('dataset-create', `Create dataset (${source})`);
} catch (error) {
errorEl.textContent = `Failed to save dataset: ${error.message}`;
}
diff --git a/src/styles.css b/src/styles.css
index 4bc8d01..fc5f3d7 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -374,6 +374,12 @@ body { font-family: var(--font-main); height: 100vh; overflow: hidden; backgroun
.detection-badges { display: flex; gap: 6px; align-items: center; }
.detection-badge { background: var(--win-blue); color: var(--bg-white); padding: 2px 8px; font-size: 10px; font-weight: bold; border: 1px solid var(--win-blue-dark); border-radius: 2px; }
.detected-confidence { font-size: 9px; padding: 2px 6px; border-radius: 2px; }
+
+/* Schema Warning */
+.dataset-schema-warning { margin: 12px 0; padding: 12px; background: #fff8dc; border: 2px solid #ffa500; border-radius: 4px; font-size: 11px; line-height: 1.6; color: #000; }
+:root[data-theme="experimental"] .dataset-schema-warning { background: #3a3020; border-color: #cc8800; color: #ffd966; }
+.dataset-schema-warning strong { font-weight: 600; }
+.dataset-schema-warning em { font-style: italic; opacity: 0.9; }
.detected-confidence.high { background: #90ee90; border: 1px solid #60c060; }
:root[data-theme="experimental"] .detected-confidence.high { background: #3a6a3a; color: #88ff88; border-color: #66dd66; }
.detected-confidence.medium { background: #ffff90; border: 1px solid #d0d060; }