diff --git a/custom_routes.py b/custom_routes.py
index 70d19e0..849fc92 100644
--- a/custom_routes.py
+++ b/custom_routes.py
@@ -31,6 +31,7 @@ import torch
import psutil
from collections import OrderedDict
import io
+from urllib.parse import urlencode
# Global session
client_session = None
@@ -2993,3 +2994,44 @@ async def create_workflow_version_proxy(request):
return web.json_response(json_data, status=response.status)
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
+
+
+@server.PromptServer.instance.routes.get("/comfyui-deploy/workflows")
+async def get_workflows_proxy(request):
+ api_url = request.rel_url.query.get("api_url", "https://api.comfydeploy.com")
+ search = request.rel_url.query.get("search", "")
+ limit = request.rel_url.query.get("limit", 10)
+ offset = request.rel_url.query.get("offset", 0)
+ auth_header = request.headers.get("Authorization")
+
+ if not auth_header:
+ return web.json_response(
+ {"error": "Authorization header is required"}, status=401
+ )
+
+ # Build query parameters properly
+ params = {}
+ if search:
+ params["search"] = search
+ if limit:
+ params["limit"] = limit
+ if offset:
+ params["offset"] = offset
+
+ target_url = f"{api_url}/api/workflows"
+ if params:
+ target_url += f"?{urlencode(params)}"
+
+ try:
+ await ensure_client_session()
+ async with client_session.get(
+ target_url,
+ headers={
+ "Content-Type": "application/json",
+ "Authorization": auth_header,
+ },
+ ) as response:
+ json_data = await response.json()
+ return web.json_response(json_data, status=response.status)
+ except Exception as e:
+ return web.json_response({"error": str(e)}, status=500)
diff --git a/web-plugin/index.js b/web-plugin/index.js
index cb17bde..7351552 100644
--- a/web-plugin/index.js
+++ b/web-plugin/index.js
@@ -3,6 +3,7 @@ import { api } from "../../scripts/api.js";
// import { LGraphNode } from "../../scripts/widgets.js";
LGraphNode = LiteGraph.LGraphNode;
import { ComfyDialog, $el } from "../../scripts/ui.js";
+import { initializeWorkflowsList, addWorkflowSearch } from "./workflow-list.js";
import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25";
// import { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69";
@@ -1973,7 +1974,7 @@ if (isLocalhost) {
title: "Deploy",
tooltip: "Deploy and Configure",
type: "custom",
- render: (el) => {
+ render: async (el) => {
el.innerHTML = `
Comfy Deploy
@@ -2038,7 +2039,12 @@ if (isLocalhost) {
const workflowsList = el.querySelector("#workflows-list");
const workflowsLoading = el.querySelector("#workflows-loading");
- // refreshWorkflowsList(el);
+ // Initialize workflows list and search
+ const data = getData();
+ if (data.apiKey) {
+ addWorkflowSearch(el, getData, getTimeAgo);
+ await initializeWorkflowsList(el, getData, getTimeAgo);
+ }
},
});
}
diff --git a/web-plugin/workflow-list.js b/web-plugin/workflow-list.js
new file mode 100644
index 0000000..46ce5a2
--- /dev/null
+++ b/web-plugin/workflow-list.js
@@ -0,0 +1,305 @@
+// Workflow list management
+let workflowsState = {
+ workflows: [],
+ offset: 0,
+ limit: 20,
+ loading: false,
+ hasMore: true,
+ initialized: false,
+ currentSearch: "",
+};
+
+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) {
+ const li = document.createElement("li");
+ 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";
+ });
+
+ const updatedDate = new Date(workflow.updated_at);
+ const timeAgo = getTimeAgo(updatedDate);
+
+ li.innerHTML = `
+
+
+ ${
+ workflow.cover_image
+ ? `
+
+

+
+ 📄
+
+
+ `
+ : `
+
+ 📄
+
+ `
+ }
+
+
+
+
+ ${workflow.name}
+
+ ${
+ workflow.pinned
+ ? '📌'
+ : ""
+ }
+
+
+
+
+ Updated ${timeAgo}
+
+
+
+
+
+ `;
+
+ 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);
+ 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");
+
+ if (workflowsState.initialized) return;
+
+ try {
+ // Reset state
+ workflowsState = {
+ workflows: [],
+ offset: 0,
+ limit: 20,
+ loading: false,
+ hasMore: true,
+ initialized: true,
+ currentSearch: "",
+ };
+
+ // 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 - 400px);
+ 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);
+
+ // Show the list
+ workflowsList.style.display = "block";
+ } catch (error) {
+ console.error("Error initializing workflows list:", error);
+ workflowsLoading.innerHTML = `
+
+
Failed to load workflows
+
+
+ `;
+ }
+}
+
+// Search functionality
+function addWorkflowSearch(element, getData, getTimeAgo) {
+ const workflowsContainer = element.querySelector("#workflows-container");
+ const h4 = workflowsContainer.querySelector("h4");
+
+ const searchContainer = document.createElement("div");
+ 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);
+}
+
+// Export the functions
+export {
+ initializeWorkflowsList,
+ addWorkflowSearch,
+ workflowsState,
+ fetchWorkflows,
+ loadMoreWorkflows,
+};