mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
feat: implement meta fields with auto-save functionality using Alpine.js
This commit is contained in:
14
index.html
14
index.html
@@ -152,10 +152,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="snippet-meta" id="snippet-meta" style="display: none;">
|
<div class="snippet-meta" id="snippet-meta" style="display: none;">
|
||||||
<div class="meta-header">Name</div>
|
<div class="meta-header">Name</div>
|
||||||
<input type="text" id="snippet-name" class="input small" placeholder="Snippet name..." />
|
<input type="text"
|
||||||
|
id="snippet-name"
|
||||||
|
class="input small"
|
||||||
|
placeholder="Snippet name..."
|
||||||
|
x-model="snippetName"
|
||||||
|
@input="saveMetaDebounced()" />
|
||||||
|
|
||||||
<div class="meta-header">Comment</div>
|
<div class="meta-header">Comment</div>
|
||||||
<textarea id="snippet-comment" class="input textarea medium" placeholder="Add a comment..." rows="3"></textarea>
|
<textarea id="snippet-comment"
|
||||||
|
class="input textarea medium"
|
||||||
|
placeholder="Add a comment..."
|
||||||
|
rows="3"
|
||||||
|
x-model="snippetComment"
|
||||||
|
@input="saveMetaDebounced()"></textarea>
|
||||||
|
|
||||||
<div class="meta-info">
|
<div class="meta-info">
|
||||||
<div class="meta-info-item">
|
<div class="meta-info-item">
|
||||||
|
|||||||
@@ -211,25 +211,39 @@ Chart builder form with dataset selection, chart type, and field mappings.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 6: Meta Fields (Name, Comment)
|
## Phase 6: Meta Fields (Name, Comment) ✅ COMPLETE
|
||||||
|
|
||||||
**Status**: Planned
|
**Status**: Done
|
||||||
**Files**: `index.html`, `src/js/snippet-manager.js`
|
**Files**: `index.html`, `src/js/snippet-manager.js`
|
||||||
|
|
||||||
### What to Convert
|
### What Was Converted
|
||||||
|
|
||||||
Name and comment fields with auto-save functionality.
|
- Name and comment input fields with `x-model` bindings
|
||||||
|
- Debounced auto-save functionality moved to Alpine component
|
||||||
|
- Metadata loading when snippet is selected
|
||||||
|
|
||||||
### Implementation Approach
|
### Implementation Approach
|
||||||
|
|
||||||
1. Add `snippetName` and `snippetComment` to `snippetList()` component
|
1. Added `snippetName` and `snippetComment` properties to `snippetList()` component
|
||||||
2. Use `x-model` with debounced input handlers
|
2. Added `loadMetadata()` method to load fields when snippet selected
|
||||||
3. Call `loadMetadata()` when snippet selected
|
3. Added `saveMetaDebounced()` and `saveMeta()` methods for auto-saving
|
||||||
4. Auto-save on change with 500ms debounce
|
4. Converted HTML inputs to use `x-model` with `@input="saveMetaDebounced()"`
|
||||||
|
5. Updated `selectSnippet()` to call Alpine component's `loadMetadata()` via `_x_dataStack`
|
||||||
|
6. Removed vanilla event listeners and old `autoSaveMeta()`/`debouncedAutoSaveMeta()` functions
|
||||||
|
|
||||||
### What Stays Vanilla
|
### What Stays Vanilla
|
||||||
|
|
||||||
- SnippetStorage save operations
|
- SnippetStorage save operations (called from Alpine component)
|
||||||
|
- Name generation logic (generateSnippetName)
|
||||||
|
- Snippet list re-rendering after save
|
||||||
|
|
||||||
|
### Key Learnings
|
||||||
|
|
||||||
|
- Alpine's `x-model` provides two-way data binding for inputs
|
||||||
|
- `@input` event handler triggers debounced save on every keystroke
|
||||||
|
- Alpine component accessed via DOM element's `_x_dataStack[0]` property
|
||||||
|
- Debounce timeout stored in component state for proper cleanup
|
||||||
|
- Net code reduction: ~40 lines of manual event listener setup removed
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -284,7 +298,7 @@ Toast notification system with auto-dismiss.
|
|||||||
2. ✅ **Phase 2: Dataset Manager** - DONE
|
2. ✅ **Phase 2: Dataset Manager** - DONE
|
||||||
3. ✅ **Phase 3: View Mode Toggle** - DONE
|
3. ✅ **Phase 3: View Mode Toggle** - DONE
|
||||||
4. ✅ **Phase 4: Settings Modal** - DONE
|
4. ✅ **Phase 4: Settings Modal** - DONE
|
||||||
5. **Phase 6: Meta Fields** - Before Chart Builder (simpler)
|
5. ✅ **Phase 6: Meta Fields** - DONE
|
||||||
6. **Phase 7: Panel Toggles** - Quick win
|
6. **Phase 7: Panel Toggles** - Quick win
|
||||||
7. **Phase 5: Chart Builder** - More complex, save for when confident
|
7. **Phase 5: Chart Builder** - More complex, save for when confident
|
||||||
8. **Phase 8: Toast Notifications** - Optional polish
|
8. **Phase 8: Toast Notifications** - Optional polish
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ function snippetList() {
|
|||||||
sortBy: AppSettings.get('sortBy') || 'modified',
|
sortBy: AppSettings.get('sortBy') || 'modified',
|
||||||
sortOrder: AppSettings.get('sortOrder') || 'desc',
|
sortOrder: AppSettings.get('sortOrder') || 'desc',
|
||||||
|
|
||||||
|
// Meta fields for selected snippet
|
||||||
|
snippetName: '',
|
||||||
|
snippetComment: '',
|
||||||
|
metaSaveTimeout: null,
|
||||||
|
|
||||||
// Computed property: calls SnippetStorage with current filters/sort
|
// Computed property: calls SnippetStorage with current filters/sort
|
||||||
get filteredSnippets() {
|
get filteredSnippets() {
|
||||||
return SnippetStorage.listSnippets(
|
return SnippetStorage.listSnippets(
|
||||||
@@ -62,6 +67,34 @@ function snippetList() {
|
|||||||
return JSON.stringify(snippet.spec) !== JSON.stringify(snippet.draftSpec);
|
return JSON.stringify(snippet.spec) !== JSON.stringify(snippet.draftSpec);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Load meta fields when a snippet is selected
|
||||||
|
loadMetadata(snippet) {
|
||||||
|
this.snippetName = snippet.name || '';
|
||||||
|
this.snippetComment = snippet.comment || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save meta fields with debouncing (called via x-model watchers)
|
||||||
|
saveMetaDebounced() {
|
||||||
|
clearTimeout(this.metaSaveTimeout);
|
||||||
|
this.metaSaveTimeout = setTimeout(() => this.saveMeta(), 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save meta fields to storage
|
||||||
|
saveMeta() {
|
||||||
|
const snippet = getCurrentSnippet();
|
||||||
|
if (snippet) {
|
||||||
|
snippet.name = this.snippetName.trim() || generateSnippetName();
|
||||||
|
snippet.comment = this.snippetComment;
|
||||||
|
SnippetStorage.saveSnippet(snippet);
|
||||||
|
|
||||||
|
// Update the snippet list display to reflect the new name
|
||||||
|
renderSnippetList();
|
||||||
|
|
||||||
|
// Restore selection after re-render
|
||||||
|
restoreSnippetSelection();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
selectSnippet(snippetId) {
|
selectSnippet(snippetId) {
|
||||||
window.selectSnippet(snippetId);
|
window.selectSnippet(snippetId);
|
||||||
@@ -469,16 +502,21 @@ function selectSnippet(snippetId, updateURL = true) {
|
|||||||
|
|
||||||
// Show and populate meta fields
|
// Show and populate meta fields
|
||||||
const metaSection = document.getElementById('snippet-meta');
|
const metaSection = document.getElementById('snippet-meta');
|
||||||
const nameField = document.getElementById('snippet-name');
|
|
||||||
const commentField = document.getElementById('snippet-comment');
|
|
||||||
const createdField = document.getElementById('snippet-created');
|
const createdField = document.getElementById('snippet-created');
|
||||||
const modifiedField = document.getElementById('snippet-modified');
|
const modifiedField = document.getElementById('snippet-modified');
|
||||||
const placeholder = document.querySelector('.placeholder');
|
const placeholder = document.querySelector('.placeholder');
|
||||||
|
|
||||||
if (metaSection && nameField && commentField) {
|
if (metaSection) {
|
||||||
metaSection.style.display = 'block';
|
metaSection.style.display = 'block';
|
||||||
nameField.value = snippet.name || '';
|
|
||||||
commentField.value = snippet.comment || '';
|
// Load metadata into Alpine component
|
||||||
|
const snippetPanel = document.getElementById('snippet-panel');
|
||||||
|
if (snippetPanel && snippetPanel._x_dataStack) {
|
||||||
|
const alpineData = snippetPanel._x_dataStack[0];
|
||||||
|
if (alpineData && typeof alpineData.loadMetadata === 'function') {
|
||||||
|
alpineData.loadMetadata(snippet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Format and display dates
|
// Format and display dates
|
||||||
if (createdField) {
|
if (createdField) {
|
||||||
@@ -488,7 +526,9 @@ function selectSnippet(snippetId, updateURL = true) {
|
|||||||
modifiedField.textContent = formatFullDate(snippet.modified);
|
modifiedField.textContent = formatFullDate(snippet.modified);
|
||||||
}
|
}
|
||||||
|
|
||||||
placeholder.style.display = 'none';
|
if (placeholder) {
|
||||||
|
placeholder.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store currently selected snippet ID globally
|
// Store currently selected snippet ID globally
|
||||||
@@ -612,21 +652,7 @@ function debouncedAutoSave() {
|
|||||||
|
|
||||||
// Initialize auto-save on editor changes
|
// Initialize auto-save on editor changes
|
||||||
function initializeAutoSave() {
|
function initializeAutoSave() {
|
||||||
// Initialize meta fields auto-save
|
// Meta fields auto-save now handled by Alpine.js in snippetList() component
|
||||||
const nameField = document.getElementById('snippet-name');
|
|
||||||
const commentField = document.getElementById('snippet-comment');
|
|
||||||
|
|
||||||
if (nameField) {
|
|
||||||
nameField.addEventListener('input', () => {
|
|
||||||
debouncedAutoSaveMeta();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (commentField) {
|
|
||||||
commentField.addEventListener('input', () => {
|
|
||||||
debouncedAutoSaveMeta();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize button event listeners
|
// Initialize button event listeners
|
||||||
const duplicateBtn = document.getElementById('duplicate-btn');
|
const duplicateBtn = document.getElementById('duplicate-btn');
|
||||||
@@ -649,33 +675,6 @@ function initializeAutoSave() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save meta fields (name and comment) for the selected snippet
|
|
||||||
function autoSaveMeta() {
|
|
||||||
const nameField = document.getElementById('snippet-name');
|
|
||||||
const commentField = document.getElementById('snippet-comment');
|
|
||||||
if (!nameField || !commentField) return;
|
|
||||||
|
|
||||||
const snippet = getCurrentSnippet();
|
|
||||||
if (snippet) {
|
|
||||||
snippet.name = nameField.value.trim() || generateSnippetName();
|
|
||||||
snippet.comment = commentField.value;
|
|
||||||
SnippetStorage.saveSnippet(snippet);
|
|
||||||
|
|
||||||
// Update the snippet list display to reflect the new name
|
|
||||||
renderSnippetList();
|
|
||||||
|
|
||||||
// Restore selection after re-render
|
|
||||||
restoreSnippetSelection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounced meta auto-save
|
|
||||||
let metaAutoSaveTimeout;
|
|
||||||
function debouncedAutoSaveMeta() {
|
|
||||||
clearTimeout(metaAutoSaveTimeout);
|
|
||||||
metaAutoSaveTimeout = setTimeout(autoSaveMeta, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRUD Operations
|
// CRUD Operations
|
||||||
|
|
||||||
// Create new snippet
|
// Create new snippet
|
||||||
|
|||||||
Reference in New Issue
Block a user