2983 lines
103 KiB
JavaScript
2983 lines
103 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
||
import { api } from "../../scripts/api.js";
|
||
|
||
class AutoNotesManager {
|
||
constructor() {
|
||
this.sidebar = null;
|
||
// Load last display mode from localStorage, default to "all"
|
||
this.displayMode = localStorage.getItem('autonotes_displayMode') || "all";
|
||
this.notes = [];
|
||
this.folders = [];
|
||
this.editDialog = null;
|
||
this.selectedNodeType = null;
|
||
this.selectedNodeAttributes = {};
|
||
this.workflowName = "";
|
||
this.nodeTypes = {}; // Store node types and their definitions
|
||
this.expandedFolders = new Set(); // Track which folders are expanded
|
||
this.lastSelectedNodeType = null; // Track last selected node to detect changes
|
||
|
||
this.init();
|
||
}
|
||
|
||
async init() {
|
||
// Wait for app to be ready
|
||
await new Promise(resolve => {
|
||
if (app.canvas) {
|
||
resolve();
|
||
} else {
|
||
app.registerExtension({
|
||
name: "AutoNotes.Init",
|
||
setup() {
|
||
resolve();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Load marked library
|
||
await this.loadMarkedLibrary();
|
||
|
||
// Load node definitions
|
||
await this.loadNodeTypes();
|
||
|
||
this.createSidebar();
|
||
this.setupEventListeners();
|
||
await this.refreshNotes();
|
||
}
|
||
|
||
async loadMarkedLibrary() {
|
||
// Load the marked library using script tag since it's UMD format
|
||
if (typeof window.marked !== 'undefined') {
|
||
this.marked = window.marked;
|
||
return;
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const script = document.createElement('script');
|
||
script.src = '/extensions/ComfyUI-AutoNotes/marked.min.js';
|
||
script.onload = () => {
|
||
this.marked = window.marked;
|
||
resolve();
|
||
};
|
||
script.onerror = () => {
|
||
console.error('Failed to load marked library');
|
||
this.marked = null;
|
||
resolve(); // Resolve anyway, will use fallback
|
||
};
|
||
document.head.appendChild(script);
|
||
});
|
||
}
|
||
|
||
async loadNodeTypes() {
|
||
try {
|
||
const response = await api.fetchApi('/object_info');
|
||
this.nodeTypes = await response.json();
|
||
} catch (error) {
|
||
console.error('Failed to load node types:', error);
|
||
this.nodeTypes = {};
|
||
}
|
||
}
|
||
|
||
createSidebar() {
|
||
// Load saved width from localStorage
|
||
const savedWidth = localStorage.getItem('autonotes_sidebarWidth') || '300';
|
||
|
||
// Calculate top bar height dynamically
|
||
const calculateTopBarHeight = () => {
|
||
// Check if user has manually set a value in localStorage
|
||
const manualHeight = localStorage.getItem('autonotes_topBarHeight');
|
||
if (manualHeight) {
|
||
const height = parseInt(manualHeight);
|
||
console.log('AutoNotes: Using manually configured top bar height:', height);
|
||
return height;
|
||
}
|
||
|
||
// Look for ComfyUI's main menu bar that contains buttons like Manager, Queue, etc.
|
||
const allElements = document.querySelectorAll('*');
|
||
let candidates = [];
|
||
|
||
for (const el of allElements) {
|
||
const style = window.getComputedStyle(el);
|
||
const position = style.position;
|
||
|
||
if (position === 'fixed' || position === 'absolute') {
|
||
const rect = el.getBoundingClientRect();
|
||
// Look for elements near the top that are reasonably wide (likely to be top bars)
|
||
// Relaxed criteria: width > 300 instead of 500
|
||
if (rect.top >= 0 && rect.top <= 100 && rect.width > 300 && rect.bottom < 250 && rect.bottom > 50) {
|
||
candidates.push({ bottom: rect.bottom, width: rect.width, element: el.tagName });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Sort by bottom position
|
||
if (candidates.length > 0) {
|
||
candidates.sort((a, b) => b.bottom - a.bottom);
|
||
const topBarBottom = Math.ceil(candidates[0].bottom);
|
||
console.log('AutoNotes: Detected top bar height:', topBarBottom, 'from', candidates[0].element, 'candidates:', candidates.length);
|
||
console.log('AutoNotes: To manually override, set: localStorage.setItem("autonotes_topBarHeight", "YOUR_VALUE")');
|
||
return topBarBottom;
|
||
}
|
||
|
||
// Fallback - use a reasonable default that's close to the top bar
|
||
console.log('AutoNotes: Using fallback top bar height: 140 (no candidates found)');
|
||
console.log('AutoNotes: To manually override, set: localStorage.setItem("autonotes_topBarHeight", "YOUR_VALUE")');
|
||
return 140;
|
||
};
|
||
|
||
const topBarHeight = calculateTopBarHeight();
|
||
|
||
// Create sidebar container - full height with padding at top
|
||
this.sidebar = document.createElement('div');
|
||
this.sidebar.id = 'autonotes-sidebar';
|
||
this.sidebar.style.cssText = `
|
||
position: fixed;
|
||
right: 0;
|
||
top: 0;
|
||
width: ${savedWidth}px;
|
||
height: 100vh;
|
||
background: #2a2a2a;
|
||
border-left: 1px solid #555;
|
||
z-index: 1000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: ${topBarHeight}px 10px 10px 10px;
|
||
box-sizing: border-box;
|
||
font-family: Arial, sans-serif;
|
||
color: #fff;
|
||
`;
|
||
|
||
// Create resize handle
|
||
const resizeHandle = document.createElement('div');
|
||
resizeHandle.style.cssText = `
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 5px;
|
||
height: 100%;
|
||
cursor: ew-resize;
|
||
background: transparent;
|
||
z-index: 1001;
|
||
`;
|
||
|
||
resizeHandle.addEventListener('mouseenter', () => {
|
||
resizeHandle.style.background = '#007acc';
|
||
});
|
||
|
||
resizeHandle.addEventListener('mouseleave', () => {
|
||
if (!this.isResizing) {
|
||
resizeHandle.style.background = 'transparent';
|
||
}
|
||
});
|
||
|
||
// Resize functionality
|
||
this.isResizing = false;
|
||
let startX, startWidth;
|
||
|
||
resizeHandle.addEventListener('mousedown', (e) => {
|
||
this.isResizing = true;
|
||
startX = e.clientX;
|
||
startWidth = parseInt(this.sidebar.style.width);
|
||
resizeHandle.style.background = '#007acc';
|
||
document.body.style.cursor = 'ew-resize';
|
||
document.body.style.userSelect = 'none';
|
||
e.preventDefault();
|
||
});
|
||
|
||
document.addEventListener('mousemove', (e) => {
|
||
if (!this.isResizing) return;
|
||
|
||
const dx = startX - e.clientX;
|
||
const newWidth = Math.max(200, Math.min(800, startWidth + dx));
|
||
this.sidebar.style.width = newWidth + 'px';
|
||
});
|
||
|
||
document.addEventListener('mouseup', () => {
|
||
if (this.isResizing) {
|
||
this.isResizing = false;
|
||
resizeHandle.style.background = 'transparent';
|
||
document.body.style.cursor = '';
|
||
document.body.style.userSelect = '';
|
||
|
||
// Save width to localStorage
|
||
localStorage.setItem('autonotes_sidebarWidth', parseInt(this.sidebar.style.width));
|
||
}
|
||
});
|
||
|
||
this.sidebar.appendChild(resizeHandle);
|
||
|
||
// Control panel
|
||
const controlPanel = document.createElement('div');
|
||
controlPanel.style.cssText = `
|
||
margin-bottom: 15px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #555;
|
||
`;
|
||
|
||
// Display mode selector
|
||
const modeContainer = document.createElement('div');
|
||
modeContainer.style.marginBottom = '10px';
|
||
|
||
const modeLabel = document.createElement('label');
|
||
modeLabel.textContent = 'Display: ';
|
||
modeLabel.style.marginRight = '10px';
|
||
|
||
const modeSelect = document.createElement('select');
|
||
modeSelect.style.cssText = `
|
||
background: #333;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
padding: 5px;
|
||
`;
|
||
modeSelect.innerHTML = `
|
||
<option value="all">All</option>
|
||
<option value="automatic">Automatic</option>
|
||
`;
|
||
modeSelect.value = this.displayMode;
|
||
modeSelect.addEventListener('change', (e) => {
|
||
this.displayMode = e.target.value;
|
||
localStorage.setItem('autonotes_displayMode', this.displayMode);
|
||
this.refreshNotes();
|
||
});
|
||
|
||
modeContainer.appendChild(modeLabel);
|
||
modeContainer.appendChild(modeSelect);
|
||
|
||
// Buttons
|
||
const buttonContainer = document.createElement('div');
|
||
buttonContainer.style.display = 'flex';
|
||
buttonContainer.style.gap = '10px';
|
||
|
||
const addButton = document.createElement('button');
|
||
addButton.textContent = 'Add';
|
||
addButton.style.cssText = `
|
||
background: #007acc;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
`;
|
||
addButton.addEventListener('click', () => this.addNote());
|
||
|
||
const editButton = document.createElement('button');
|
||
editButton.textContent = 'Edit';
|
||
editButton.style.cssText = `
|
||
background: #666;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
`;
|
||
editButton.addEventListener('click', () => this.openEditDialog());
|
||
|
||
buttonContainer.appendChild(addButton);
|
||
buttonContainer.appendChild(editButton);
|
||
|
||
controlPanel.appendChild(modeContainer);
|
||
controlPanel.appendChild(buttonContainer);
|
||
|
||
// Notes display area
|
||
this.notesContainer = document.createElement('div');
|
||
this.notesContainer.style.cssText = `
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding-right: 5px;
|
||
`;
|
||
|
||
this.sidebar.appendChild(controlPanel);
|
||
this.sidebar.appendChild(this.notesContainer);
|
||
|
||
document.body.appendChild(this.sidebar);
|
||
}
|
||
|
||
setupEventListeners() {
|
||
// Use polling to check for selection changes (more reliable)
|
||
this.lastSelectionCheck = null;
|
||
setInterval(() => {
|
||
if (app.canvas && app.canvas.selected_nodes) {
|
||
const selectedNodeIds = Object.keys(app.canvas.selected_nodes);
|
||
const currentSelection = selectedNodeIds.join(',');
|
||
|
||
// Only update if selection changed
|
||
if (currentSelection !== this.lastSelectionCheck) {
|
||
this.lastSelectionCheck = currentSelection;
|
||
this.updateSelectedNode();
|
||
}
|
||
}
|
||
}, 300); // Check every 300ms
|
||
|
||
// Listen for workflow changes
|
||
api.addEventListener("graphChanged", () => {
|
||
this.updateWorkflowName();
|
||
});
|
||
}
|
||
|
||
updateSelectedNode() {
|
||
// Try multiple methods to get selected nodes
|
||
let selectedNodes = app.canvas?.selected_nodes;
|
||
|
||
// Alternative: check graph canvas
|
||
if (!selectedNodes && app.graph?.list_of_graphcanvas?.[0]) {
|
||
selectedNodes = app.graph.list_of_graphcanvas[0].selected_nodes;
|
||
}
|
||
|
||
let currentNodeType = null;
|
||
|
||
// Check if it's an object with keys (not a Set)
|
||
const selectedNodeIds = selectedNodes ? Object.keys(selectedNodes) : [];
|
||
|
||
if (selectedNodeIds.length > 0) {
|
||
// Get the first selected node
|
||
const nodeId = selectedNodeIds[0];
|
||
const node = selectedNodes[nodeId];
|
||
|
||
if (node && node.type) {
|
||
currentNodeType = node.type;
|
||
this.selectedNodeType = node.type;
|
||
|
||
// Collect node attributes (widgets/inputs)
|
||
this.selectedNodeAttributes = {};
|
||
if (node.widgets) {
|
||
for (const widget of node.widgets) {
|
||
this.selectedNodeAttributes[widget.name] = widget.value;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
this.selectedNodeType = null;
|
||
this.selectedNodeAttributes = {};
|
||
}
|
||
|
||
// Only refresh if selection changed and in automatic mode
|
||
if (this.displayMode === "automatic" && currentNodeType !== this.lastSelectedNodeType) {
|
||
this.lastSelectedNodeType = currentNodeType;
|
||
this.refreshNotes();
|
||
}
|
||
}
|
||
|
||
updateWorkflowName() {
|
||
// Try to get workflow name from various sources
|
||
this.workflowName = app.ui.lastQueueSize || document.title || "";
|
||
|
||
if (this.displayMode === "automatic") {
|
||
this.refreshNotes();
|
||
}
|
||
}
|
||
|
||
async refreshNotes() {
|
||
try {
|
||
const params = new URLSearchParams({
|
||
mode: this.displayMode
|
||
});
|
||
|
||
if (this.selectedNodeType) {
|
||
params.append('node_type', this.selectedNodeType);
|
||
}
|
||
|
||
if (Object.keys(this.selectedNodeAttributes).length > 0) {
|
||
params.append('node_attributes', JSON.stringify(this.selectedNodeAttributes));
|
||
}
|
||
|
||
if (this.workflowName) {
|
||
params.append('workflow_name', this.workflowName);
|
||
}
|
||
|
||
// Collect workflow node information for node_in_workflow and attribute_in_workflow triggers
|
||
const workflowNodes = {};
|
||
if (app.graph && app.graph._nodes) {
|
||
for (const node of app.graph._nodes) {
|
||
// For each node type in workflow, collect first instance's attributes
|
||
if (!workflowNodes[node.type]) {
|
||
const attributes = {};
|
||
if (node.properties) {
|
||
Object.assign(attributes, node.properties);
|
||
}
|
||
if (node.widgets) {
|
||
for (const widget of node.widgets) {
|
||
attributes[widget.name] = widget.value;
|
||
}
|
||
}
|
||
workflowNodes[node.type] = attributes;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (Object.keys(workflowNodes).length > 0) {
|
||
params.append('workflow_nodes', JSON.stringify(workflowNodes));
|
||
}
|
||
|
||
const response = await api.fetchApi(`/autonotes/notes?${params}`);
|
||
this.notes = await response.json();
|
||
this.renderNotes();
|
||
} catch (error) {
|
||
console.error('Failed to fetch notes:', error);
|
||
}
|
||
}
|
||
|
||
renderNotes() {
|
||
this.notesContainer.innerHTML = '';
|
||
|
||
if (this.notes.length === 0) {
|
||
const emptyMessage = document.createElement('div');
|
||
emptyMessage.textContent = 'No notes to display';
|
||
emptyMessage.style.cssText = `
|
||
text-align: center;
|
||
color: #888;
|
||
font-style: italic;
|
||
margin-top: 20px;
|
||
`;
|
||
this.notesContainer.appendChild(emptyMessage);
|
||
return;
|
||
}
|
||
|
||
for (const note of this.notes) {
|
||
this.renderNote(note);
|
||
}
|
||
}
|
||
|
||
renderNote(note) {
|
||
const noteElement = document.createElement('div');
|
||
noteElement.style.cssText = `
|
||
margin-bottom: 15px;
|
||
border: 1px solid #555;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
`;
|
||
|
||
// Note header (collapsible)
|
||
const header = document.createElement('div');
|
||
header.style.cssText = `
|
||
background: #333;
|
||
padding: 10px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
`;
|
||
|
||
const title = document.createElement('span');
|
||
title.textContent = note.name || 'Untitled Note';
|
||
title.style.fontWeight = 'bold';
|
||
|
||
const controls = document.createElement('div');
|
||
controls.style.display = 'flex';
|
||
controls.style.gap = '5px';
|
||
controls.style.alignItems = 'center';
|
||
|
||
// Pin button
|
||
const pinButton = document.createElement('button');
|
||
pinButton.textContent = note.pinned ? '📌' : '📍';
|
||
pinButton.title = note.pinned ? 'Unpin' : 'Pin';
|
||
pinButton.style.cssText = `
|
||
background: transparent;
|
||
border: none;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
font-size: 16px;
|
||
opacity: ${note.pinned ? '1' : '0.5'};
|
||
`;
|
||
pinButton.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
await this.togglePin(note);
|
||
});
|
||
controls.appendChild(pinButton);
|
||
|
||
// Collapse indicator
|
||
const collapseIcon = document.createElement('span');
|
||
collapseIcon.textContent = '▼';
|
||
collapseIcon.style.transition = 'transform 0.2s';
|
||
controls.appendChild(collapseIcon);
|
||
|
||
header.appendChild(title);
|
||
header.appendChild(controls);
|
||
|
||
// Content container
|
||
const contentContainer = document.createElement('div');
|
||
contentContainer.style.cssText = `
|
||
background: #2a2a2a;
|
||
border-top: 1px solid #555;
|
||
`;
|
||
|
||
// Mode toggle (for markdown notes)
|
||
let isEditMode = false;
|
||
let modeToggle = null;
|
||
|
||
if (note.format_style === 'markdown') {
|
||
const toggleBar = document.createElement('div');
|
||
toggleBar.style.cssText = `
|
||
padding: 5px 10px;
|
||
background: #252525;
|
||
border-bottom: 1px solid #555;
|
||
display: flex;
|
||
gap: 5px;
|
||
`;
|
||
|
||
const viewBtn = document.createElement('button');
|
||
viewBtn.textContent = 'View';
|
||
viewBtn.style.cssText = `
|
||
background: #007acc;
|
||
color: white;
|
||
border: none;
|
||
padding: 3px 10px;
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
font-size: 11px;
|
||
`;
|
||
|
||
const editBtn = document.createElement('button');
|
||
editBtn.textContent = 'Edit';
|
||
editBtn.style.cssText = `
|
||
background: #555;
|
||
color: white;
|
||
border: none;
|
||
padding: 3px 10px;
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
font-size: 11px;
|
||
`;
|
||
|
||
toggleBar.appendChild(viewBtn);
|
||
toggleBar.appendChild(editBtn);
|
||
contentContainer.appendChild(toggleBar);
|
||
|
||
modeToggle = { viewBtn, editBtn };
|
||
}
|
||
|
||
// View content (rendered markdown or plain text)
|
||
const viewContent = document.createElement('div');
|
||
viewContent.style.cssText = `
|
||
padding: 10px;
|
||
`;
|
||
|
||
if (note.format_style === 'markdown') {
|
||
viewContent.innerHTML = this.renderMarkdown(note.content);
|
||
} else {
|
||
viewContent.textContent = note.content;
|
||
viewContent.style.whiteSpace = 'pre-wrap';
|
||
}
|
||
|
||
// Edit content (textarea)
|
||
const editContent = document.createElement('textarea');
|
||
editContent.value = note.content;
|
||
editContent.style.cssText = `
|
||
width: 100%;
|
||
min-height: 100px;
|
||
padding: 10px;
|
||
background: #1a1a1a;
|
||
color: #fff;
|
||
border: none;
|
||
font-family: monospace;
|
||
font-size: 12px;
|
||
resize: vertical;
|
||
box-sizing: border-box;
|
||
display: none;
|
||
`;
|
||
|
||
// Save button
|
||
const saveBtn = document.createElement('button');
|
||
saveBtn.textContent = 'Save';
|
||
saveBtn.style.cssText = `
|
||
margin: 5px 10px 10px 10px;
|
||
background: #007acc;
|
||
color: white;
|
||
border: none;
|
||
padding: 5px 15px;
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
display: none;
|
||
`;
|
||
|
||
saveBtn.addEventListener('click', async () => {
|
||
await this.updateNoteContent(note.uuid, editContent.value);
|
||
});
|
||
|
||
contentContainer.appendChild(viewContent);
|
||
contentContainer.appendChild(editContent);
|
||
contentContainer.appendChild(saveBtn);
|
||
|
||
// Toggle between view and edit mode
|
||
if (modeToggle) {
|
||
const switchToView = () => {
|
||
isEditMode = false;
|
||
viewContent.style.display = 'block';
|
||
editContent.style.display = 'none';
|
||
saveBtn.style.display = 'none';
|
||
modeToggle.viewBtn.style.background = '#007acc';
|
||
modeToggle.editBtn.style.background = '#555';
|
||
};
|
||
|
||
const switchToEdit = () => {
|
||
isEditMode = true;
|
||
viewContent.style.display = 'none';
|
||
editContent.style.display = 'block';
|
||
saveBtn.style.display = 'block';
|
||
modeToggle.viewBtn.style.background = '#555';
|
||
modeToggle.editBtn.style.background = '#007acc';
|
||
};
|
||
|
||
modeToggle.viewBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
switchToView();
|
||
});
|
||
|
||
modeToggle.editBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
switchToEdit();
|
||
});
|
||
} else {
|
||
// For plain text, click to edit directly
|
||
viewContent.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
viewContent.style.display = 'none';
|
||
editContent.style.display = 'block';
|
||
saveBtn.style.display = 'block';
|
||
editContent.focus();
|
||
});
|
||
}
|
||
|
||
// Collapsible functionality
|
||
let isCollapsed = false;
|
||
header.addEventListener('click', () => {
|
||
isCollapsed = !isCollapsed;
|
||
contentContainer.style.display = isCollapsed ? 'none' : 'block';
|
||
collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
|
||
});
|
||
|
||
noteElement.appendChild(header);
|
||
noteElement.appendChild(contentContainer);
|
||
this.notesContainer.appendChild(noteElement);
|
||
}
|
||
|
||
renderMarkdown(text) {
|
||
if (!text) return '';
|
||
|
||
// Use the bundled marked library if available
|
||
if (this.marked) {
|
||
try {
|
||
return this.marked.parse(text, {
|
||
breaks: true,
|
||
gfm: true
|
||
});
|
||
} catch (error) {
|
||
console.error('Markdown rendering error:', error);
|
||
}
|
||
}
|
||
|
||
// Fallback to plain text with line breaks
|
||
return text.replace(/\n/g, '<br>');
|
||
}
|
||
|
||
async addNote() {
|
||
// Load folders first if not already loaded
|
||
await this.loadFolders();
|
||
|
||
// Show custom dialog to get note name and folder
|
||
const result = await this.showAddNoteDialog();
|
||
if (!result) return;
|
||
|
||
try {
|
||
const response = await api.fetchApi('/autonotes/notes', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
name: result.name,
|
||
folder_uuid: result.folder_uuid
|
||
}),
|
||
});
|
||
|
||
const noteResult = await response.json();
|
||
if (noteResult.uuid) {
|
||
await this.refreshNotes();
|
||
await this.openEditDialog(noteResult.uuid);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to create note:', error);
|
||
alert('Failed to create note');
|
||
}
|
||
}
|
||
|
||
async showAddNoteDialog() {
|
||
return new Promise((resolve) => {
|
||
// Create overlay
|
||
const overlay = document.createElement('div');
|
||
overlay.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
z-index: 3000;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
`;
|
||
|
||
// Create dialog
|
||
const dialog = document.createElement('div');
|
||
dialog.style.cssText = `
|
||
background: #2a2a2a;
|
||
border: 1px solid #555;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
width: 400px;
|
||
color: #fff;
|
||
font-family: Arial, sans-serif;
|
||
`;
|
||
|
||
// Title
|
||
const title = document.createElement('h2');
|
||
title.textContent = 'Create New Note';
|
||
title.style.cssText = `
|
||
margin: 0 0 20px 0;
|
||
font-size: 1.3em;
|
||
`;
|
||
|
||
// Name input
|
||
const nameLabel = document.createElement('label');
|
||
nameLabel.textContent = 'Note Name:';
|
||
nameLabel.style.cssText = `
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
`;
|
||
|
||
const nameInput = document.createElement('input');
|
||
nameInput.type = 'text';
|
||
nameInput.placeholder = 'Enter note name...';
|
||
nameInput.style.cssText = `
|
||
width: 100%;
|
||
padding: 8px;
|
||
background: #1a1a1a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 4px;
|
||
box-sizing: border-box;
|
||
margin-bottom: 20px;
|
||
font-size: 14px;
|
||
`;
|
||
|
||
// Folder selection
|
||
const folderLabel = document.createElement('label');
|
||
folderLabel.textContent = 'Folder:';
|
||
folderLabel.style.cssText = `
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
`;
|
||
|
||
const folderTreeContainer = document.createElement('div');
|
||
folderTreeContainer.style.cssText = `
|
||
width: 100%;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
background: #1a1a1a;
|
||
border: 1px solid #555;
|
||
border-radius: 4px;
|
||
margin-bottom: 20px;
|
||
padding: 8px;
|
||
`;
|
||
|
||
let selectedFolderUuid = null;
|
||
|
||
// Root option
|
||
const rootItem = document.createElement('div');
|
||
rootItem.style.cssText = `
|
||
padding: 5px 8px;
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
background: #007acc;
|
||
margin-bottom: 5px;
|
||
`;
|
||
rootItem.textContent = '📁 [Root]';
|
||
rootItem.dataset.folderUuid = '';
|
||
|
||
rootItem.addEventListener('click', () => {
|
||
// Remove selection from all items
|
||
folderTreeContainer.querySelectorAll('div[data-folder-uuid]').forEach(item => {
|
||
item.style.background = '';
|
||
});
|
||
rootItem.style.background = '#007acc';
|
||
selectedFolderUuid = null;
|
||
});
|
||
|
||
folderTreeContainer.appendChild(rootItem);
|
||
|
||
// Build folder tree recursively
|
||
// level 0 = [Root]
|
||
// level 1 = folders with parent_uuid = null (direct children of root)
|
||
// level 2+ = nested folders
|
||
const buildFolderTree = (parentUuid, level = 1) => {
|
||
// Filter folders by parent - handle both null and undefined
|
||
const childFolders = this.folders.filter(f => {
|
||
const folderParent = f.parent_uuid || null;
|
||
const searchParent = parentUuid || null;
|
||
return folderParent === searchParent;
|
||
});
|
||
|
||
for (const folder of childFolders) {
|
||
const folderItem = document.createElement('div');
|
||
const indent = 8 + (level * 20);
|
||
folderItem.style.cssText = `
|
||
padding: 5px 8px;
|
||
padding-left: ${indent}px;
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
margin-bottom: 2px;
|
||
`;
|
||
folderItem.textContent = `${' '.repeat(level)}📁 ${folder.name}`;
|
||
folderItem.dataset.folderUuid = folder.uuid;
|
||
|
||
folderItem.addEventListener('click', () => {
|
||
// Remove selection from all items
|
||
folderTreeContainer.querySelectorAll('div[data-folder-uuid]').forEach(item => {
|
||
item.style.background = '';
|
||
});
|
||
folderItem.style.background = '#007acc';
|
||
selectedFolderUuid = folder.uuid;
|
||
});
|
||
|
||
folderTreeContainer.appendChild(folderItem);
|
||
|
||
// Recursively build child folders
|
||
buildFolderTree(folder.uuid, level + 1);
|
||
}
|
||
};
|
||
|
||
// Start building from root (null parent) at level 1
|
||
// Level 0 is [Root] itself, so children start at level 1
|
||
buildFolderTree(null, 1);
|
||
|
||
// Buttons
|
||
const buttonContainer = document.createElement('div');
|
||
buttonContainer.style.cssText = `
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
`;
|
||
|
||
const cancelBtn = document.createElement('button');
|
||
cancelBtn.textContent = 'Cancel';
|
||
cancelBtn.style.cssText = `
|
||
background: #666;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 20px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
`;
|
||
|
||
const createBtn = document.createElement('button');
|
||
createBtn.textContent = 'Create';
|
||
createBtn.style.cssText = `
|
||
background: #007acc;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 20px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
`;
|
||
|
||
// Event handlers
|
||
const closeDialog = (result) => {
|
||
overlay.remove();
|
||
resolve(result);
|
||
};
|
||
|
||
cancelBtn.addEventListener('click', () => closeDialog(null));
|
||
|
||
createBtn.addEventListener('click', () => {
|
||
const name = nameInput.value.trim();
|
||
if (!name) {
|
||
alert('Please enter a note name');
|
||
return;
|
||
}
|
||
|
||
closeDialog({
|
||
name: name,
|
||
folder_uuid: selectedFolderUuid
|
||
});
|
||
});
|
||
|
||
// Allow Enter key to create
|
||
nameInput.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
createBtn.click();
|
||
}
|
||
});
|
||
|
||
// Focus name input
|
||
setTimeout(() => nameInput.focus(), 0);
|
||
|
||
// Assemble dialog
|
||
buttonContainer.appendChild(cancelBtn);
|
||
buttonContainer.appendChild(createBtn);
|
||
|
||
dialog.appendChild(title);
|
||
dialog.appendChild(nameLabel);
|
||
dialog.appendChild(nameInput);
|
||
dialog.appendChild(folderLabel);
|
||
dialog.appendChild(folderTreeContainer);
|
||
dialog.appendChild(buttonContainer);
|
||
|
||
overlay.appendChild(dialog);
|
||
document.body.appendChild(overlay);
|
||
|
||
// Close on overlay click
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) {
|
||
closeDialog(null);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
async openEditDialog(noteUuid = null) {
|
||
if (this.editDialog) {
|
||
this.editDialog.remove();
|
||
}
|
||
|
||
await this.loadFolders();
|
||
// Load all notes for the edit dialog, regardless of display mode
|
||
await this.loadAllNotes();
|
||
this.createEditDialog(noteUuid);
|
||
}
|
||
|
||
async loadAllNotes() {
|
||
try {
|
||
const response = await api.fetchApi('/autonotes/notes?mode=all');
|
||
this.notes = await response.json();
|
||
} catch (error) {
|
||
console.error('Failed to load all notes:', error);
|
||
}
|
||
}
|
||
|
||
async loadFolders() {
|
||
try {
|
||
const response = await api.fetchApi('/autonotes/folders');
|
||
this.folders = await response.json();
|
||
} catch (error) {
|
||
console.error('Failed to load folders:', error);
|
||
this.folders = [];
|
||
}
|
||
}
|
||
|
||
createEditDialog(selectedNoteUuid = null) {
|
||
// Create modal overlay
|
||
const overlay = document.createElement('div');
|
||
overlay.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
z-index: 2000;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
`;
|
||
|
||
// Load saved dialog size
|
||
const savedWidth = localStorage.getItem('autonotes_editDialogWidth') || '800px';
|
||
const savedHeight = localStorage.getItem('autonotes_editDialogHeight') || '600px';
|
||
|
||
// Create dialog
|
||
this.editDialog = document.createElement('div');
|
||
this.editDialog.style.cssText = `
|
||
background: #2a2a2a;
|
||
border: 1px solid #555;
|
||
border-radius: 8px;
|
||
width: ${savedWidth};
|
||
height: ${savedHeight};
|
||
display: flex;
|
||
color: #fff;
|
||
font-family: Arial, sans-serif;
|
||
position: relative;
|
||
`;
|
||
|
||
// Track if we're currently resizing to prevent closing on overlay click
|
||
this.isResizingDialog = false;
|
||
|
||
// Create resize handles
|
||
const createResizeHandle = (position, cursor) => {
|
||
const handle = document.createElement('div');
|
||
handle.style.cssText = `
|
||
position: absolute;
|
||
background: transparent;
|
||
z-index: 10;
|
||
`;
|
||
|
||
if (position === 'right') {
|
||
handle.style.cssText += `
|
||
right: 0;
|
||
top: 0;
|
||
width: 5px;
|
||
height: 100%;
|
||
cursor: ${cursor};
|
||
`;
|
||
} else if (position === 'bottom') {
|
||
handle.style.cssText += `
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 5px;
|
||
cursor: ${cursor};
|
||
`;
|
||
} else if (position === 'corner') {
|
||
handle.style.cssText += `
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 15px;
|
||
height: 15px;
|
||
cursor: ${cursor};
|
||
`;
|
||
}
|
||
|
||
let isResizing = false;
|
||
let startX, startY, startWidth, startHeight;
|
||
|
||
handle.addEventListener('mousedown', (e) => {
|
||
isResizing = true;
|
||
this.isResizingDialog = true;
|
||
startX = e.clientX;
|
||
startY = e.clientY;
|
||
startWidth = this.editDialog.offsetWidth;
|
||
startHeight = this.editDialog.offsetHeight;
|
||
document.body.style.userSelect = 'none';
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
});
|
||
|
||
document.addEventListener('mousemove', (e) => {
|
||
if (!isResizing) return;
|
||
|
||
if (position === 'right' || position === 'corner') {
|
||
const newWidth = startWidth + (e.clientX - startX);
|
||
this.editDialog.style.width = Math.max(400, Math.min(window.innerWidth - 40, newWidth)) + 'px';
|
||
}
|
||
|
||
if (position === 'bottom' || position === 'corner') {
|
||
const newHeight = startHeight + (e.clientY - startY);
|
||
this.editDialog.style.height = Math.max(300, Math.min(window.innerHeight - 40, newHeight)) + 'px';
|
||
}
|
||
});
|
||
|
||
document.addEventListener('mouseup', () => {
|
||
if (isResizing) {
|
||
isResizing = false;
|
||
document.body.style.userSelect = '';
|
||
|
||
// Save size to localStorage
|
||
localStorage.setItem('autonotes_editDialogWidth', this.editDialog.style.width);
|
||
localStorage.setItem('autonotes_editDialogHeight', this.editDialog.style.height);
|
||
|
||
// Delay clearing the flag to prevent overlay click from closing
|
||
setTimeout(() => {
|
||
this.isResizingDialog = false;
|
||
}, 100);
|
||
}
|
||
});
|
||
|
||
return handle;
|
||
};
|
||
|
||
this.editDialog.appendChild(createResizeHandle('right', 'ew-resize'));
|
||
this.editDialog.appendChild(createResizeHandle('bottom', 'ns-resize'));
|
||
this.editDialog.appendChild(createResizeHandle('corner', 'nwse-resize'));
|
||
|
||
// Tree view (left side)
|
||
const treeContainer = document.createElement('div');
|
||
treeContainer.style.cssText = `
|
||
width: 250px;
|
||
border-right: 1px solid #555;
|
||
padding: 10px;
|
||
overflow-y: auto;
|
||
`;
|
||
|
||
const treeHeader = document.createElement('div');
|
||
treeHeader.style.cssText = `
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
`;
|
||
|
||
const treeTitle = document.createElement('h3');
|
||
treeTitle.textContent = 'Notes';
|
||
treeTitle.style.margin = '0';
|
||
|
||
const folderButtons = document.createElement('div');
|
||
folderButtons.style.display = 'flex';
|
||
folderButtons.style.gap = '5px';
|
||
|
||
const addFolderButton = document.createElement('button');
|
||
addFolderButton.textContent = '+';
|
||
addFolderButton.title = 'Add Folder';
|
||
addFolderButton.style.cssText = `
|
||
background: #007acc;
|
||
color: white;
|
||
border: none;
|
||
padding: 2px 8px;
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
font-size: 16px;
|
||
`;
|
||
addFolderButton.addEventListener('click', () => this.addFolder(treeContainer, selectedNoteUuid));
|
||
|
||
folderButtons.appendChild(addFolderButton);
|
||
|
||
treeHeader.appendChild(treeTitle);
|
||
treeHeader.appendChild(folderButtons);
|
||
treeContainer.appendChild(treeHeader);
|
||
|
||
this.renderNoteTree(treeContainer, selectedNoteUuid);
|
||
|
||
// Make tree container a drop target for root
|
||
treeContainer.addEventListener('dragover', (e) => {
|
||
// Check if we're over empty space (not over a folder or note)
|
||
const target = e.target;
|
||
if (target === treeContainer || target === treeHeader || target === treeTitle) {
|
||
e.preventDefault();
|
||
treeContainer.style.background = 'rgba(0, 122, 204, 0.1)';
|
||
}
|
||
});
|
||
|
||
treeContainer.addEventListener('dragleave', (e) => {
|
||
if (e.target === treeContainer) {
|
||
treeContainer.style.background = '';
|
||
}
|
||
});
|
||
|
||
treeContainer.addEventListener('drop', async (e) => {
|
||
// Only handle if dropped on empty space
|
||
const target = e.target;
|
||
if (target === treeContainer || target === treeHeader || target === treeTitle) {
|
||
e.preventDefault();
|
||
treeContainer.style.background = '';
|
||
|
||
const noteUuid = e.dataTransfer.getData('text/plain');
|
||
if (noteUuid) {
|
||
await this.moveNoteToFolder(noteUuid, null, treeContainer, selectedNoteUuid);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Edit panel (right side)
|
||
const editPanel = document.createElement('div');
|
||
editPanel.style.cssText = `
|
||
flex: 1;
|
||
padding: 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
`;
|
||
|
||
this.createEditPanel(editPanel, selectedNoteUuid);
|
||
|
||
this.editDialog.appendChild(treeContainer);
|
||
this.editDialog.appendChild(editPanel);
|
||
|
||
overlay.appendChild(this.editDialog);
|
||
document.body.appendChild(overlay);
|
||
}
|
||
|
||
renderNoteTree(container, selectedNoteUuid) {
|
||
// Clear existing tree items (but not the header)
|
||
const existingItems = container.querySelectorAll('.tree-item, .folder-item');
|
||
existingItems.forEach(item => item.remove());
|
||
|
||
// Render folders first
|
||
for (const folder of this.folders) {
|
||
const folderItem = this.createFolderItem(folder, container, selectedNoteUuid);
|
||
container.appendChild(folderItem);
|
||
}
|
||
|
||
// Render notes (only top-level notes without folders for now)
|
||
for (const note of this.notes) {
|
||
if (!note.folder_uuid) {
|
||
const noteItem = this.createNoteItem(note, selectedNoteUuid, container);
|
||
container.appendChild(noteItem);
|
||
}
|
||
}
|
||
}
|
||
|
||
createFolderItem(folder, treeContainer, selectedNoteUuid) {
|
||
const folderItem = document.createElement('div');
|
||
folderItem.className = 'folder-item';
|
||
folderItem.style.cssText = `
|
||
margin-bottom: 5px;
|
||
`;
|
||
|
||
const folderHeader = document.createElement('div');
|
||
folderHeader.style.cssText = `
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 5px;
|
||
border-radius: 3px;
|
||
background: #333;
|
||
cursor: pointer;
|
||
`;
|
||
|
||
// Collapse/expand icon
|
||
const expandIcon = document.createElement('span');
|
||
expandIcon.textContent = '▶';
|
||
expandIcon.style.cssText = `
|
||
transition: transform 0.2s;
|
||
font-size: 10px;
|
||
`;
|
||
|
||
// Make folder a drop target
|
||
folderHeader.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
folderHeader.style.background = '#007acc';
|
||
});
|
||
|
||
folderHeader.addEventListener('dragleave', (e) => {
|
||
folderHeader.style.background = '#333';
|
||
});
|
||
|
||
folderHeader.addEventListener('drop', async (e) => {
|
||
e.preventDefault();
|
||
folderHeader.style.background = '#333';
|
||
|
||
const noteUuid = e.dataTransfer.getData('text/plain');
|
||
if (noteUuid) {
|
||
await this.moveNoteToFolder(noteUuid, folder.uuid, treeContainer, selectedNoteUuid);
|
||
}
|
||
});
|
||
|
||
const folderName = document.createElement('span');
|
||
folderName.textContent = `📁 ${folder.name}`;
|
||
folderName.style.flex = '1';
|
||
|
||
const renameBtn = document.createElement('button');
|
||
renameBtn.textContent = '✏️';
|
||
renameBtn.title = 'Rename Folder';
|
||
renameBtn.style.cssText = `
|
||
background: transparent;
|
||
border: none;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
padding: 2px;
|
||
font-size: 12px;
|
||
`;
|
||
renameBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
this.renameFolder(folder, treeContainer, selectedNoteUuid);
|
||
});
|
||
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.textContent = '🗑️';
|
||
deleteBtn.title = 'Delete Folder';
|
||
deleteBtn.style.cssText = `
|
||
background: transparent;
|
||
border: none;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
padding: 2px;
|
||
font-size: 12px;
|
||
`;
|
||
deleteBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
this.deleteFolder(folder, treeContainer, selectedNoteUuid);
|
||
});
|
||
|
||
folderHeader.appendChild(expandIcon);
|
||
folderHeader.appendChild(folderName);
|
||
folderHeader.appendChild(renameBtn);
|
||
folderHeader.appendChild(deleteBtn);
|
||
|
||
// Notes container (children)
|
||
const notesContainer = document.createElement('div');
|
||
notesContainer.style.cssText = `
|
||
padding-left: 20px;
|
||
display: none;
|
||
`;
|
||
|
||
// Find notes in this folder
|
||
const folderNotes = this.notes.filter(note => note.folder_uuid === folder.uuid);
|
||
for (const note of folderNotes) {
|
||
const noteItem = this.createNoteItem(note, selectedNoteUuid, treeContainer);
|
||
notesContainer.appendChild(noteItem);
|
||
}
|
||
|
||
// Toggle expand/collapse
|
||
const isExpanded = this.expandedFolders.has(folder.uuid);
|
||
notesContainer.style.display = isExpanded ? 'block' : 'none';
|
||
expandIcon.style.transform = isExpanded ? 'rotate(90deg)' : 'rotate(0deg)';
|
||
|
||
folderHeader.addEventListener('click', (e) => {
|
||
// Don't toggle if clicking buttons
|
||
if (e.target === renameBtn || e.target === deleteBtn) {
|
||
return;
|
||
}
|
||
|
||
if (this.expandedFolders.has(folder.uuid)) {
|
||
this.expandedFolders.delete(folder.uuid);
|
||
notesContainer.style.display = 'none';
|
||
expandIcon.style.transform = 'rotate(0deg)';
|
||
} else {
|
||
this.expandedFolders.add(folder.uuid);
|
||
notesContainer.style.display = 'block';
|
||
expandIcon.style.transform = 'rotate(90deg)';
|
||
}
|
||
});
|
||
|
||
folderItem.appendChild(folderHeader);
|
||
folderItem.appendChild(notesContainer);
|
||
|
||
return folderItem;
|
||
}
|
||
|
||
createNoteItem(note, selectedNoteUuid, treeContainer) {
|
||
const noteItem = document.createElement('div');
|
||
noteItem.className = 'tree-item';
|
||
noteItem.draggable = true;
|
||
noteItem.style.cssText = `
|
||
padding: 5px;
|
||
cursor: move;
|
||
border-radius: 3px;
|
||
margin-bottom: 2px;
|
||
${note.uuid === selectedNoteUuid ? 'background: #007acc;' : ''}
|
||
`;
|
||
noteItem.textContent = note.name || 'Untitled';
|
||
|
||
// Drag start
|
||
noteItem.addEventListener('dragstart', (e) => {
|
||
e.dataTransfer.setData('text/plain', note.uuid);
|
||
noteItem.style.opacity = '0.5';
|
||
});
|
||
|
||
// Drag end
|
||
noteItem.addEventListener('dragend', (e) => {
|
||
noteItem.style.opacity = '1';
|
||
});
|
||
|
||
// Left click to select
|
||
noteItem.addEventListener('click', () => {
|
||
// Remove previous selection
|
||
treeContainer.querySelectorAll('.tree-item').forEach(item => {
|
||
item.style.background = '';
|
||
});
|
||
noteItem.style.background = '#007acc';
|
||
|
||
this.loadNoteIntoEditor(note.uuid);
|
||
});
|
||
|
||
// Right click for context menu
|
||
noteItem.addEventListener('contextmenu', (e) => {
|
||
e.preventDefault();
|
||
this.showNoteContextMenu(e, note, treeContainer, selectedNoteUuid);
|
||
});
|
||
|
||
return noteItem;
|
||
}
|
||
|
||
createEditPanel(panel, selectedNoteUuid) {
|
||
// Content textarea
|
||
const contentContainer = document.createElement('div');
|
||
contentContainer.style.cssText = `
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-bottom: 10px;
|
||
`;
|
||
|
||
const contentLabel = document.createElement('label');
|
||
contentLabel.textContent = 'Content: ';
|
||
contentLabel.style.display = 'block';
|
||
contentLabel.style.marginBottom = '5px';
|
||
|
||
this.contentTextarea = document.createElement('textarea');
|
||
this.contentTextarea.style.cssText = `
|
||
flex: 1;
|
||
padding: 10px;
|
||
background: #333;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
resize: none;
|
||
font-family: monospace;
|
||
`;
|
||
|
||
contentContainer.appendChild(contentLabel);
|
||
contentContainer.appendChild(this.contentTextarea);
|
||
|
||
// Format style
|
||
const formatContainer = document.createElement('div');
|
||
formatContainer.style.cssText = `
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-bottom: 10px;
|
||
align-items: center;
|
||
`;
|
||
|
||
const formatDiv = document.createElement('div');
|
||
const formatLabel = document.createElement('label');
|
||
formatLabel.textContent = 'Format: ';
|
||
formatLabel.style.marginRight = '10px';
|
||
|
||
this.formatSelect = document.createElement('select');
|
||
this.formatSelect.style.cssText = `
|
||
background: #333;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
padding: 5px;
|
||
`;
|
||
this.formatSelect.innerHTML = `
|
||
<option value="plaintext">Plain Text</option>
|
||
<option value="markdown">Markdown</option>
|
||
`;
|
||
|
||
formatDiv.appendChild(formatLabel);
|
||
formatDiv.appendChild(this.formatSelect);
|
||
|
||
// Pinned checkbox
|
||
const pinnedDiv = document.createElement('div');
|
||
this.pinnedCheckbox = document.createElement('input');
|
||
this.pinnedCheckbox.type = 'checkbox';
|
||
this.pinnedCheckbox.id = 'note-pinned';
|
||
|
||
const pinnedLabel = document.createElement('label');
|
||
pinnedLabel.htmlFor = 'note-pinned';
|
||
pinnedLabel.textContent = 'Pinned';
|
||
pinnedLabel.style.marginLeft = '5px';
|
||
|
||
pinnedDiv.appendChild(this.pinnedCheckbox);
|
||
pinnedDiv.appendChild(pinnedLabel);
|
||
|
||
formatContainer.appendChild(formatDiv);
|
||
formatContainer.appendChild(pinnedDiv);
|
||
|
||
// Trigger conditions section
|
||
const triggerContainer = document.createElement('div');
|
||
triggerContainer.style.cssText = `
|
||
margin-bottom: 10px;
|
||
border: 1px solid #555;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
`;
|
||
|
||
const triggerTitle = document.createElement('h4');
|
||
triggerTitle.textContent = 'Trigger Conditions';
|
||
triggerTitle.style.margin = '0 0 10px 0';
|
||
|
||
const addTriggerButton = document.createElement('button');
|
||
addTriggerButton.textContent = 'Add Trigger';
|
||
addTriggerButton.style.cssText = `
|
||
background: #007acc;
|
||
color: white;
|
||
border: none;
|
||
padding: 5px 10px;
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
margin-bottom: 10px;
|
||
`;
|
||
addTriggerButton.addEventListener('click', () => this.addTriggerCondition());
|
||
|
||
this.triggerList = document.createElement('div');
|
||
|
||
triggerContainer.appendChild(triggerTitle);
|
||
triggerContainer.appendChild(addTriggerButton);
|
||
triggerContainer.appendChild(this.triggerList);
|
||
|
||
// Buttons
|
||
const buttonContainer = document.createElement('div');
|
||
buttonContainer.style.cssText = `
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 10px;
|
||
`;
|
||
|
||
const leftButtons = document.createElement('div');
|
||
leftButtons.style.display = 'flex';
|
||
leftButtons.style.gap = '10px';
|
||
|
||
const saveButton = document.createElement('button');
|
||
saveButton.textContent = 'Save';
|
||
saveButton.style.cssText = `
|
||
background: #007acc;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
`;
|
||
saveButton.addEventListener('click', () => this.saveCurrentNote());
|
||
|
||
const revertButton = document.createElement('button');
|
||
revertButton.textContent = 'Revert';
|
||
revertButton.style.cssText = `
|
||
background: #666;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
`;
|
||
|
||
leftButtons.appendChild(saveButton);
|
||
leftButtons.appendChild(revertButton);
|
||
|
||
const rightButtons = document.createElement('div');
|
||
rightButtons.style.display = 'flex';
|
||
rightButtons.style.gap = '10px';
|
||
|
||
const okButton = document.createElement('button');
|
||
okButton.textContent = 'OK';
|
||
okButton.style.cssText = `
|
||
background: #007acc;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
`;
|
||
okButton.addEventListener('click', () => this.closeEditDialog(true));
|
||
|
||
const cancelButton = document.createElement('button');
|
||
cancelButton.textContent = 'Cancel';
|
||
cancelButton.style.cssText = `
|
||
background: #666;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
`;
|
||
cancelButton.addEventListener('click', () => this.closeEditDialog(false));
|
||
|
||
rightButtons.appendChild(okButton);
|
||
rightButtons.appendChild(cancelButton);
|
||
|
||
buttonContainer.appendChild(leftButtons);
|
||
buttonContainer.appendChild(rightButtons);
|
||
|
||
panel.appendChild(contentContainer);
|
||
panel.appendChild(formatContainer);
|
||
panel.appendChild(triggerContainer);
|
||
panel.appendChild(buttonContainer);
|
||
|
||
// Load initial note if specified
|
||
if (selectedNoteUuid) {
|
||
this.loadNoteIntoEditor(selectedNoteUuid);
|
||
}
|
||
}
|
||
|
||
loadNoteIntoEditor(noteUuid) {
|
||
const note = this.notes.find(n => n.uuid === noteUuid);
|
||
if (!note) return;
|
||
|
||
this.currentEditingNote = note;
|
||
this.contentTextarea.value = note.content || '';
|
||
this.formatSelect.value = note.format_style || 'plaintext';
|
||
this.pinnedCheckbox.checked = note.pinned || false;
|
||
|
||
// Load trigger conditions
|
||
this.currentTriggerConditions = [...(note.trigger_conditions || [])];
|
||
this.renderTriggerConditions();
|
||
}
|
||
|
||
addTriggerCondition() {
|
||
const condition = {
|
||
type: 'node_selected',
|
||
node_types: [],
|
||
node_type: null,
|
||
attribute_name: null,
|
||
attribute_values: [],
|
||
workflow_names: []
|
||
};
|
||
|
||
this.currentTriggerConditions = this.currentTriggerConditions || [];
|
||
this.currentTriggerConditions.push(condition);
|
||
this.renderTriggerConditions();
|
||
}
|
||
|
||
renderTriggerConditions() {
|
||
this.triggerList.innerHTML = '';
|
||
|
||
if (!this.currentTriggerConditions) {
|
||
this.currentTriggerConditions = [];
|
||
}
|
||
|
||
this.currentTriggerConditions.forEach((condition, index) => {
|
||
const conditionElement = this.createTriggerConditionElement(condition, index);
|
||
this.triggerList.appendChild(conditionElement);
|
||
});
|
||
}
|
||
|
||
createTriggerConditionElement(condition, index) {
|
||
const container = document.createElement('div');
|
||
container.style.cssText = `
|
||
border: 1px solid #666;
|
||
border-radius: 3px;
|
||
margin-bottom: 10px;
|
||
background: #333;
|
||
`;
|
||
|
||
// Header (collapsible)
|
||
const header = document.createElement('div');
|
||
header.style.cssText = `
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px;
|
||
cursor: pointer;
|
||
background: #3a3a3a;
|
||
border-radius: 3px 3px 0 0;
|
||
`;
|
||
|
||
// Collapse icon
|
||
const collapseIcon = document.createElement('span');
|
||
collapseIcon.textContent = '▼';
|
||
collapseIcon.style.cssText = `
|
||
transition: transform 0.2s;
|
||
margin-right: 10px;
|
||
font-size: 10px;
|
||
`;
|
||
|
||
// Title showing condition type
|
||
const titleSpan = document.createElement('span');
|
||
titleSpan.style.flex = '1';
|
||
const getConditionTitle = () => {
|
||
if (condition.type === 'node_selected') return 'Node Selected';
|
||
if (condition.type === 'node_attribute') return 'Node Attribute';
|
||
if (condition.type === 'node_in_workflow') return 'Node in Workflow';
|
||
if (condition.type === 'attribute_in_workflow') return 'Attribute in Workflow';
|
||
if (condition.type === 'workflow_name') return 'Workflow Name';
|
||
return 'Trigger Condition';
|
||
};
|
||
titleSpan.textContent = getConditionTitle();
|
||
|
||
header.appendChild(collapseIcon);
|
||
header.appendChild(titleSpan);
|
||
|
||
// Content container (collapsible)
|
||
const content = document.createElement('div');
|
||
content.style.cssText = `
|
||
padding: 10px;
|
||
display: block;
|
||
`;
|
||
|
||
// Type selector
|
||
const typeContainer = document.createElement('div');
|
||
typeContainer.style.cssText = `
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 10px;
|
||
`;
|
||
|
||
const typeLabel = document.createElement('label');
|
||
typeLabel.textContent = 'Type: ';
|
||
typeLabel.style.marginRight = '10px';
|
||
|
||
const typeSelect = document.createElement('select');
|
||
typeSelect.style.cssText = `
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
padding: 5px;
|
||
margin-right: 10px;
|
||
`;
|
||
typeSelect.innerHTML = `
|
||
<option value="node_selected">Node Selected</option>
|
||
<option value="node_attribute">Node Attribute</option>
|
||
<option value="node_in_workflow">Node in Workflow</option>
|
||
<option value="attribute_in_workflow">Attribute in Workflow</option>
|
||
<option value="workflow_name">Workflow Name</option>
|
||
`;
|
||
typeSelect.value = condition.type;
|
||
|
||
const removeButton = document.createElement('button');
|
||
removeButton.textContent = 'Remove';
|
||
removeButton.style.cssText = `
|
||
background: #cc0000;
|
||
color: white;
|
||
border: none;
|
||
padding: 5px 10px;
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
margin-left: auto;
|
||
`;
|
||
removeButton.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
this.currentTriggerConditions.splice(index, 1);
|
||
this.renderTriggerConditions();
|
||
});
|
||
|
||
typeContainer.appendChild(typeLabel);
|
||
typeContainer.appendChild(typeSelect);
|
||
typeContainer.appendChild(removeButton);
|
||
|
||
// Fields container for type-specific fields
|
||
const fieldsContainer = document.createElement('div');
|
||
|
||
const updateContent = () => {
|
||
condition.type = typeSelect.value;
|
||
titleSpan.textContent = getConditionTitle();
|
||
fieldsContainer.innerHTML = '';
|
||
|
||
if (condition.type === 'node_selected') {
|
||
this.createNodeSelectedFields(fieldsContainer, condition);
|
||
} else if (condition.type === 'node_attribute') {
|
||
this.createNodeAttributeFields(fieldsContainer, condition);
|
||
} else if (condition.type === 'node_in_workflow') {
|
||
this.createNodeInWorkflowFields(fieldsContainer, condition);
|
||
} else if (condition.type === 'attribute_in_workflow') {
|
||
this.createAttributeInWorkflowFields(fieldsContainer, condition);
|
||
} else if (condition.type === 'workflow_name') {
|
||
this.createWorkflowNameFields(fieldsContainer, condition);
|
||
}
|
||
};
|
||
|
||
typeSelect.addEventListener('change', updateContent);
|
||
updateContent();
|
||
|
||
content.appendChild(typeContainer);
|
||
content.appendChild(fieldsContainer);
|
||
|
||
// Collapse/expand functionality
|
||
let isCollapsed = false;
|
||
header.addEventListener('click', (e) => {
|
||
// Don't collapse if clicking on buttons/inputs
|
||
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'SELECT') {
|
||
return;
|
||
}
|
||
|
||
isCollapsed = !isCollapsed;
|
||
content.style.display = isCollapsed ? 'none' : 'block';
|
||
collapseIcon.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
|
||
});
|
||
|
||
container.appendChild(header);
|
||
container.appendChild(content);
|
||
|
||
return container;
|
||
}
|
||
|
||
createNodeSelectedFields(container, condition) {
|
||
const label = document.createElement('label');
|
||
label.textContent = 'Node Types: ';
|
||
label.style.display = 'block';
|
||
label.style.marginBottom = '5px';
|
||
|
||
// Multi-select dropdown with searchable autocomplete
|
||
const selectContainer = document.createElement('div');
|
||
selectContainer.style.cssText = `
|
||
position: relative;
|
||
width: 100%;
|
||
`;
|
||
|
||
// Create a searchable input
|
||
const searchInput = document.createElement('input');
|
||
searchInput.type = 'text';
|
||
searchInput.placeholder = 'Search node types...';
|
||
searchInput.style.cssText = `
|
||
width: 100%;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
margin-bottom: 5px;
|
||
`;
|
||
|
||
// Selected items display
|
||
const selectedDisplay = document.createElement('div');
|
||
selectedDisplay.style.cssText = `
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 5px;
|
||
margin-bottom: 10px;
|
||
min-height: 30px;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
`;
|
||
|
||
// Dropdown list
|
||
const dropdownList = document.createElement('div');
|
||
dropdownList.style.cssText = `
|
||
position: absolute;
|
||
width: 100%;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
background: #2a2a2a;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
z-index: 1000;
|
||
display: none;
|
||
`;
|
||
|
||
// Get node types from workflow
|
||
const workflowNodeTypes = new Set();
|
||
if (app.graph && app.graph._nodes) {
|
||
for (const node of app.graph._nodes) {
|
||
workflowNodeTypes.add(node.type);
|
||
}
|
||
}
|
||
|
||
// Populate dropdown with all node types
|
||
const nodeTypesList = Object.keys(this.nodeTypes).sort();
|
||
|
||
const updateSelectedDisplay = () => {
|
||
selectedDisplay.innerHTML = '';
|
||
(condition.node_types || []).forEach(nodeType => {
|
||
const tag = document.createElement('span');
|
||
tag.style.cssText = `
|
||
background: #007acc;
|
||
color: #fff;
|
||
padding: 3px 8px;
|
||
border-radius: 3px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
`;
|
||
|
||
// Add icon if in workflow
|
||
if (workflowNodeTypes.has(nodeType)) {
|
||
const icon = document.createElement('span');
|
||
icon.textContent = '✓';
|
||
icon.style.color = '#0f0';
|
||
tag.appendChild(icon);
|
||
}
|
||
|
||
const nameSpan = document.createElement('span');
|
||
nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType;
|
||
tag.appendChild(nameSpan);
|
||
|
||
const removeBtn = document.createElement('span');
|
||
removeBtn.textContent = '×';
|
||
removeBtn.style.cursor = 'pointer';
|
||
removeBtn.style.fontWeight = 'bold';
|
||
removeBtn.addEventListener('click', () => {
|
||
condition.node_types = condition.node_types.filter(t => t !== nodeType);
|
||
updateSelectedDisplay();
|
||
});
|
||
tag.appendChild(removeBtn);
|
||
|
||
selectedDisplay.appendChild(tag);
|
||
});
|
||
};
|
||
|
||
const filterDropdown = (searchTerm) => {
|
||
dropdownList.innerHTML = '';
|
||
const filtered = nodeTypesList.filter(nodeType => {
|
||
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
|
||
return displayName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
nodeType.toLowerCase().includes(searchTerm.toLowerCase());
|
||
});
|
||
|
||
filtered.forEach(nodeType => {
|
||
const option = document.createElement('div');
|
||
option.style.cssText = `
|
||
padding: 8px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
`;
|
||
|
||
// Add checkmark if in workflow
|
||
if (workflowNodeTypes.has(nodeType)) {
|
||
const icon = document.createElement('span');
|
||
icon.textContent = '✓';
|
||
icon.style.color = '#0f0';
|
||
icon.style.fontWeight = 'bold';
|
||
option.appendChild(icon);
|
||
}
|
||
|
||
const nameSpan = document.createElement('span');
|
||
nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType;
|
||
option.appendChild(nameSpan);
|
||
|
||
option.addEventListener('mouseenter', () => {
|
||
option.style.background = '#007acc';
|
||
});
|
||
option.addEventListener('mouseleave', () => {
|
||
option.style.background = '';
|
||
});
|
||
option.addEventListener('click', () => {
|
||
if (!condition.node_types) condition.node_types = [];
|
||
if (!condition.node_types.includes(nodeType)) {
|
||
condition.node_types.push(nodeType);
|
||
updateSelectedDisplay();
|
||
}
|
||
searchInput.value = '';
|
||
dropdownList.style.display = 'none';
|
||
});
|
||
|
||
dropdownList.appendChild(option);
|
||
});
|
||
|
||
dropdownList.style.display = filtered.length > 0 ? 'block' : 'none';
|
||
};
|
||
|
||
searchInput.addEventListener('focus', () => {
|
||
filterDropdown(searchInput.value);
|
||
});
|
||
|
||
searchInput.addEventListener('input', () => {
|
||
filterDropdown(searchInput.value);
|
||
});
|
||
|
||
// Close dropdown when clicking outside
|
||
document.addEventListener('click', (e) => {
|
||
if (!selectContainer.contains(e.target)) {
|
||
dropdownList.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
selectContainer.appendChild(searchInput);
|
||
selectContainer.appendChild(dropdownList);
|
||
|
||
container.appendChild(label);
|
||
container.appendChild(selectedDisplay);
|
||
container.appendChild(selectContainer);
|
||
|
||
// Initialize display
|
||
updateSelectedDisplay();
|
||
}
|
||
|
||
createNodeAttributeFields(container, condition) {
|
||
// Node type searchable dropdown
|
||
const nodeTypeContainer = document.createElement('div');
|
||
nodeTypeContainer.style.marginBottom = '10px';
|
||
|
||
const nodeTypeLabel = document.createElement('label');
|
||
nodeTypeLabel.textContent = 'Node Type: ';
|
||
nodeTypeLabel.style.display = 'block';
|
||
nodeTypeLabel.style.marginBottom = '5px';
|
||
|
||
// Get node types from workflow
|
||
const workflowNodeTypes = new Set();
|
||
if (app.graph && app.graph._nodes) {
|
||
for (const node of app.graph._nodes) {
|
||
workflowNodeTypes.add(node.type);
|
||
}
|
||
}
|
||
|
||
const selectContainer = document.createElement('div');
|
||
selectContainer.style.cssText = `
|
||
position: relative;
|
||
width: 100%;
|
||
`;
|
||
|
||
// Display selected value
|
||
const selectedDisplay = document.createElement('div');
|
||
selectedDisplay.style.cssText = `
|
||
width: 100%;
|
||
padding: 8px;
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
`;
|
||
|
||
const updateSelectedDisplay = () => {
|
||
selectedDisplay.innerHTML = '';
|
||
if (condition.node_type) {
|
||
if (workflowNodeTypes.has(condition.node_type)) {
|
||
const icon = document.createElement('span');
|
||
icon.textContent = '✓';
|
||
icon.style.color = '#0f0';
|
||
icon.style.fontWeight = 'bold';
|
||
selectedDisplay.appendChild(icon);
|
||
}
|
||
const text = document.createElement('span');
|
||
text.textContent = this.nodeTypes[condition.node_type]?.display_name || condition.node_type;
|
||
selectedDisplay.appendChild(text);
|
||
} else {
|
||
const text = document.createElement('span');
|
||
text.textContent = '-- Select Node Type --';
|
||
text.style.color = '#888';
|
||
selectedDisplay.appendChild(text);
|
||
}
|
||
};
|
||
|
||
// Search input
|
||
const searchInput = document.createElement('input');
|
||
searchInput.type = 'text';
|
||
searchInput.placeholder = 'Search node types...';
|
||
searchInput.style.cssText = `
|
||
width: 100%;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
display: none;
|
||
`;
|
||
|
||
// Dropdown list
|
||
const dropdownList = document.createElement('div');
|
||
dropdownList.style.cssText = `
|
||
position: absolute;
|
||
width: 100%;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
background: #2a2a2a;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
z-index: 1000;
|
||
display: none;
|
||
margin-top: 2px;
|
||
`;
|
||
|
||
const nodeTypesList = Object.keys(this.nodeTypes).sort();
|
||
|
||
const filterDropdown = (searchTerm) => {
|
||
dropdownList.innerHTML = '';
|
||
const filtered = nodeTypesList.filter(nodeType => {
|
||
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
|
||
return displayName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
nodeType.toLowerCase().includes(searchTerm.toLowerCase());
|
||
});
|
||
|
||
filtered.forEach(nodeType => {
|
||
const option = document.createElement('div');
|
||
option.style.cssText = `
|
||
padding: 8px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
`;
|
||
|
||
if (workflowNodeTypes.has(nodeType)) {
|
||
const icon = document.createElement('span');
|
||
icon.textContent = '✓';
|
||
icon.style.color = '#0f0';
|
||
icon.style.fontWeight = 'bold';
|
||
option.appendChild(icon);
|
||
}
|
||
|
||
const nameSpan = document.createElement('span');
|
||
nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType;
|
||
option.appendChild(nameSpan);
|
||
|
||
option.addEventListener('mouseenter', () => {
|
||
option.style.background = '#007acc';
|
||
});
|
||
option.addEventListener('mouseleave', () => {
|
||
option.style.background = '';
|
||
});
|
||
option.addEventListener('click', () => {
|
||
condition.node_type = nodeType;
|
||
updateSelectedDisplay();
|
||
updateAttributeOptions(nodeType);
|
||
dropdownList.style.display = 'none';
|
||
searchInput.style.display = 'none';
|
||
selectedDisplay.style.display = 'flex';
|
||
});
|
||
|
||
dropdownList.appendChild(option);
|
||
});
|
||
|
||
dropdownList.style.display = filtered.length > 0 ? 'block' : 'none';
|
||
};
|
||
|
||
selectedDisplay.addEventListener('click', () => {
|
||
selectedDisplay.style.display = 'none';
|
||
searchInput.style.display = 'block';
|
||
searchInput.focus();
|
||
filterDropdown('');
|
||
});
|
||
|
||
searchInput.addEventListener('input', () => {
|
||
filterDropdown(searchInput.value);
|
||
});
|
||
|
||
searchInput.addEventListener('blur', () => {
|
||
setTimeout(() => {
|
||
searchInput.style.display = 'none';
|
||
selectedDisplay.style.display = 'flex';
|
||
dropdownList.style.display = 'none';
|
||
}, 200);
|
||
});
|
||
|
||
selectContainer.appendChild(selectedDisplay);
|
||
selectContainer.appendChild(searchInput);
|
||
selectContainer.appendChild(dropdownList);
|
||
|
||
nodeTypeContainer.appendChild(nodeTypeLabel);
|
||
nodeTypeContainer.appendChild(selectContainer);
|
||
|
||
// Attribute name dropdown (populated based on selected node)
|
||
const attrNameContainer = document.createElement('div');
|
||
attrNameContainer.style.marginBottom = '10px';
|
||
|
||
const attrNameLabel = document.createElement('label');
|
||
attrNameLabel.textContent = 'Attribute Name: ';
|
||
attrNameLabel.style.display = 'block';
|
||
attrNameLabel.style.marginBottom = '5px';
|
||
|
||
const attrNameSelect = document.createElement('select');
|
||
attrNameSelect.style.cssText = `
|
||
width: 100%;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
`;
|
||
|
||
// Function to update attribute dropdown based on selected node type
|
||
const updateAttributeOptions = (nodeType) => {
|
||
attrNameSelect.innerHTML = '';
|
||
|
||
// Add empty option
|
||
const emptyOption = document.createElement('option');
|
||
emptyOption.value = '';
|
||
emptyOption.textContent = '-- Select Attribute --';
|
||
attrNameSelect.appendChild(emptyOption);
|
||
|
||
if (nodeType && this.nodeTypes[nodeType]) {
|
||
const nodeInfo = this.nodeTypes[nodeType];
|
||
const inputs = nodeInfo.input?.required || {};
|
||
const optionalInputs = nodeInfo.input?.optional || {};
|
||
|
||
// Add required inputs
|
||
for (const [inputName, inputDef] of Object.entries(inputs)) {
|
||
const option = document.createElement('option');
|
||
option.value = inputName;
|
||
option.textContent = inputName;
|
||
if (condition.attribute_name === inputName) {
|
||
option.selected = true;
|
||
}
|
||
attrNameSelect.appendChild(option);
|
||
}
|
||
|
||
// Add optional inputs
|
||
for (const [inputName, inputDef] of Object.entries(optionalInputs)) {
|
||
const option = document.createElement('option');
|
||
option.value = inputName;
|
||
option.textContent = `${inputName} (optional)`;
|
||
if (condition.attribute_name === inputName) {
|
||
option.selected = true;
|
||
}
|
||
attrNameSelect.appendChild(option);
|
||
}
|
||
}
|
||
};
|
||
|
||
attrNameSelect.addEventListener('change', () => {
|
||
condition.attribute_name = attrNameSelect.value;
|
||
});
|
||
|
||
// Initialize displays
|
||
updateSelectedDisplay();
|
||
updateAttributeOptions(condition.node_type);
|
||
|
||
attrNameContainer.appendChild(attrNameLabel);
|
||
attrNameContainer.appendChild(attrNameSelect);
|
||
|
||
// Attribute values
|
||
const attrValuesContainer = document.createElement('div');
|
||
attrValuesContainer.style.marginBottom = '10px';
|
||
|
||
const attrValuesLabel = document.createElement('label');
|
||
attrValuesLabel.textContent = 'Attribute Values (comma-separated): ';
|
||
attrValuesLabel.style.display = 'block';
|
||
attrValuesLabel.style.marginBottom = '5px';
|
||
|
||
const attrValuesInput = document.createElement('input');
|
||
attrValuesInput.type = 'text';
|
||
attrValuesInput.value = (condition.attribute_values || []).join(', ');
|
||
attrValuesInput.placeholder = 'e.g., model1.safetensors, model2.ckpt';
|
||
attrValuesInput.style.cssText = `
|
||
width: 100%;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
`;
|
||
|
||
attrValuesInput.addEventListener('input', () => {
|
||
condition.attribute_values = attrValuesInput.value.split(',').map(s => s.trim()).filter(s => s);
|
||
});
|
||
|
||
attrValuesContainer.appendChild(attrValuesLabel);
|
||
attrValuesContainer.appendChild(attrValuesInput);
|
||
|
||
container.appendChild(nodeTypeContainer);
|
||
container.appendChild(attrNameContainer);
|
||
container.appendChild(attrValuesContainer);
|
||
}
|
||
|
||
createWorkflowNameFields(container, condition) {
|
||
const label = document.createElement('label');
|
||
label.textContent = 'Workflow Names (comma-separated): ';
|
||
label.style.display = 'block';
|
||
label.style.marginBottom = '5px';
|
||
|
||
const input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.value = (condition.workflow_names || []).join(', ');
|
||
input.placeholder = 'e.g., portrait, landscape, character';
|
||
input.style.cssText = `
|
||
width: 100%;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
`;
|
||
|
||
input.addEventListener('input', () => {
|
||
condition.workflow_names = input.value.split(',').map(s => s.trim()).filter(s => s);
|
||
});
|
||
|
||
container.appendChild(label);
|
||
container.appendChild(input);
|
||
}
|
||
|
||
createNodeInWorkflowFields(container, condition) {
|
||
const label = document.createElement('label');
|
||
label.textContent = 'Node Types: ';
|
||
label.style.display = 'block';
|
||
label.style.marginBottom = '5px';
|
||
|
||
// Multi-select dropdown with searchable autocomplete
|
||
const selectContainer = document.createElement('div');
|
||
selectContainer.style.cssText = `
|
||
position: relative;
|
||
width: 100%;
|
||
`;
|
||
|
||
// Create a searchable input
|
||
const searchInput = document.createElement('input');
|
||
searchInput.type = 'text';
|
||
searchInput.placeholder = 'Search node types...';
|
||
searchInput.style.cssText = `
|
||
width: 100%;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
margin-bottom: 5px;
|
||
`;
|
||
|
||
// Selected items display
|
||
const selectedDisplay = document.createElement('div');
|
||
selectedDisplay.style.cssText = `
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 5px;
|
||
margin-bottom: 10px;
|
||
min-height: 30px;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
`;
|
||
|
||
// Dropdown list
|
||
const dropdownList = document.createElement('div');
|
||
dropdownList.style.cssText = `
|
||
position: absolute;
|
||
width: 100%;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
background: #2a2a2a;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
z-index: 1000;
|
||
display: none;
|
||
`;
|
||
|
||
// Get node types from workflow
|
||
const workflowNodeTypes = new Set();
|
||
if (app.graph && app.graph._nodes) {
|
||
for (const node of app.graph._nodes) {
|
||
workflowNodeTypes.add(node.type);
|
||
}
|
||
}
|
||
|
||
// Populate dropdown with all node types
|
||
const nodeTypesList = Object.keys(this.nodeTypes).sort();
|
||
|
||
const updateSelectedDisplay = () => {
|
||
selectedDisplay.innerHTML = '';
|
||
(condition.node_types || []).forEach(nodeType => {
|
||
const tag = document.createElement('span');
|
||
tag.style.cssText = `
|
||
background: #007acc;
|
||
color: #fff;
|
||
padding: 3px 8px;
|
||
border-radius: 3px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
`;
|
||
|
||
// Add icon if in workflow
|
||
if (workflowNodeTypes.has(nodeType)) {
|
||
const icon = document.createElement('span');
|
||
icon.textContent = '✓';
|
||
icon.style.color = '#0f0';
|
||
tag.appendChild(icon);
|
||
}
|
||
|
||
const nameSpan = document.createElement('span');
|
||
nameSpan.textContent = this.nodeTypes[nodeType]?.display_name || nodeType;
|
||
tag.appendChild(nameSpan);
|
||
|
||
const removeBtn = document.createElement('button');
|
||
removeBtn.textContent = '×';
|
||
removeBtn.style.cssText = `
|
||
background: transparent;
|
||
border: none;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
padding: 0 3px;
|
||
font-size: 16px;
|
||
`;
|
||
removeBtn.addEventListener('click', () => {
|
||
condition.node_types = condition.node_types.filter(t => t !== nodeType);
|
||
updateSelectedDisplay();
|
||
});
|
||
|
||
tag.appendChild(removeBtn);
|
||
selectedDisplay.appendChild(tag);
|
||
});
|
||
};
|
||
|
||
const filterDropdown = (searchTerm) => {
|
||
dropdownList.innerHTML = '';
|
||
const lowerSearch = searchTerm.toLowerCase();
|
||
|
||
const filtered = nodeTypesList.filter(nodeType => {
|
||
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
|
||
return displayName.toLowerCase().includes(lowerSearch) &&
|
||
!(condition.node_types || []).includes(nodeType);
|
||
});
|
||
|
||
if (filtered.length === 0) {
|
||
dropdownList.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
filtered.forEach(nodeType => {
|
||
const option = document.createElement('div');
|
||
option.style.cssText = `
|
||
padding: 8px;
|
||
cursor: pointer;
|
||
border-bottom: 1px solid #555;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
`;
|
||
|
||
// Add icon if in workflow
|
||
if (workflowNodeTypes.has(nodeType)) {
|
||
const icon = document.createElement('span');
|
||
icon.textContent = '✓';
|
||
icon.style.color = '#0f0';
|
||
option.appendChild(icon);
|
||
}
|
||
|
||
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
|
||
const textSpan = document.createElement('span');
|
||
textSpan.textContent = displayName;
|
||
option.appendChild(textSpan);
|
||
|
||
option.addEventListener('mouseover', () => {
|
||
option.style.background = '#007acc';
|
||
});
|
||
|
||
option.addEventListener('mouseout', () => {
|
||
option.style.background = '';
|
||
});
|
||
|
||
option.addEventListener('click', () => {
|
||
condition.node_types = condition.node_types || [];
|
||
condition.node_types.push(nodeType);
|
||
searchInput.value = '';
|
||
updateSelectedDisplay();
|
||
filterDropdown('');
|
||
searchInput.focus();
|
||
});
|
||
|
||
dropdownList.appendChild(option);
|
||
});
|
||
|
||
dropdownList.style.display = 'block';
|
||
};
|
||
|
||
searchInput.addEventListener('focus', () => {
|
||
filterDropdown(searchInput.value);
|
||
});
|
||
|
||
searchInput.addEventListener('input', () => {
|
||
filterDropdown(searchInput.value);
|
||
});
|
||
|
||
searchInput.addEventListener('blur', () => {
|
||
setTimeout(() => {
|
||
dropdownList.style.display = 'none';
|
||
}, 200);
|
||
});
|
||
|
||
updateSelectedDisplay();
|
||
|
||
selectContainer.appendChild(searchInput);
|
||
selectContainer.appendChild(dropdownList);
|
||
|
||
container.appendChild(label);
|
||
container.appendChild(selectedDisplay);
|
||
container.appendChild(selectContainer);
|
||
}
|
||
|
||
createAttributeInWorkflowFields(container, condition) {
|
||
const nodeTypeLabel = document.createElement('label');
|
||
nodeTypeLabel.textContent = 'Node Type: ';
|
||
nodeTypeLabel.style.display = 'block';
|
||
nodeTypeLabel.style.marginBottom = '5px';
|
||
|
||
// Node Type dropdown with search
|
||
const nodeTypeContainer = document.createElement('div');
|
||
nodeTypeContainer.style.cssText = `
|
||
position: relative;
|
||
width: 100%;
|
||
margin-bottom: 10px;
|
||
`;
|
||
|
||
const nodeTypeInput = document.createElement('input');
|
||
nodeTypeInput.type = 'text';
|
||
nodeTypeInput.placeholder = 'Search node type...';
|
||
nodeTypeInput.value = condition.node_type ? (this.nodeTypes[condition.node_type]?.display_name || condition.node_type) : '';
|
||
nodeTypeInput.style.cssText = `
|
||
width: 100%;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
`;
|
||
|
||
const nodeTypeDropdown = document.createElement('div');
|
||
nodeTypeDropdown.style.cssText = `
|
||
position: absolute;
|
||
width: 100%;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
background: #2a2a2a;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
z-index: 1000;
|
||
display: none;
|
||
`;
|
||
|
||
// Get node types from workflow
|
||
const workflowNodeTypes = new Set();
|
||
if (app.graph && app.graph._nodes) {
|
||
for (const node of app.graph._nodes) {
|
||
workflowNodeTypes.add(node.type);
|
||
}
|
||
}
|
||
|
||
const nodeTypesList = Object.keys(this.nodeTypes).sort();
|
||
|
||
const filterNodeTypeDropdown = (searchTerm) => {
|
||
nodeTypeDropdown.innerHTML = '';
|
||
const lowerSearch = searchTerm.toLowerCase();
|
||
|
||
const filtered = nodeTypesList.filter(nodeType => {
|
||
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
|
||
return displayName.toLowerCase().includes(lowerSearch);
|
||
});
|
||
|
||
if (filtered.length === 0) {
|
||
nodeTypeDropdown.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
filtered.forEach(nodeType => {
|
||
const option = document.createElement('div');
|
||
option.style.cssText = `
|
||
padding: 8px;
|
||
cursor: pointer;
|
||
border-bottom: 1px solid #555;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
`;
|
||
|
||
// Add icon if in workflow
|
||
if (workflowNodeTypes.has(nodeType)) {
|
||
const icon = document.createElement('span');
|
||
icon.textContent = '✓';
|
||
icon.style.color = '#0f0';
|
||
option.appendChild(icon);
|
||
}
|
||
|
||
const displayName = this.nodeTypes[nodeType]?.display_name || nodeType;
|
||
const textSpan = document.createElement('span');
|
||
textSpan.textContent = displayName;
|
||
option.appendChild(textSpan);
|
||
|
||
option.addEventListener('mouseover', () => {
|
||
option.style.background = '#007acc';
|
||
});
|
||
|
||
option.addEventListener('mouseout', () => {
|
||
option.style.background = '';
|
||
});
|
||
|
||
option.addEventListener('click', () => {
|
||
condition.node_type = nodeType;
|
||
nodeTypeInput.value = displayName;
|
||
nodeTypeDropdown.style.display = 'none';
|
||
// Refresh attribute dropdown
|
||
updateAttributeDropdown();
|
||
});
|
||
|
||
nodeTypeDropdown.appendChild(option);
|
||
});
|
||
|
||
nodeTypeDropdown.style.display = 'block';
|
||
};
|
||
|
||
nodeTypeInput.addEventListener('focus', () => {
|
||
filterNodeTypeDropdown(nodeTypeInput.value);
|
||
});
|
||
|
||
nodeTypeInput.addEventListener('input', () => {
|
||
filterNodeTypeDropdown(nodeTypeInput.value);
|
||
});
|
||
|
||
nodeTypeInput.addEventListener('blur', () => {
|
||
setTimeout(() => {
|
||
nodeTypeDropdown.style.display = 'none';
|
||
}, 200);
|
||
});
|
||
|
||
nodeTypeContainer.appendChild(nodeTypeInput);
|
||
nodeTypeContainer.appendChild(nodeTypeDropdown);
|
||
|
||
// Attribute Name dropdown
|
||
const attrLabel = document.createElement('label');
|
||
attrLabel.textContent = 'Attribute Name: ';
|
||
attrLabel.style.display = 'block';
|
||
attrLabel.style.marginBottom = '5px';
|
||
attrLabel.style.marginTop = '10px';
|
||
|
||
const attrSelect = document.createElement('select');
|
||
attrSelect.style.cssText = `
|
||
width: 100%;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
margin-bottom: 10px;
|
||
`;
|
||
|
||
const updateAttributeDropdown = () => {
|
||
attrSelect.innerHTML = '<option value="">Select attribute...</option>';
|
||
|
||
if (condition.node_type && this.nodeTypes[condition.node_type]) {
|
||
const attributes = this.nodeTypes[condition.node_type].attributes || {};
|
||
Object.keys(attributes).forEach(attrName => {
|
||
const option = document.createElement('option');
|
||
option.value = attrName;
|
||
option.textContent = attrName;
|
||
if (condition.attribute_name === attrName) {
|
||
option.selected = true;
|
||
}
|
||
attrSelect.appendChild(option);
|
||
});
|
||
}
|
||
};
|
||
|
||
attrSelect.addEventListener('change', () => {
|
||
condition.attribute_name = attrSelect.value || null;
|
||
});
|
||
|
||
updateAttributeDropdown();
|
||
|
||
// Attribute Values
|
||
const valuesLabel = document.createElement('label');
|
||
valuesLabel.textContent = 'Attribute Values (comma-separated, optional): ';
|
||
valuesLabel.style.display = 'block';
|
||
valuesLabel.style.marginBottom = '5px';
|
||
|
||
const valuesInput = document.createElement('input');
|
||
valuesInput.type = 'text';
|
||
valuesInput.value = (condition.attribute_values || []).join(', ');
|
||
valuesInput.placeholder = 'e.g., value1, value2';
|
||
valuesInput.style.cssText = `
|
||
width: 100%;
|
||
padding: 5px;
|
||
background: #2a2a2a;
|
||
color: #fff;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
`;
|
||
|
||
valuesInput.addEventListener('input', () => {
|
||
condition.attribute_values = valuesInput.value.split(',').map(s => s.trim()).filter(s => s);
|
||
});
|
||
|
||
container.appendChild(nodeTypeLabel);
|
||
container.appendChild(nodeTypeContainer);
|
||
container.appendChild(attrLabel);
|
||
container.appendChild(attrSelect);
|
||
container.appendChild(valuesLabel);
|
||
container.appendChild(valuesInput);
|
||
}
|
||
|
||
async saveCurrentNote() {
|
||
if (!this.currentEditingNote) return false;
|
||
|
||
// Clean up trigger conditions - remove null values
|
||
const cleanedTriggerConditions = (this.currentTriggerConditions || []).map(tc => {
|
||
const cleaned = { type: tc.type };
|
||
if (tc.node_types !== null && tc.node_types !== undefined) cleaned.node_types = tc.node_types;
|
||
if (tc.node_type !== null && tc.node_type !== undefined) cleaned.node_type = tc.node_type;
|
||
if (tc.attribute_name !== null && tc.attribute_name !== undefined) cleaned.attribute_name = tc.attribute_name;
|
||
if (tc.attribute_values !== null && tc.attribute_values !== undefined) cleaned.attribute_values = tc.attribute_values;
|
||
if (tc.workflow_names !== null && tc.workflow_names !== undefined) cleaned.workflow_names = tc.workflow_names;
|
||
return cleaned;
|
||
});
|
||
|
||
const data = {
|
||
content: this.contentTextarea.value,
|
||
format_style: this.formatSelect.value,
|
||
pinned: this.pinnedCheckbox.checked,
|
||
trigger_conditions: cleanedTriggerConditions
|
||
};
|
||
|
||
try {
|
||
const response = await api.fetchApi(`/autonotes/notes/${this.currentEditingNote.uuid}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(data),
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
// Update local copy
|
||
Object.assign(this.currentEditingNote, data);
|
||
await this.refreshNotes();
|
||
return true;
|
||
} else {
|
||
alert('Failed to save note');
|
||
return false;
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to save note:', error);
|
||
alert('Failed to save note');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async closeEditDialog(save = false) {
|
||
if (save && this.currentEditingNote) {
|
||
await this.saveCurrentNote();
|
||
}
|
||
|
||
if (this.editDialog) {
|
||
this.editDialog.parentElement.remove(); // Remove overlay
|
||
this.editDialog = null;
|
||
this.currentEditingNote = null;
|
||
}
|
||
}
|
||
|
||
async togglePin(note) {
|
||
try {
|
||
const newPinnedState = !note.pinned;
|
||
const response = await api.fetchApi(`/autonotes/notes/${note.uuid}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
content: note.content,
|
||
format_style: note.format_style,
|
||
pinned: newPinnedState,
|
||
trigger_conditions: note.trigger_conditions || []
|
||
}),
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
note.pinned = newPinnedState;
|
||
await this.refreshNotes();
|
||
} else {
|
||
alert('Failed to toggle pin');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to toggle pin:', error);
|
||
alert('Failed to toggle pin');
|
||
}
|
||
}
|
||
|
||
async addFolder(treeContainer, selectedNoteUuid) {
|
||
const name = prompt('Enter folder name:');
|
||
if (!name) return;
|
||
|
||
try {
|
||
const response = await api.fetchApi('/autonotes/folders', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ name }),
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.uuid) {
|
||
await this.loadFolders();
|
||
this.renderNoteTree(treeContainer, selectedNoteUuid);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to create folder:', error);
|
||
alert('Failed to create folder');
|
||
}
|
||
}
|
||
|
||
async renameFolder(folder, treeContainer, selectedNoteUuid) {
|
||
const newName = prompt('Enter new folder name:', folder.name);
|
||
if (!newName || newName === folder.name) return;
|
||
|
||
try {
|
||
const response = await api.fetchApi(`/autonotes/folders/${folder.uuid}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ name: newName }),
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
await this.loadFolders();
|
||
this.renderNoteTree(treeContainer, selectedNoteUuid);
|
||
} else {
|
||
alert('Failed to rename folder');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to rename folder:', error);
|
||
alert('Failed to rename folder');
|
||
}
|
||
}
|
||
|
||
async deleteFolder(folder, treeContainer, selectedNoteUuid) {
|
||
if (!confirm(`Delete folder "${folder.name}"? Notes inside will not be deleted.`)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await api.fetchApi(`/autonotes/folders/${folder.uuid}`, {
|
||
method: 'DELETE',
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
await this.loadFolders();
|
||
this.renderNoteTree(treeContainer, selectedNoteUuid);
|
||
} else {
|
||
alert('Failed to delete folder');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to delete folder:', error);
|
||
alert('Failed to delete folder');
|
||
}
|
||
}
|
||
|
||
async moveNoteToFolder(noteUuid, folderUuid, treeContainer, selectedNoteUuid) {
|
||
try {
|
||
const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ folder_uuid: folderUuid }),
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
await this.refreshNotes();
|
||
this.renderNoteTree(treeContainer, selectedNoteUuid);
|
||
} else {
|
||
alert('Failed to move note');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to move note:', error);
|
||
alert('Failed to move note');
|
||
}
|
||
}
|
||
|
||
showNoteContextMenu(event, note, treeContainer, selectedNoteUuid) {
|
||
// Remove any existing context menu
|
||
const existingMenu = document.querySelector('.note-context-menu');
|
||
if (existingMenu) {
|
||
existingMenu.remove();
|
||
}
|
||
|
||
const menu = document.createElement('div');
|
||
menu.className = 'note-context-menu';
|
||
menu.style.cssText = `
|
||
position: fixed;
|
||
left: ${event.clientX}px;
|
||
top: ${event.clientY}px;
|
||
background: #2a2a2a;
|
||
border: 1px solid #555;
|
||
border-radius: 4px;
|
||
padding: 5px 0;
|
||
z-index: 10000;
|
||
min-width: 150px;
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
||
`;
|
||
|
||
const renameOption = document.createElement('div');
|
||
renameOption.textContent = 'Rename';
|
||
renameOption.style.cssText = `
|
||
padding: 8px 15px;
|
||
cursor: pointer;
|
||
color: #fff;
|
||
`;
|
||
renameOption.addEventListener('mouseenter', () => {
|
||
renameOption.style.background = '#007acc';
|
||
});
|
||
renameOption.addEventListener('mouseleave', () => {
|
||
renameOption.style.background = '';
|
||
});
|
||
renameOption.addEventListener('click', async () => {
|
||
menu.remove();
|
||
const newName = prompt('Enter new note name:', note.name);
|
||
if (newName && newName !== note.name) {
|
||
await this.renameNote(note.uuid, newName, treeContainer, selectedNoteUuid);
|
||
}
|
||
});
|
||
|
||
const deleteOption = document.createElement('div');
|
||
deleteOption.textContent = 'Delete';
|
||
deleteOption.style.cssText = `
|
||
padding: 8px 15px;
|
||
cursor: pointer;
|
||
color: #fff;
|
||
`;
|
||
deleteOption.addEventListener('mouseenter', () => {
|
||
deleteOption.style.background = '#cc0000';
|
||
});
|
||
deleteOption.addEventListener('mouseleave', () => {
|
||
deleteOption.style.background = '';
|
||
});
|
||
deleteOption.addEventListener('click', async () => {
|
||
menu.remove();
|
||
if (confirm(`Delete note "${note.name}"?`)) {
|
||
await this.deleteNote(note.uuid, treeContainer, selectedNoteUuid);
|
||
}
|
||
});
|
||
|
||
menu.appendChild(renameOption);
|
||
menu.appendChild(deleteOption);
|
||
document.body.appendChild(menu);
|
||
|
||
// Close menu when clicking outside
|
||
const closeMenu = (e) => {
|
||
if (!menu.contains(e.target)) {
|
||
menu.remove();
|
||
document.removeEventListener('click', closeMenu);
|
||
}
|
||
};
|
||
setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
||
}
|
||
|
||
async renameNote(noteUuid, newName, treeContainer, selectedNoteUuid) {
|
||
try {
|
||
const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ name: newName }),
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
await this.refreshNotes();
|
||
this.renderNoteTree(treeContainer, selectedNoteUuid);
|
||
} else {
|
||
alert('Failed to rename note');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to rename note:', error);
|
||
alert('Failed to rename note');
|
||
}
|
||
}
|
||
|
||
async deleteNote(noteUuid, treeContainer, selectedNoteUuid) {
|
||
try {
|
||
const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, {
|
||
method: 'DELETE',
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
await this.refreshNotes();
|
||
this.renderNoteTree(treeContainer, selectedNoteUuid);
|
||
// Clear editor if deleted note was being edited
|
||
if (this.currentEditingNote && this.currentEditingNote.uuid === noteUuid) {
|
||
this.currentEditingNote = null;
|
||
if (this.contentTextarea) {
|
||
this.contentTextarea.value = '';
|
||
}
|
||
}
|
||
} else {
|
||
alert('Failed to delete note');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to delete note:', error);
|
||
alert('Failed to delete note');
|
||
}
|
||
}
|
||
|
||
async updateNoteContent(noteUuid, content) {
|
||
try {
|
||
const response = await api.fetchApi(`/autonotes/notes/${noteUuid}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ content }),
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
// Refresh to show updated content
|
||
await this.refreshNotes();
|
||
} else {
|
||
alert('Failed to save note');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to save note:', error);
|
||
alert('Failed to save note');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Initialize the AutoNotes manager when the extension loads
|
||
app.registerExtension({
|
||
name: "AutoNotes",
|
||
async setup() {
|
||
new AutoNotesManager();
|
||
},
|
||
}); |