Append submenu

This commit is contained in:
Alexopus
2025-07-19 23:49:17 +02:00
parent 1e4db67254
commit 1febceb0ff
3 changed files with 112 additions and 25 deletions
+11 -1
View File
@@ -35,11 +35,17 @@ This is a ComfyUI extension that adds a server-side notes sidebar for managing m
## API Endpoints
The extension exposes these REST endpoints:
### Notes Management
- `GET /notes_sidebar/list` - List all notes
- `GET /notes_sidebar/note/{note_name}` - Get specific note content
- `POST /notes_sidebar/note/{note_name}` - Save/create note
- `DELETE /notes_sidebar/note/{note_name}` - Delete note
### LoRA Integration
- `GET /notes_sidebar/loras` - List all available LoRA files
- `GET /notes_sidebar/loras_with_metadata` - List LoRAs with associated metadata from `.txt` files
## Development Notes
### Extension Structure
@@ -56,10 +62,11 @@ The extension exposes these REST endpoints:
- Active note tracking with tab highlighting
- Content persistence across sidebar open/close cycles
- Real-time tab rendering on note operations
- Button layout: Save/Rename on left, Refresh/Delete on right
- Button layout: Save/Rename/LoRA submenu on left, Refresh/Delete on right
- **Unsaved Changes Tracking**: Visual indicators (*) for modified notes
- **Content Preservation**: Tab switching preserves unsaved content
- **Change Detection**: Automatic tracking of modifications vs saved state
- **LoRA Integration**: Submenu with three formatting options for appending LoRA lists
### Unsaved Changes Behavior
- Notes with modifications show asterisk (*) in tab name
@@ -74,6 +81,7 @@ The extension exposes these REST endpoints:
- **API Design**: RESTful endpoints with JSON responses and consistent error handling
- **UI Updates**: Event-driven architecture prevents manual DOM manipulation
- **Extension Integration**: Follows ComfyUI's extension patterns (NODE_CLASS_MAPPINGS, registerExtension)
- **ComfyUI Integration**: Leverages `folder_paths` module for LoRA discovery and metadata loading
## Development Commands
@@ -88,6 +96,8 @@ No automated tests defined. Manual testing workflow:
2. Verify sidebar tab appears with "Notes" label and book icon
3. Test CRUD operations: create, edit, save, rename, delete notes
4. Verify tab switching and content persistence
5. Test LoRA integration: verify submenu appears on click with three format options
6. Verify unsaved changes tracking with asterisk (*) indicators
### Development Workflow
1. Modify Python code in `__init__.py` for backend changes
+69 -20
View File
@@ -173,7 +173,7 @@ class NotesSidebar {
}
updateButtonState() {
if (!this.saveBtn || !this.renameBtn || !this.deleteBtn || !this.appendLorasBtn || !this.appendLorasTagsBtn || !this.appendLorasMetaBtn) return;
if (!this.saveBtn || !this.renameBtn || !this.deleteBtn || !this.appendLorasBtn) return;
const hasActiveNote = !!this.activeNote;
const hasUnsavedChanges = hasActiveNote && this.unsavedChanges.get(this.activeNote);
@@ -185,8 +185,6 @@ class NotesSidebar {
this.renameBtn.disabled = !hasActiveNote;
this.deleteBtn.disabled = !hasActiveNote;
this.appendLorasBtn.disabled = !hasActiveNote;
this.appendLorasTagsBtn.disabled = !hasActiveNote;
this.appendLorasMetaBtn.disabled = !hasActiveNote;
}
async init() {
@@ -290,32 +288,48 @@ class NotesSidebar {
this.deleteBtn.onclick = () => this.deleteActiveNote();
this.deleteBtn.disabled = true;
// LoRA submenu container
this.loraMenuContainer = document.createElement('div');
this.loraMenuContainer.className = 'notes-submenu-container';
this.appendLorasBtn = document.createElement('button');
this.appendLorasBtn.innerHTML = '<i class="pi pi-plus-circle"></i>';
this.appendLorasBtn.className = 'notes-btn append-loras';
this.appendLorasBtn.title = 'Append LoRAs List';
this.appendLorasBtn.onclick = () => this.appendLorasList();
this.appendLorasBtn.title = 'Append';
this.appendLorasBtn.onclick = () => this.toggleLoraMenu();
this.appendLorasBtn.disabled = true;
this.appendLorasTagsBtn = document.createElement('button');
this.appendLorasTagsBtn.innerHTML = '<i class="pi pi-tags"></i>';
this.appendLorasTagsBtn.className = 'notes-btn append-loras-tags';
this.appendLorasTagsBtn.title = 'Append LoRAs as Tags';
this.appendLorasTagsBtn.onclick = () => this.appendLorasTags();
this.appendLorasTagsBtn.disabled = true;
// Create submenu
this.loraSubmenu = document.createElement('div');
this.loraSubmenu.className = 'notes-submenu';
this.loraSubmenu.style.display = 'none';
this.appendLorasMetaBtn = document.createElement('button');
this.appendLorasMetaBtn.innerHTML = '<i class="pi pi-info-circle"></i>';
this.appendLorasMetaBtn.className = 'notes-btn append-loras-meta';
this.appendLorasMetaBtn.title = 'Append LoRAs with Metadata';
this.appendLorasMetaBtn.onclick = () => this.appendLorasWithMetadata();
this.appendLorasMetaBtn.disabled = true;
// Create submenu items
const rawOption = document.createElement('div');
rawOption.className = 'notes-submenu-item';
rawOption.textContent = 'Append raw LoRA filenames list';
rawOption.onclick = () => this.selectLoraOption('raw');
const tagOption = document.createElement('div');
tagOption.className = 'notes-submenu-item';
tagOption.textContent = "Append LoRAs in '<lora:name>' format";
tagOption.onclick = () => this.selectLoraOption('tag');
const metaOption = document.createElement('div');
metaOption.className = 'notes-submenu-item';
metaOption.textContent = "Append LoRAs in '<lora:name>, metadata' format";
metaOption.onclick = () => this.selectLoraOption('meta');
this.loraSubmenu.appendChild(rawOption);
this.loraSubmenu.appendChild(tagOption);
this.loraSubmenu.appendChild(metaOption);
this.loraMenuContainer.appendChild(this.appendLorasBtn);
this.loraMenuContainer.appendChild(this.loraSubmenu);
leftButtonContainer.appendChild(this.saveBtn);
leftButtonContainer.appendChild(this.renameBtn);
leftButtonContainer.appendChild(this.appendLorasBtn);
leftButtonContainer.appendChild(this.appendLorasTagsBtn);
leftButtonContainer.appendChild(this.appendLorasMetaBtn);
leftButtonContainer.appendChild(this.loraMenuContainer);
rightButtonContainer.appendChild(this.deleteBtn);
buttonContainer.appendChild(leftButtonContainer);
@@ -579,6 +593,41 @@ class NotesSidebar {
this.store.setUnsavedChanges(this.activeNote, hasUnsavedChanges);
}
toggleLoraMenu() {
if (this.loraSubmenu.style.display === 'none') {
this.loraSubmenu.style.display = 'block';
// Add click listener to close menu when clicking outside
document.addEventListener('click', this.closeLoraMenuHandler);
} else {
this.loraSubmenu.style.display = 'none';
document.removeEventListener('click', this.closeLoraMenuHandler);
}
}
closeLoraMenuHandler = (event) => {
if (!this.loraMenuContainer.contains(event.target)) {
this.loraSubmenu.style.display = 'none';
document.removeEventListener('click', this.closeLoraMenuHandler);
}
}
selectLoraOption(type) {
this.loraSubmenu.style.display = 'none';
document.removeEventListener('click', this.closeLoraMenuHandler);
switch(type) {
case 'raw':
this.appendLorasList();
break;
case 'tag':
this.appendLorasTags();
break;
case 'meta':
this.appendLorasWithMetadata();
break;
}
}
async appendLorasList() {
try {
const response = await fetch('/notes_sidebar/loras');
+32 -4
View File
@@ -152,10 +152,38 @@
background: #9C27B0;
}
.notes-btn.append-loras-tags {
background: #673AB7;
/* Submenu styles */
.notes-submenu-container {
position: relative;
display: inline-block;
}
.notes-btn.append-loras-meta {
background: #3F51B5;
.notes-submenu {
position: absolute;
top: 100%;
left: 0;
background: #2a2a2a;
border: 1px solid #444;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
z-index: 1000;
min-width: 200px;
margin-top: 2px;
}
.notes-submenu-item {
padding: 8px 12px;
color: #fff;
cursor: pointer;
border-bottom: 1px solid #444;
font-size: 12px;
white-space: nowrap;
}
.notes-submenu-item:last-child {
border-bottom: none;
}
.notes-submenu-item:hover {
background: #444;
}