mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2025-12-21 21:22:25 +00:00
feat: implement draft versioning and UI controls for snippet management
This commit is contained in:
@@ -28,8 +28,11 @@
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>Editor</h2>
|
<h2>Editor</h2>
|
||||||
|
<div class="editor-controls">
|
||||||
|
<button class="button secondary" id="version-switch" style="display: none">View Saved Version</button>
|
||||||
<button class="button" id="save-snippet" disabled>Save Changes</button>
|
<button class="button" id="save-snippet" disabled>Save Changes</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div id="monaco-editor"></div>
|
<div id="monaco-editor"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="resize-handle"></div>
|
<div class="resize-handle"></div>
|
||||||
|
|||||||
@@ -2,10 +2,21 @@ export class SnippetManager {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.currentSnippetId = null;
|
this.currentSnippetId = null;
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
|
this.isDraftVersion = false;
|
||||||
|
this.drafts = new Map(); // Store draft versions
|
||||||
|
this.readOnlyMode = false;
|
||||||
this.loadSnippets();
|
this.loadSnippets();
|
||||||
|
this.loadDrafts();
|
||||||
this.setupUI();
|
this.setupUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasDraftChanges(id) {
|
||||||
|
if (!this.drafts.has(id)) return false;
|
||||||
|
const snippet = this.snippets.find(s => s.id === id);
|
||||||
|
const draft = this.drafts.get(id);
|
||||||
|
return JSON.stringify(snippet.content) !== JSON.stringify(draft);
|
||||||
|
}
|
||||||
|
|
||||||
loadSnippets() {
|
loadSnippets() {
|
||||||
// Try to load from localStorage
|
// Try to load from localStorage
|
||||||
const stored = localStorage.getItem('vegaSnippets');
|
const stored = localStorage.getItem('vegaSnippets');
|
||||||
@@ -17,10 +28,23 @@ export class SnippetManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadDrafts() {
|
||||||
|
const stored = localStorage.getItem('vegaDrafts');
|
||||||
|
if (stored) {
|
||||||
|
const draftsObj = JSON.parse(stored);
|
||||||
|
this.drafts = new Map(Object.entries(draftsObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveToStorage() {
|
saveToStorage() {
|
||||||
localStorage.setItem('vegaSnippets', JSON.stringify(this.snippets));
|
localStorage.setItem('vegaSnippets', JSON.stringify(this.snippets));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveDraftsToStorage() {
|
||||||
|
const draftsObj = Object.fromEntries(this.drafts);
|
||||||
|
localStorage.setItem('vegaDrafts', JSON.stringify(draftsObj));
|
||||||
|
}
|
||||||
|
|
||||||
renderSnippetList() {
|
renderSnippetList() {
|
||||||
const container = document.getElementById('snippet-list');
|
const container = document.getElementById('snippet-list');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
@@ -28,25 +52,32 @@ export class SnippetManager {
|
|||||||
this.snippets.forEach(snippet => {
|
this.snippets.forEach(snippet => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = `snippet-item ${snippet.id === this.currentSnippetId ? 'active' : ''}`;
|
div.className = `snippet-item ${snippet.id === this.currentSnippetId ? 'active' : ''}`;
|
||||||
div.textContent = snippet.name;
|
const hasChanges = this.hasDraftChanges(snippet.id);
|
||||||
|
const indicator = hasChanges ? '🟡' : '🟢';
|
||||||
|
div.textContent = `${indicator} ${snippet.name}`;
|
||||||
div.onclick = () => this.loadSnippet(snippet.id);
|
div.onclick = () => this.loadSnippet(snippet.id);
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSnippet(id) {
|
loadSnippet(id, forceDraft = null) {
|
||||||
if (this.hasUnsavedChanges) {
|
|
||||||
if (!confirm('You have unsaved changes. Do you want to discard them?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const snippet = this.snippets.find(s => s.id === id);
|
const snippet = this.snippets.find(s => s.id === id);
|
||||||
if (snippet) {
|
if (snippet) {
|
||||||
this.currentSnippetId = id;
|
this.currentSnippetId = id;
|
||||||
this.editor.setValue(JSON.stringify(snippet.content, null, 2));
|
const hasChanges = this.hasDraftChanges(id);
|
||||||
|
|
||||||
|
// Default to draft if available, unless explicitly specified
|
||||||
|
this.isDraftVersion = forceDraft !== null ? forceDraft : hasChanges;
|
||||||
|
|
||||||
|
const content = this.isDraftVersion && this.drafts.has(id) ?
|
||||||
|
this.drafts.get(id) :
|
||||||
|
snippet.content;
|
||||||
|
|
||||||
|
this.editor.setValue(JSON.stringify(content, null, 2));
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
|
this.updateReadOnlyState();
|
||||||
this.updateSaveButton();
|
this.updateSaveButton();
|
||||||
|
this.updateVersionSwitch();
|
||||||
this.renderSnippetList();
|
this.renderSnippetList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +102,26 @@ export class SnippetManager {
|
|||||||
this.loadSnippet(id);
|
this.loadSnippet(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveDraft() {
|
||||||
|
if (!this.currentSnippetId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = JSON.parse(this.editor.getValue());
|
||||||
|
const currentSnippet = this.snippets.find(s => s.id === this.currentSnippetId);
|
||||||
|
|
||||||
|
// Only save draft if content is different from saved version
|
||||||
|
if (JSON.stringify(content) !== JSON.stringify(currentSnippet.content)) {
|
||||||
|
this.drafts.set(this.currentSnippetId, content);
|
||||||
|
this.isDraftVersion = true;
|
||||||
|
this.saveDraftsToStorage();
|
||||||
|
this.renderSnippetList();
|
||||||
|
this.updateVersionSwitch();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid JSON in editor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
saveCurrentSnippet() {
|
saveCurrentSnippet() {
|
||||||
if (!this.currentSnippetId) return;
|
if (!this.currentSnippetId) return;
|
||||||
|
|
||||||
@@ -80,9 +131,14 @@ export class SnippetManager {
|
|||||||
|
|
||||||
if (snippetIndex !== -1) {
|
if (snippetIndex !== -1) {
|
||||||
this.snippets[snippetIndex].content = content;
|
this.snippets[snippetIndex].content = content;
|
||||||
|
this.drafts.delete(this.currentSnippetId); // Remove draft after saving
|
||||||
|
this.saveDraftsToStorage();
|
||||||
this.saveToStorage();
|
this.saveToStorage();
|
||||||
this.hasUnsavedChanges = false;
|
this.hasUnsavedChanges = false;
|
||||||
|
this.isDraftVersion = false;
|
||||||
this.updateSaveButton();
|
this.updateSaveButton();
|
||||||
|
this.updateVersionSwitch();
|
||||||
|
this.renderSnippetList();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Invalid JSON in editor');
|
alert('Invalid JSON in editor');
|
||||||
@@ -94,6 +150,19 @@ export class SnippetManager {
|
|||||||
saveButton.disabled = !this.hasUnsavedChanges;
|
saveButton.disabled = !this.hasUnsavedChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateVersionSwitch() {
|
||||||
|
const versionSwitch = document.getElementById('version-switch');
|
||||||
|
if (!versionSwitch) return;
|
||||||
|
|
||||||
|
const hasChanges = this.hasDraftChanges(this.currentSnippetId);
|
||||||
|
versionSwitch.style.display = hasChanges ? 'block' : 'none';
|
||||||
|
|
||||||
|
const buttonText = this.isDraftVersion ?
|
||||||
|
'View Saved Version (Read-only)' :
|
||||||
|
'Switch to Draft Version (Editable)';
|
||||||
|
versionSwitch.textContent = buttonText;
|
||||||
|
}
|
||||||
|
|
||||||
setupUI() {
|
setupUI() {
|
||||||
// New snippet button
|
// New snippet button
|
||||||
document.getElementById('new-snippet').onclick = () => this.createNewSnippet();
|
document.getElementById('new-snippet').onclick = () => this.createNewSnippet();
|
||||||
@@ -101,6 +170,12 @@ export class SnippetManager {
|
|||||||
// Save button
|
// Save button
|
||||||
document.getElementById('save-snippet').onclick = () => this.saveCurrentSnippet();
|
document.getElementById('save-snippet').onclick = () => this.saveCurrentSnippet();
|
||||||
|
|
||||||
|
// Version switch button
|
||||||
|
const versionSwitch = document.getElementById('version-switch');
|
||||||
|
versionSwitch.onclick = () => {
|
||||||
|
this.loadSnippet(this.currentSnippetId, !this.isDraftVersion);
|
||||||
|
};
|
||||||
|
|
||||||
// Initial render
|
// Initial render
|
||||||
this.renderSnippetList();
|
this.renderSnippetList();
|
||||||
}
|
}
|
||||||
@@ -108,15 +183,33 @@ export class SnippetManager {
|
|||||||
setEditor(editor) {
|
setEditor(editor) {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
|
|
||||||
// Setup change tracking and visualization update
|
|
||||||
let timeoutId = null;
|
let timeoutId = null;
|
||||||
editor.onDidChangeModelContent(() => {
|
editor.onDidChangeModelContent((e) => {
|
||||||
|
// Only show warning if we're in read-only mode AND this is a user edit
|
||||||
|
// (not a programmatic change from loadSnippet)
|
||||||
|
if (this.readOnlyMode && e.isUndoRedo === false) {
|
||||||
|
if (confirm('Editing the saved version will overwrite your draft. Continue?')) {
|
||||||
|
this.readOnlyMode = false;
|
||||||
|
this.isDraftVersion = true;
|
||||||
|
this.drafts.delete(this.currentSnippetId);
|
||||||
|
this.saveDraftsToStorage();
|
||||||
|
this.updateVersionSwitch();
|
||||||
|
this.renderSnippetList();
|
||||||
|
} else {
|
||||||
|
// Revert the change
|
||||||
|
const snippet = this.snippets.find(s => s.id === this.currentSnippetId);
|
||||||
|
this.editor.setValue(JSON.stringify(snippet.content, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.hasUnsavedChanges = true;
|
this.hasUnsavedChanges = true;
|
||||||
this.updateSaveButton();
|
this.updateSaveButton();
|
||||||
|
|
||||||
// Debounce visualization updates
|
// Auto-save to draft
|
||||||
if (timeoutId) clearTimeout(timeoutId);
|
if (timeoutId) clearTimeout(timeoutId);
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
|
this.saveDraft();
|
||||||
try {
|
try {
|
||||||
const value = editor.getValue();
|
const value = editor.getValue();
|
||||||
this.updateVisualization(value);
|
this.updateVisualization(value);
|
||||||
@@ -132,6 +225,12 @@ export class SnippetManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateReadOnlyState() {
|
||||||
|
const hasChanges = this.hasDraftChanges(this.currentSnippetId);
|
||||||
|
this.readOnlyMode = hasChanges && !this.isDraftVersion;
|
||||||
|
this.editor.updateOptions({ readOnly: this.readOnlyMode });
|
||||||
|
}
|
||||||
|
|
||||||
async updateVisualization(spec) {
|
async updateVisualization(spec) {
|
||||||
try {
|
try {
|
||||||
const parsedSpec = typeof spec === 'string' ? JSON.parse(spec) : spec;
|
const parsedSpec = typeof spec === 'string' ? JSON.parse(spec) : spec;
|
||||||
|
|||||||
14
styles.css
14
styles.css
@@ -63,6 +63,14 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button.secondary {
|
||||||
|
background: #607D8B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.secondary:hover {
|
||||||
|
background: #546E7A;
|
||||||
|
}
|
||||||
|
|
||||||
.snippet-list {
|
.snippet-list {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -94,3 +102,9 @@
|
|||||||
.preview-panel>*:not(.panel-header) {
|
.preview-panel>*:not(.panel-header) {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user