From d73d0f0d2f7519b92bca752b5abfadeea63c297b Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Sat, 17 Feb 2024 02:05:37 +0800 Subject: [PATCH 1/5] pass privacy settings to share api --- ui/src/components/CustomSelector.tsx | 44 +++++++++++++---- ui/src/components/DropdownTitle.tsx | 3 +- ui/src/components/HoverMenu.tsx | 49 ++++++++++++++++++ ui/src/share/ShareDialog.tsx | 74 ++++++---------------------- ui/src/share/SharedTopbarButton.tsx | 69 ++++++++++++++++++++------ ui/src/share/shareUtils.tsx | 45 +++++++++++++++++ ui/src/types/dbTypes.ts | 3 +- 7 files changed, 202 insertions(+), 85 deletions(-) create mode 100644 ui/src/components/HoverMenu.tsx create mode 100644 ui/src/share/shareUtils.tsx diff --git a/ui/src/components/CustomSelector.tsx b/ui/src/components/CustomSelector.tsx index 1261c2c..38d15c1 100644 --- a/ui/src/components/CustomSelector.tsx +++ b/ui/src/components/CustomSelector.tsx @@ -2,21 +2,25 @@ import React, { useState, useRef, useEffect } from "react"; import { Box, Button, Card } from "@chakra-ui/react"; import { IconChevronDown } from "@tabler/icons-react"; -export interface CustomSelectorOption { +export interface CustomSelectorOption { label: string; - value: string; + value: T; icon?: React.ReactElement; } -type Props = { - options: CustomSelectorOption[]; +type Props = { + options: CustomSelectorOption[]; + value: T; + onChange: (value: T) => void; }; -export default function CustomSelector({ options }: Props) { +export default function CustomSelector({ + options, + value, + onChange, +}: Props) { const [isOpen, setIsOpen] = useState(false); const ref = useRef(null); - const [selectedOption, setSelectedOption] = useState< - CustomSelectorOption | undefined - >(options[0]); + const selectedOption = options.find((option) => option.value === value); useEffect(() => { const handleClickOutside = (event: any) => { @@ -41,6 +45,25 @@ export default function CustomSelector({ options }: Props) { > {selectedOption?.label} + {/* {isOpen && ( + + + } + iconSpacing={1} + command={saveShortcut} + > + Save + + + + )} */} {isOpen && ( {options.map((option) => ( + { + window.open(workflowsTable?.curWorkflow?.cloudURL); + }} + aria-label={"Shared"} + size={"xs"} + iconSpacing={"2px"} + leftIcon={} + height={"26px"} + px={1} + > + Public + + } + menuContent={ + + + {cloudURL} + + } + aria-label="copy link" + > + + } + /> ); } diff --git a/ui/src/share/shareUtils.tsx b/ui/src/share/shareUtils.tsx new file mode 100644 index 0000000..4841dfc --- /dev/null +++ b/ui/src/share/shareUtils.tsx @@ -0,0 +1,45 @@ +import { IconLink, IconLock, IconWorld } from "@tabler/icons-react"; +import { CustomSelectorOption } from "../components/CustomSelector"; +import { WorkflowPrivacy } from "../types/dbTypes"; +// @ts-expect-error +import { app } from "/scripts/app.js"; + +export function generateRandomKey(length: number) { + // Generate a random array of bytes + const array = new Uint8Array(length); + window.crypto.getRandomValues(array); + + // Convert the bytes to a hex string + return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join( + "", + ); +} + +export const privacyOptions: CustomSelectorOption[] = [ + { label: "Private", value: "PRIVATE", icon: }, + { + label: "Anyone with the link can access", + value: "UNLISTED", + icon: , + }, + { label: "Public", value: "PUBLIC", icon: }, +]; + +export const getNodeDefs = () => { + const allNodes = app.graph._nodes; + // console.log("allNodes", allNodes); + // console.log( + // "LiteGraph.registered_node_types", + // LiteGraph.registered_node_types, + // ); + const nodeDefs = {}; + for (let n of allNodes) { + // @ts-ignore + if (n.type in LiteGraph.registered_node_types) { + // TODO: js only nodes has no nodeData field, need to handle this.... + // @ts-ignore + nodeDefs[n.type] = LiteGraph.registered_node_types[n.type].nodeData; + } + } + return nodeDefs; +}; diff --git a/ui/src/types/dbTypes.ts b/ui/src/types/dbTypes.ts index 6406f7e..7543c11 100644 --- a/ui/src/types/dbTypes.ts +++ b/ui/src/types/dbTypes.ts @@ -16,7 +16,6 @@ export interface Workflow extends SortableItem { coverMediaPath?: string; cloudID?: string; cloudURL?: string; - privacy?: WorkflowPrivacy; } export interface TableBaseModel { @@ -121,6 +120,6 @@ export enum EFlowOperationType { DELETE = "delete", } -type WorkflowPrivacy = "PUBLIC" | "PRIVATE" | "UNLISTED"; +export type WorkflowPrivacy = "PUBLIC" | "PRIVATE" | "UNLISTED"; export const WORKSPACE_INDEXDB_NAME = "comfyui_workspace_db"; From 16a53b7064d5b7b2a5b85934013ed582053d5ee8 Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Sat, 17 Feb 2024 15:42:59 +0800 Subject: [PATCH 2/5] radio button select versions --- ui/src/components/DropdownTitle.tsx | 3 +- ui/src/share/ShareDialog.tsx | 129 ++++++++++++++++++---------- ui/src/share/SharedTopbarButton.tsx | 9 +- 3 files changed, 87 insertions(+), 54 deletions(-) diff --git a/ui/src/components/DropdownTitle.tsx b/ui/src/components/DropdownTitle.tsx index fa9cf38..3067aa1 100644 --- a/ui/src/components/DropdownTitle.tsx +++ b/ui/src/components/DropdownTitle.tsx @@ -135,7 +135,8 @@ export default function DropdownTitle() { height={"29px"} aria-label="menu" size={"sm"} - colorScheme="teal" + colorScheme="gray" + backgroundColor={"gray.700"} onClick={onOpen} onMouseEnter={onOpen} onMouseLeave={delayedClose} diff --git a/ui/src/share/ShareDialog.tsx b/ui/src/share/ShareDialog.tsx index e9be3df..fc68cdd 100644 --- a/ui/src/share/ShareDialog.tsx +++ b/ui/src/share/ShareDialog.tsx @@ -1,14 +1,20 @@ import { + Box, Button, Flex, HStack, Input, + InputGroup, + InputLeftAddon, + InputRightAddon, Link, Modal, ModalBody, ModalContent, ModalHeader, ModalOverlay, + Radio, + RadioGroup, Stack, Text, useToast, @@ -16,7 +22,13 @@ import { import { Workflow, WorkflowPrivacy, WorkflowVersion } from "../types/dbTypes"; import { useEffect, useRef, useState } from "react"; import CustomSelector from "../components/CustomSelector"; -import { IconCloud, IconCopy, IconExternalLink } from "@tabler/icons-react"; +import { + IconCheck, + IconCloud, + IconCopy, + IconExternalLink, + IconPlus, +} from "@tabler/icons-react"; import { userSettingsTable, workflowVersionsTable, @@ -27,6 +39,7 @@ import { api } from "/scripts/api.js"; // @ts-ignore import { app } from "/scripts/app.js"; import { generateRandomKey, getNodeDefs, privacyOptions } from "./shareUtils"; +import { formatTimestamp } from "../utils"; interface Props { onClose: () => void; @@ -40,6 +53,9 @@ export default function ShareDialog({ onClose }: Props) { const [loading, setLoading] = useState(false); const [cloudHost, setCloudHost] = useState(""); const [privacy, setPrivacy] = useState("PRIVATE"); + const [selectedVersion, setSelectedVersion] = useState< + string | "new_version" + >("new_version"); const cloudHostRef = useRef(""); const [workflow, setWorkflow] = useState(); const toast = useToast(); @@ -69,6 +85,8 @@ export default function ShareDialog({ onClose }: Props) { cloudID: cloudVersionID, })); loadData(); + window.open(cloudHost + "/workflow/" + cloudID, "_blank"); + setLoading(false); }; useEffect(() => { loadData(); @@ -116,14 +134,19 @@ export default function ShareDialog({ onClose }: Props) { ? ((await userSettingsTable?.getSetting("cloudHost")) as string) : "http://localhost:3000"; const nodeDefs = getNodeDefs(); - const newVer = await workflowVersionsTable?.add({ - workflowID: workflow!.id, - name: versionName, - createTime: Date.now(), - json: JSON.stringify(app.graph.serialize()), - }); - if (!newVer) { - alert("Failed to create new version, please try again."); + let version: WorkflowVersion | undefined; + if (selectedVersion === "new_version") { + version = await workflowVersionsTable?.add({ + workflowID: workflow!.id, + name: versionName, + createTime: Date.now(), + json: JSON.stringify(app.graph.serialize()), + }); + } else { + version = await workflowVersionsTable?.get(selectedVersion); + } + if (!version) { + alert(`Failed to find version: ${selectedVersion}, please try again.`); setLoading(false); return; } @@ -137,16 +160,14 @@ export default function ShareDialog({ onClose }: Props) { "Share Workflow", "width=800,height=800", ); - setLoading(false); const handleChildReady = (event: MessageEvent) => { if (event.origin === host && event.data === "child_ready") { const curWorkflow = workflowsTable?.curWorkflow; // Send data to the new window after it loads - console.log("sending privacy", privacy); sharePopup!.postMessage( { workflow: curWorkflow, - version: newVer, + version: version, nodeDefs: nodeDefs, privacy: privacy, }, @@ -203,40 +224,58 @@ export default function ShareDialog({ onClose }: Props) { )} - - New Version - { - setVersionName(e.target.value); - }} - /> - - {localVersions.slice(0, 4).map((ver) => { - return ( - - {ver.name} - {ver.cloudID && ( - - - - )} + Choose a version to share: + { + setSelectedVersion(val); + }} + > + + + { + setVersionName(e.target.value); + }} + /> + + + New version + - ); - })} + + {localVersions.slice(0, 4).map((ver) => { + return ( + + + {ver.name} + {formatTimestamp(ver.createTime)} + {ver.cloudID && ( + + + + )} + + + ); + })} + - - )} - - - ); - })} + + + )} + + + ); + })} + diff --git a/ui/src/utils.tsx b/ui/src/utils.tsx index 85a948d..ff7c8f7 100644 --- a/ui/src/utils.tsx +++ b/ui/src/utils.tsx @@ -106,6 +106,7 @@ function isValidFileName(fileName: string) { export function formatTimestamp( unixTimestamp: number, + showHourMinue: boolean = true, showSec: boolean = false, ) { // Create a new Date object from the UNIX timestamp @@ -119,7 +120,10 @@ export function formatTimestamp( const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); // Format the date and time string - const res = `${month}-${day}-${year} ${hours}:${minutes}`; + const res = `${month}-${day}-${year}`; + if (showHourMinue) { + return res + ` ${hours}:${minutes}`; + } if (showSec) { return res + `:${seconds}`; } From ee00b34f9b2ce16cab9c2f5893a8266bf70140e5 Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Sat, 17 Feb 2024 17:46:48 +0800 Subject: [PATCH 4/5] fix redirect to url --- ui/src/share/ShareDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/share/ShareDialog.tsx b/ui/src/share/ShareDialog.tsx index 1887d67..e2d9ef1 100644 --- a/ui/src/share/ShareDialog.tsx +++ b/ui/src/share/ShareDialog.tsx @@ -85,7 +85,7 @@ export default function ShareDialog({ onClose }: Props) { cloudID: cloudVersionID, })); loadData(); - window.open(cloudHost + "/workflow/" + cloudID, "_blank"); + window.open(cloudHostRef.current + "/workflow/" + cloudID, "_blank"); setLoading(false); }; useEffect(() => { From 96473555b79a726070dc2e9548a46a75ee335649 Mon Sep 17 00:00:00 2001 From: Weixuan Fu Date: Tue, 20 Feb 2024 00:23:17 +0800 Subject: [PATCH 5/5] fix sharedialog radio picker --- ui/src/share/ShareDialog.tsx | 47 +++++-------------- .../share/ShareDialogWorkflowVersionRadio.tsx | 46 ++++++++++++++++++ ui/src/share/SharedTopbarButton.tsx | 2 + ui/src/types/dbTypes.ts | 1 + 4 files changed, 62 insertions(+), 34 deletions(-) create mode 100644 ui/src/share/ShareDialogWorkflowVersionRadio.tsx diff --git a/ui/src/share/ShareDialog.tsx b/ui/src/share/ShareDialog.tsx index e2d9ef1..134a7e5 100644 --- a/ui/src/share/ShareDialog.tsx +++ b/ui/src/share/ShareDialog.tsx @@ -22,13 +22,7 @@ import { import { Workflow, WorkflowPrivacy, WorkflowVersion } from "../types/dbTypes"; import { useEffect, useRef, useState } from "react"; import CustomSelector from "../components/CustomSelector"; -import { - IconCheck, - IconCloud, - IconCopy, - IconExternalLink, - IconPlus, -} from "@tabler/icons-react"; +import { IconCloud, IconCopy, IconExternalLink } from "@tabler/icons-react"; import { userSettingsTable, workflowVersionsTable, @@ -40,6 +34,7 @@ import { api } from "/scripts/api.js"; import { app } from "/scripts/app.js"; import { generateRandomKey, getNodeDefs, privacyOptions } from "./shareUtils"; import { formatTimestamp } from "../utils"; +import ShareDialogWorkflowVersionRadio from "./ShareDialogWorkflowVersionRadio"; interface Props { onClose: () => void; @@ -77,15 +72,19 @@ export default function ShareDialog({ onClose }: Props) { localID && (await workflowsTable?.updateFlow(localID, { cloudID: cloudID, - cloudURL: cloudHost + "/workflow/" + cloudID, + cloudURL: cloudHostRef.current + "/workflow/" + cloudID, })); localVerID && cloudVersionID && (await workflowVersionsTable?.update(localVerID, { cloudID: cloudVersionID, + cloudURL: cloudHostRef.current + "/workflow_ver/" + cloudVersionID, })); loadData(); - window.open(cloudHostRef.current + "/workflow/" + cloudID, "_blank"); + window.open( + cloudHostRef.current + "/workflow_ver/" + cloudVersionID, + "_blank", + ); setLoading(false); }; useEffect(() => { @@ -252,31 +251,11 @@ export default function ShareDialog({ onClose }: Props) { {localVersions.slice(0, 4).map((ver) => { return ( - - - {ver.name} - - {formatTimestamp(ver.createTime, false)} - - {ver.cloudID && ( - - - - )} - - + ); })} diff --git a/ui/src/share/ShareDialogWorkflowVersionRadio.tsx b/ui/src/share/ShareDialogWorkflowVersionRadio.tsx new file mode 100644 index 0000000..0760124 --- /dev/null +++ b/ui/src/share/ShareDialogWorkflowVersionRadio.tsx @@ -0,0 +1,46 @@ +import { Button, HStack, Radio, Text } from "@chakra-ui/react"; +import { WorkflowVersion } from "../types/dbTypes"; +import { formatTimestamp } from "../utils"; +import { IconCloud, IconExternalLink } from "@tabler/icons-react"; + +export default function ShareDialogWorkflowVersionRadio({ + version, + cloudHost, +}: { + version: WorkflowVersion; + cloudHost: string; +}) { + const ver = version; + const content = ( + + {ver.name} + {formatTimestamp(ver.createTime, false)} + {ver.cloudID && ( + + + + )} + + ); + if (!version.cloudID) { + return ( + + {content} + + ); + } + + return <>{content}; +} diff --git a/ui/src/share/SharedTopbarButton.tsx b/ui/src/share/SharedTopbarButton.tsx index 8d789a4..5bc9be9 100644 --- a/ui/src/share/SharedTopbarButton.tsx +++ b/ui/src/share/SharedTopbarButton.tsx @@ -14,6 +14,8 @@ export function SharedTopbarButton({}) { workflowsTable?.get(curFlowID).then((flow) => { if (flow?.cloudID) { setCloudURL(flow.cloudURL); + } else { + setCloudURL(undefined); } }); } diff --git a/ui/src/types/dbTypes.ts b/ui/src/types/dbTypes.ts index 7543c11..968b1a9 100644 --- a/ui/src/types/dbTypes.ts +++ b/ui/src/types/dbTypes.ts @@ -59,6 +59,7 @@ export type WorkflowVersion = { json: string; createTime: number; cloudID?: string; + cloudURL?: string; nodeDefs?: string; //for cloud workflow version };