Files
BennyKok-comfyui-deploy/web-plugin/workflow-list.js
T

1342 lines
40 KiB
JavaScript

// Workflow list management
let workflowsState = {
workflows: [],
offset: 0,
limit: 20,
loading: false,
hasMore: true,
initialized: false,
currentSearch: "",
};
// Make workflowsState accessible globally
window.workflowsState = workflowsState;
// Helper: ensure a `ComfyDeploy` node exists and is updated with correct version
function ensureComfyDeployNode({ name, id, version }) {
if (!window.app || !window.app.graph) {
console.warn("ComfyDeploy: App or graph not available");
return;
}
const graph = window.app.graph;
let nodes = graph.findNodesByType("ComfyDeploy");
graph.beforeChange();
if (nodes.length === 0) {
// Create new ComfyDeploy node
console.log("ComfyDeploy: Creating new ComfyDeploy node", {
name,
id,
version,
});
const node = LiteGraph.createNode("ComfyDeploy");
node.configure({ widgets_values: [name || "", id || "", version || 1] });
node.pos = [0, 0];
graph.add(node);
} else {
// Update existing node - always sync all values to ensure consistency
const node = nodes[0];
const currentValues = node.widgets_values || [];
const newValues = [
name || currentValues[0] || "",
id || currentValues[1] || "",
version || currentValues[2] || 1,
];
// Check if any values changed
const valuesChanged =
JSON.stringify(currentValues) !== JSON.stringify(newValues);
if (valuesChanged) {
console.log("ComfyDeploy: Updating ComfyDeploy node", {
from: {
name: currentValues[0],
id: currentValues[1],
version: currentValues[2],
},
to: { name: newValues[0], id: newValues[1], version: newValues[2] },
});
node.widgets_values = newValues;
// Force widget updates if they exist
if (node.widgets) {
node.widgets.forEach((widget, index) => {
if (widget && newValues[index] !== undefined) {
widget.value = newValues[index];
}
});
}
} else {
console.log("ComfyDeploy: ComfyDeploy node already up to date", {
name,
id,
version,
});
}
}
graph.afterChange();
}
// Helper: automatically ensure ComfyDeploy node after workflow loading
async function ensureComfyDeployNodeAfterLoad(
{ name, id, version },
delayMs = 200
) {
// Wait for workflow to be fully loaded
await new Promise((resolve) => setTimeout(resolve, delayMs));
// Ensure the ComfyDeploy node is present and correct
ensureComfyDeployNode({ name, id, version });
// Also refresh the workflow card if it's visible
if (window.refreshCurrentWorkflowCard) {
setTimeout(() => {
window.refreshCurrentWorkflowCard(id, version);
}, 100);
}
}
function getComfyDeployMeta() {
if (!window.app || !window.app.graph) return null;
const graph = window.app.graph;
const nodes = graph.findNodesByType("ComfyDeploy");
if (nodes.length === 0) return null;
const v = nodes[0].widgets_values || [];
const meta = { name: v[0], id: v[1], version: v[2] };
console.log("Current ComfyDeploy meta:", meta);
return meta;
}
async function fetchWorkflowVersions(
getData,
workflowId,
offset = 0,
limit = 20,
search = ""
) {
const data = getData();
const params = new URLSearchParams({
workflow_id: workflowId,
offset: String(offset),
limit: String(limit),
api_url: data.apiUrl || "https://api.comfydeploy.com",
...(search && { search }),
});
const res = await fetch(`/comfyui-deploy/workflow/versions?${params}`, {
method: "GET",
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
});
if (!res.ok) return [];
const versions = await res.json();
return Array.isArray(versions) ? versions : [];
}
async function fetchWorkflowVersionData(getData, workflowId, version) {
const data = getData();
const params = new URLSearchParams({
workflow_id: workflowId,
version: String(version),
api_url: data.apiUrl || "https://api.comfydeploy.com",
});
const res = await fetch(`/comfyui-deploy/workflow/version?${params}`, {
method: "GET",
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
});
if (!res.ok) throw new Error(`Failed to fetch workflow version ${version}`);
return await res.json();
}
// Shared function to create workflow card HTML template
function createWorkflowCardHTML(workflowName, version) {
return `
<div style="display: flex; justify-content: space-between; align-items: flex-start; gap: 12px;">
<div style="flex: 1; min-width: 0;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<div style="width: 8px; height: 8px; background: #10b981; border-radius: 50%; box-shadow: 0 0 8px rgba(16, 185, 129, 0.4);"></div>
<span style="font-size: 8px; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">Current Workflow</span>
</div>
<h3 style="font-size: 14px; font-weight: 500; margin: 0 0 6px 0; color: #f9fafb; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
${workflowName}
</h3>
<div id="cd-current-comment" style="font-size: 10px; color: #d1d5db; line-height: 1.4; margin-top: 4px; display: none; font-style: italic;"></div>
</div>
<div style="display: flex; align-items: center; gap: 10px; flex-shrink: 0;">
<div style="position: relative;">
<span id="cd-version-badge" style="
display: inline-flex;
align-items: center;
font-size: 11px;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: white;
padding: 6px 10px;
border-radius: 8px;
font-weight: 600;
letter-spacing: 0.5px;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
">v${version}</span>
</div>
<button id="cd-version-btn" style="
padding: 8px 12px;
border: 1px solid #4b5563;
border-radius: 8px;
background: linear-gradient(135deg, #374151, #1f2937);
color: #f3f4f6;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.15s ease;
display: flex;
align-items: center;
gap: 4px;
" onmouseover="this.style.background='linear-gradient(135deg, #4b5563, #374151)'; this.style.borderColor='#6b7280';" onmouseout="this.style.background='linear-gradient(135deg, #374151, #1f2937)'; this.style.borderColor='#4b5563';">
<span>Change</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
</div>
</div>
<div id="cd-version-panel" style="display: none; position: absolute; top: 100%; right: 0; z-index: 1000; margin-top: 8px;">
<div id="cd-version-dropdown" style="
width: 320px;
max-height: 300px;
overflow: hidden;
background: #1f2937;
border: 1px solid #4b5563;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05);
backdrop-filter: blur(8px);
">
<div style="
position: sticky;
top: 0;
padding: 12px 16px;
background: linear-gradient(135deg, #374151, #1f2937);
border-bottom: 1px solid #4b5563;
font-size: 12px;
color: #d1d5db;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
">Select Version</div>
<div id="cd-version-list" style="
max-height: 240px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #4b5563 transparent;
"></div>
<div id="cd-version-loading" style="
display: none;
padding: 16px;
text-align: center;
color: #9ca3af;
font-size: 12px;
">Loading versions...</div>
</div>
</div>
`;
}
function renderCurrentWorkflowCard(element, getData) {
const container = element.querySelector("#workflows-container");
if (!container) return;
// Remove old card if any
const old = container.querySelector("#cd-current-workflow-card");
if (old) old.remove();
const meta = getComfyDeployMeta();
if (!meta || !meta.id) {
console.log("No ComfyDeploy meta found, not showing workflow card");
return; // Only show when node exists
}
console.log("Rendering workflow card with meta:", meta);
const card = document.createElement("div");
card.id = "cd-current-workflow-card";
card.style.cssText = `
margin: 8px 0 16px 0;
padding: 16px;
border: 1px solid #374151;
border-radius: 12px;
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
color: #f9fafb;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
transition: all 0.2s ease;
position: relative;
`;
// Add hover effect
card.addEventListener("mouseenter", () => {
card.style.transform = "translateY(-1px)";
card.style.boxShadow = "0 8px 20px rgba(0, 0, 0, 0.5)";
card.style.borderColor = "#4b5563";
});
card.addEventListener("mouseleave", () => {
card.style.transform = "translateY(0)";
card.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.4)";
card.style.borderColor = "#374151";
});
// Use the shared template
card.innerHTML = createWorkflowCardHTML(
meta.name || "Unnamed Workflow",
meta.version || 1
);
// Add custom scrollbar styles
const scrollbarStyle = document.createElement("style");
scrollbarStyle.textContent = `
#cd-version-list::-webkit-scrollbar {
width: 6px;
}
#cd-version-list::-webkit-scrollbar-track {
background: transparent;
}
#cd-version-list::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 3px;
}
#cd-version-list::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
`;
document.head.appendChild(scrollbarStyle);
const btn = card.querySelector("#cd-version-btn");
const panel = card.querySelector("#cd-version-panel");
const dropdown = card.querySelector("#cd-version-dropdown");
const list = card.querySelector("#cd-version-list");
const loading = card.querySelector("#cd-version-loading");
const state = {
offset: 0,
limit: 20,
loading: false,
hasMore: true,
isOpen: false,
};
// Close dropdown when clicking outside
const handleClickOutside = (event) => {
if (!card.contains(event.target)) {
closeDropdown();
}
};
const openDropdown = () => {
state.isOpen = true;
panel.style.display = "block";
btn.querySelector("svg").style.transform = "rotate(180deg)";
document.addEventListener("click", handleClickOutside);
// Add opening animation
dropdown.style.opacity = "0";
dropdown.style.transform = "translateY(-8px)";
setTimeout(() => {
dropdown.style.transition = "opacity 0.2s ease, transform 0.2s ease";
dropdown.style.opacity = "1";
dropdown.style.transform = "translateY(0)";
}, 10);
};
const closeDropdown = () => {
state.isOpen = false;
panel.style.display = "none";
btn.querySelector("svg").style.transform = "rotate(0deg)";
document.removeEventListener("click", handleClickOutside);
};
async function loadMore() {
if (state.loading || !state.hasMore) return;
state.loading = true;
loading.style.display = "block";
try {
const items = await fetchWorkflowVersions(
getData,
meta.id,
state.offset,
state.limit
);
if (items.length === 0) {
state.hasMore = false;
// If this is the first load and no items, show a message
if (state.offset === 0) {
const noVersionsMsg = document.createElement("div");
noVersionsMsg.style.cssText = `
padding: 16px;
text-align: center;
color: #9ca3af;
font-size: 12px;
`;
noVersionsMsg.textContent = "No versions found";
list.appendChild(noVersionsMsg);
}
} else {
items.forEach((v, index) => {
const row = document.createElement("div");
const isCurrentVersion = v.version === meta.version;
row.style.cssText = `
padding: 12px 16px;
border-bottom: 1px solid #374151;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
justify-content: space-between;
align-items: center;
${
isCurrentVersion
? "background: rgba(59, 130, 246, 0.1); border-left: 3px solid #3b82f6;"
: ""
}
`;
row.innerHTML = `
<div>
<div style="font-size: 13px; font-weight: 500; color: ${
isCurrentVersion ? "#3b82f6" : "#f3f4f6"
}; margin-bottom: 2px;">
v${v.version} ${isCurrentVersion ? "(Current)" : ""}
</div>
${
v.comment
? `<div style="font-size: 11px; color: #9ca3af; line-height: 1.3;">${v.comment}</div>`
: ""
}
</div>
${
isCurrentVersion
? '<div style="color: #3b82f6; font-size: 12px;">✓</div>'
: ""
}
`;
if (!isCurrentVersion) {
row.addEventListener("mouseenter", () => {
row.style.background = "#374151";
});
row.addEventListener("mouseleave", () => {
row.style.background = "transparent";
});
}
row.onclick = async () => {
if (isCurrentVersion) return;
try {
// Add loading state to the row
row.style.opacity = "0.6";
row.style.cursor = "wait";
const data = await fetchWorkflowVersionData(
getData,
meta.id,
v.version
);
if (data?.workflow) {
window.app.loadGraphData(data.workflow);
// Ensure ComfyDeploy node is correct and refresh UI
await ensureComfyDeployNodeAfterLoad({
name: meta.name,
id: meta.id,
version: v.version,
});
renderCurrentWorkflowCard(element, getData);
window.app.extensionManager.toast.add({
severity: "success",
summary: "Version loaded",
detail: `Loaded v${v.version}${
v.comment ? ` - ${v.comment}` : ""
}`,
life: 3000,
});
closeDropdown();
}
} catch (e) {
row.style.opacity = "1";
row.style.cursor = "pointer";
window.app.extensionManager.toast.add({
severity: "error",
summary: "Failed to load version",
detail: e.message,
life: 4000,
});
}
};
list.appendChild(row);
});
state.offset += items.length;
}
} catch (error) {
console.error("Error loading versions:", error);
// Show error message in dropdown if it's the first load
if (state.offset === 0) {
const errorMsg = document.createElement("div");
errorMsg.style.cssText = `
padding: 16px;
text-align: center;
color: #ef4444;
font-size: 12px;
line-height: 1.4;
`;
errorMsg.innerHTML = `
<div style="margin-bottom: 4px;">Failed to load versions</div>
<div style="font-size: 11px; color: #9ca3af;">Check your connection and try again</div>
`;
list.appendChild(errorMsg);
}
// Don't set hasMore to false on error, allow retry
} finally {
state.loading = false;
loading.style.display = "none";
}
}
btn.onclick = async (e) => {
e.stopPropagation();
if (state.isOpen) {
closeDropdown();
} else {
openDropdown();
// Always clear and reload to get fresh data
list.innerHTML = "";
// Reset state for fresh load
state.offset = 0;
state.hasMore = true;
state.loading = false;
await loadMore();
}
};
list.addEventListener("scroll", () => {
if (list.scrollTop + list.clientHeight >= list.scrollHeight - 10) {
loadMore();
}
});
// Populate current comment
(async () => {
try {
const vdata = await fetchWorkflowVersionData(
getData,
meta.id,
meta.version
);
const commentEl = card.querySelector("#cd-current-comment");
if (commentEl && vdata?.comment) {
commentEl.style.display = "block";
commentEl.textContent = vdata.comment;
}
} catch (error) {
console.warn(
`Could not fetch version ${meta.version} data:`,
error.message
);
// Don't show error to user for comment fetching, just log it
const commentEl = card.querySelector("#cd-current-comment");
if (commentEl) {
commentEl.style.display = "none";
}
}
})();
const searchEl = container.querySelector("#cd-search-container");
if (searchEl) {
container.insertBefore(card, searchEl);
} else {
container.prepend(card);
}
}
async function fetchWorkflows(getData, offset = 0, limit = 20, search = "") {
try {
const data = getData();
if (!data.apiKey) {
throw new Error("API key not configured");
}
const params = new URLSearchParams({
offset: offset.toString(),
limit: limit.toString(),
api_url: data.apiUrl || "https://api.comfydeploy.com",
...(search && { search }),
});
const response = await fetch(`/comfyui-deploy/workflows?${params}`, {
method: "GET",
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch workflows: ${response.status}`);
}
const result = await response.json();
console.log("result", result);
return Array.isArray(result) ? result : [];
} catch (error) {
console.error("Error fetching workflows:", error);
return [];
}
}
function createWorkflowItem(workflow, getTimeAgo, getData) {
const li = document.createElement("li");
let loadingToast = null;
li.style.cssText = `
border-bottom: 1px solid #444;
background: transparent;
transition: all 0.2s ease;
cursor: pointer;
`;
li.addEventListener("mouseenter", () => {
li.style.background = "#333";
});
li.addEventListener("mouseleave", () => {
li.style.background = "transparent";
});
// Add click handler to fetch and load workflow data
li.addEventListener("click", async () => {
try {
const data = getData();
if (!data.apiKey) {
console.error("No API key configured");
return;
}
// Show loading toast
loadingToast = window.app.extensionManager.toast.add({
severity: "info",
summary: "Loading workflow...",
detail: `Loading "${workflow.name}"`,
life: 3000,
});
const params = new URLSearchParams({
workflow_id: workflow.id,
api_url: data.apiUrl || "https://api.comfydeploy.com",
});
const response = await fetch(`/comfyui-deploy/workflow?${params}`, {
method: "GET",
headers: {
Authorization: `Bearer ${data.apiKey}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`Failed to fetch workflow: ${response.status}`);
}
const workflowData = await response.json();
console.log("Workflow data:", workflowData);
// Load the workflow into the graph
if (workflowData.versions && workflowData.versions.length > 0) {
const latestVersion = workflowData.versions[0];
if (latestVersion.workflow && window.app) {
// Load the workflow
window.app.loadGraphData(latestVersion.workflow);
// Ensure ComfyDeploy node is correct with latest version
await ensureComfyDeployNodeAfterLoad({
name: workflow.name,
id: workflow.id,
version: latestVersion.version,
});
// Show success toast
window.app.extensionManager.toast.add({
severity: "success",
summary: "Workflow loaded successfully",
detail: `Loaded "${workflow.name}" v${latestVersion.version}`,
life: 3000,
});
// Re-render header card to reflect newly selected workflow
renderCurrentWorkflowCard(document, getData);
}
}
} catch (error) {
console.error("Error loading workflow:", error);
// Show error toast
window.app.extensionManager.toast.add({
severity: "error",
summary: "Failed to load workflow",
detail: error.message,
life: 5000,
});
} finally {
if (loadingToast) {
loadingToast.close();
}
}
});
const updatedDate = new Date(workflow.updated_at);
const timeAgo = getTimeAgo(updatedDate);
li.innerHTML = `
<div style="padding: 12px 16px;">
<div style="display: flex; align-items: flex-start; gap: 12px;">
${
workflow.cover_image
? `<img src="${workflow.cover_image}"
style="width: 40px; height: 40px; border-radius: 4px; object-fit: cover; flex-shrink: 0;"
onerror="this.style.display='none'">`
: `<div style="width: 40px; height: 40px; border-radius: 4px; background: #444; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 14px; color: #888;">
${workflow.name.charAt(0).toUpperCase()}
</div>`
}
<div style="flex: 1; min-width: 0;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<h4 style="margin: 0; font-size: 14px; font-weight: 400; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${workflow.name}
</h4>
${
workflow.pinned
? `<span style="color: #ffd700; font-size: 12px;">📌</span>`
: ""
}
</div>
${
workflow.description
? `<p style="margin: 0 0 8px 0; font-size: 12px; color: #bbb; line-height: 1.3; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">
${workflow.description}
</p>`
: ""
}
<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<img src="${workflow.user_icon}"
style="width: 16px; height: 16px; border-radius: 50%;"
onerror="this.style.display='none'">
<span style="font-size: 11px; color: #888;">
${workflow.user_name} • Updated ${timeAgo}
</span>
</div>
</div>
</div>
</div>
`;
return li;
}
async function loadMoreWorkflows(element, getData, getTimeAgo) {
if (workflowsState.loading || !workflowsState.hasMore) return;
workflowsState.loading = true;
const workflowsList = element.querySelector("#workflows-list");
const workflowsLoading = element.querySelector("#workflows-loading");
// Show loading indicator
workflowsLoading.style.display = "flex";
try {
const newWorkflows = await fetchWorkflows(
getData,
workflowsState.offset,
workflowsState.limit,
workflowsState.currentSearch
);
if (newWorkflows.length === 0) {
workflowsState.hasMore = false;
} else {
workflowsState.workflows.push(...newWorkflows);
workflowsState.offset += newWorkflows.length;
// Render new workflow items
newWorkflows.forEach((workflow) => {
const workflowItem = createWorkflowItem(workflow, getTimeAgo, getData);
workflowsList.appendChild(workflowItem);
});
}
} catch (error) {
console.error("Error loading more workflows:", error);
} finally {
workflowsState.loading = false;
workflowsLoading.style.display = "none";
}
}
function setupInfiniteScroll(container, element, getData, getTimeAgo) {
let isScrolling = false;
container.addEventListener("scroll", () => {
if (isScrolling) return;
const { scrollTop, scrollHeight, clientHeight } = container;
// Load more when scrolled to bottom (with 100px threshold)
if (scrollTop + clientHeight >= scrollHeight - 100) {
isScrolling = true;
loadMoreWorkflows(element, getData, getTimeAgo).finally(() => {
isScrolling = false;
});
}
});
}
async function initializeWorkflowsList(element, getData, getTimeAgo) {
const workflowsContainer = element.querySelector("#workflows-container");
const workflowsList = element.querySelector("#workflows-list");
const workflowsLoading = element.querySelector("#workflows-loading");
// Check if already initialized AND the DOM elements still exist
if (
workflowsState.initialized &&
workflowsList &&
workflowsList.children.length > 0
)
return;
try {
// Reset state (always reset when reinitializing)
workflowsState = {
workflows: [],
offset: 0,
limit: 20,
loading: false,
hasMore: true,
initialized: true,
currentSearch: "",
};
// Clear existing content in case of reinitialization
if (workflowsList) {
workflowsList.innerHTML = "";
}
// Show container and loading
workflowsContainer.style.display = "block";
workflowsLoading.style.display = "flex";
// Style the workflows list for full height scrolling
workflowsList.style.cssText = `
list-style-type: none;
padding: 0;
margin: 0;
height: calc(100vh - 650px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #666 transparent;
border-top: 1px solid #444;
`;
// Add webkit scrollbar styles
const style = document.createElement("style");
style.textContent = `
#workflows-list::-webkit-scrollbar {
width: 6px;
}
#workflows-list::-webkit-scrollbar-track {
background: transparent;
}
#workflows-list::-webkit-scrollbar-thumb {
background: #666;
border-radius: 3px;
}
#workflows-list::-webkit-scrollbar-thumb:hover {
background: #777;
}
`;
document.head.appendChild(style);
// Setup infinite scroll
setupInfiniteScroll(workflowsList, element, getData, getTimeAgo);
// Load initial workflows
await loadMoreWorkflows(element, getData, getTimeAgo);
// Render current workflow card if a ComfyDeploy node exists
try {
renderCurrentWorkflowCard(element, getData);
} catch (e) {
console.error("Render current workflow card failed", e);
}
// Show the list
workflowsList.style.display = "block";
} catch (error) {
console.error("Error initializing workflows list:", error);
workflowsLoading.innerHTML = `
<div style="text-align: center; color: #e74c3c; font-size: 12px; padding: 20px;">
<div>Failed to load workflows</div>
<button onclick="initializeWorkflowsList(this.closest('.comfy-menu'), getData, getTimeAgo)"
style="margin-top: 8px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; cursor: pointer;">
Retry
</button>
</div>
`;
}
}
// Search functionality
function addWorkflowSearch(element, getData, getTimeAgo) {
const workflowsContainer = element.querySelector("#workflows-container");
const h4 = workflowsContainer.querySelector("h4");
const searchContainer = document.createElement("div");
searchContainer.id = "cd-search-container";
searchContainer.style.cssText = "margin-bottom: 12px;";
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = "Search workflows...";
searchInput.style.cssText = `
width: 100%;
padding: 8px 12px;
border: 1px solid #555;
border-radius: 6px;
font-size: 12px;
box-sizing: border-box;
background: #333;
color: #fff;
`;
let searchTimeout;
searchInput.addEventListener("input", (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
const searchTerm = e.target.value.trim();
// Update the tracked search term
workflowsState.currentSearch = searchTerm;
// Reset state for new search
workflowsState.workflows = [];
workflowsState.offset = 0;
workflowsState.hasMore = true;
// Clear current list
const workflowsList = element.querySelector("#workflows-list");
workflowsList.innerHTML = "";
// Load with search term
workflowsState.loading = false;
await loadMoreWorkflows(element, getData, getTimeAgo);
}, 300);
});
searchContainer.appendChild(searchInput);
h4.after(searchContainer);
}
// Function to refresh current workflow card after deployment
function refreshCurrentWorkflowCard(workflowId = null, version = null) {
try {
console.log("Refreshing current workflow card...", { workflowId, version });
// Find the current workflow card container
const container = document.querySelector("#workflows-container");
if (container) {
const getDataFunction = window.comfyDeployGetData;
// If specific workflow ID and version are provided, use those
if (workflowId && version) {
console.log("Using provided workflow ID and version:", {
workflowId,
version,
});
// Create a custom renderCurrentWorkflowCard that uses the provided data
renderCurrentWorkflowCardWithData(
document,
getDataFunction,
workflowId,
version
);
} else {
// Fallback to getting meta from ComfyDeploy node
const currentMeta = getComfyDeployMeta();
console.log("Meta from node:", currentMeta);
// Re-render the current workflow card with updated version
renderCurrentWorkflowCard(document, getDataFunction);
}
console.log("Current workflow card refreshed after deployment");
} else {
console.log("Workflows container not found, skipping refresh");
}
} catch (error) {
console.error("Error refreshing current workflow card:", error);
}
}
// Function to render workflow card with specific data (not from ComfyDeploy node)
function renderCurrentWorkflowCardWithData(
element,
getData,
workflowId,
version
) {
const container = element.querySelector("#workflows-container");
if (!container) return;
// Remove old card if any
const old = container.querySelector("#cd-current-workflow-card");
if (old) old.remove();
// Get the workflow name from the ComfyDeploy node if available
const meta = getComfyDeployMeta();
const workflowName = meta?.name || "Unnamed Workflow";
console.log("Rendering workflow card with provided data:", {
workflowId,
version,
workflowName,
});
const card = document.createElement("div");
card.id = "cd-current-workflow-card";
card.style.cssText = `
margin: 8px 0 16px 0;
padding: 16px;
border: 1px solid #374151;
border-radius: 12px;
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
color: #f9fafb;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
transition: all 0.2s ease;
position: relative;
`;
// Add hover effect
card.addEventListener("mouseenter", () => {
card.style.transform = "translateY(-1px)";
card.style.boxShadow = "0 8px 20px rgba(0, 0, 0, 0.5)";
card.style.borderColor = "#4b5563";
});
card.addEventListener("mouseleave", () => {
card.style.transform = "translateY(0)";
card.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.4)";
card.style.borderColor = "#374151";
});
// Use the shared template
card.innerHTML = createWorkflowCardHTML(workflowName, version);
// Add the same dropdown functionality as the original card
setupVersionDropdown(card, getData, workflowId, version, workflowName);
// Populate current comment using the provided version
(async () => {
try {
const vdata = await fetchWorkflowVersionData(
getData,
workflowId,
version
);
const commentEl = card.querySelector("#cd-current-comment");
if (commentEl && vdata?.comment) {
commentEl.style.display = "block";
commentEl.textContent = vdata.comment;
}
} catch (error) {
console.warn(`Could not fetch version ${version} data:`, error.message);
// Don't show error to user for comment fetching, just log it
const commentEl = card.querySelector("#cd-current-comment");
if (commentEl) {
commentEl.style.display = "none";
}
}
})();
const searchEl = container.querySelector("#cd-search-container");
if (searchEl) {
container.insertBefore(card, searchEl);
} else {
container.prepend(card);
}
}
// Extract dropdown setup logic to reuse
function setupVersionDropdown(
card,
getData,
workflowId,
currentVersion,
workflowName
) {
const btn = card.querySelector("#cd-version-btn");
const panel = card.querySelector("#cd-version-panel");
const dropdown = card.querySelector("#cd-version-dropdown");
const list = card.querySelector("#cd-version-list");
const loading = card.querySelector("#cd-version-loading");
const state = {
offset: 0,
limit: 20,
loading: false,
hasMore: true,
isOpen: false,
};
// Close dropdown when clicking outside
const handleClickOutside = (event) => {
if (!card.contains(event.target)) {
closeDropdown();
}
};
const openDropdown = () => {
state.isOpen = true;
panel.style.display = "block";
btn.querySelector("svg").style.transform = "rotate(180deg)";
document.addEventListener("click", handleClickOutside);
// Add opening animation
dropdown.style.opacity = "0";
dropdown.style.transform = "translateY(-8px)";
setTimeout(() => {
dropdown.style.transition = "opacity 0.2s ease, transform 0.2s ease";
dropdown.style.opacity = "1";
dropdown.style.transform = "translateY(0)";
}, 10);
};
const closeDropdown = () => {
state.isOpen = false;
panel.style.display = "none";
btn.querySelector("svg").style.transform = "rotate(0deg)";
document.removeEventListener("click", handleClickOutside);
};
async function loadMore() {
if (state.loading || !state.hasMore) return;
state.loading = true;
loading.style.display = "block";
try {
const items = await fetchWorkflowVersions(
getData,
workflowId,
state.offset,
state.limit
);
if (items.length === 0) {
state.hasMore = false;
if (state.offset === 0) {
const noVersionsMsg = document.createElement("div");
noVersionsMsg.style.cssText = `
padding: 16px;
text-align: center;
color: #9ca3af;
font-size: 12px;
`;
noVersionsMsg.textContent = "No versions found";
list.appendChild(noVersionsMsg);
}
} else {
items.forEach((v, index) => {
const row = document.createElement("div");
const isCurrentVersion = v.version == currentVersion; // Use == for type coercion
row.style.cssText = `
padding: 12px 16px;
border-bottom: 1px solid #374151;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
justify-content: space-between;
align-items: center;
${
isCurrentVersion
? "background: rgba(59, 130, 246, 0.1); border-left: 3px solid #3b82f6;"
: ""
}
`;
row.innerHTML = `
<div>
<div style="font-size: 13px; font-weight: 500; color: ${
isCurrentVersion ? "#3b82f6" : "#f3f4f6"
}; margin-bottom: 2px;">
v${v.version} ${isCurrentVersion ? "(Current)" : ""}
</div>
${
v.comment
? `<div style="font-size: 11px; color: #9ca3af; line-height: 1.3;">${v.comment}</div>`
: ""
}
</div>
${
isCurrentVersion
? '<div style="color: #3b82f6; font-size: 12px;">✓</div>'
: ""
}
`;
if (!isCurrentVersion) {
row.addEventListener("mouseenter", () => {
row.style.background = "#374151";
});
row.addEventListener("mouseleave", () => {
row.style.background = "transparent";
});
}
row.onclick = async () => {
if (isCurrentVersion) return;
try {
row.style.opacity = "0.6";
row.style.cursor = "wait";
const data = await fetchWorkflowVersionData(
getData,
workflowId,
v.version
);
if (data?.workflow) {
window.app.loadGraphData(data.workflow);
// Ensure ComfyDeploy node is correct and refresh UI
await ensureComfyDeployNodeAfterLoad({
name: workflowName,
id: workflowId,
version: v.version,
});
window.app.extensionManager.toast.add({
severity: "success",
summary: "Version loaded",
detail: `Loaded v${v.version}${
v.comment ? ` - ${v.comment}` : ""
}`,
life: 3000,
});
closeDropdown();
} else {
throw new Error(
data === null
? `Version ${v.version} not found or no longer exists`
: `Version ${v.version} has no workflow data`
);
}
} catch (e) {
row.style.opacity = "1";
row.style.cursor = "pointer";
window.app.extensionManager.toast.add({
severity: "error",
summary: "Failed to load version",
detail: e.message || `Could not load version ${v.version}`,
life: 4000,
});
}
};
list.appendChild(row);
});
state.offset += items.length;
}
} catch (error) {
console.error("Error loading versions:", error);
if (state.offset === 0) {
const errorMsg = document.createElement("div");
errorMsg.style.cssText = `
padding: 16px;
text-align: center;
color: #ef4444;
font-size: 12px;
line-height: 1.4;
`;
errorMsg.innerHTML = `
<div style="margin-bottom: 4px;">Failed to load versions</div>
<div style="font-size: 11px; color: #9ca3af;">Check your connection and try again</div>
`;
list.appendChild(errorMsg);
}
} finally {
state.loading = false;
loading.style.display = "none";
}
}
btn.onclick = async (e) => {
e.stopPropagation();
if (state.isOpen) {
closeDropdown();
} else {
openDropdown();
// Always clear and reload to get fresh data
list.innerHTML = "";
state.offset = 0;
state.hasMore = true;
state.loading = false;
await loadMore();
}
};
list.addEventListener("scroll", () => {
if (list.scrollTop + list.clientHeight >= list.scrollHeight - 10) {
loadMore();
}
});
}
// Make the refresh function available globally
window.refreshCurrentWorkflowCard = refreshCurrentWorkflowCard;
// Export the functions
export {
initializeWorkflowsList,
addWorkflowSearch,
workflowsState,
fetchWorkflows,
loadMoreWorkflows,
refreshCurrentWorkflowCard,
};