diff --git a/package-lock.json b/package-lock.json index 5a6b00e84..27110c150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "react-dom": "^18.1.0", "react-hotkeys-hook": "^3.4.6", "react-i18next": "^12.0.0", - "react-icons": "^4.7.1", + "react-icons": "^4.10.1", "react-markdown": "^8.0.3", "react-query": "^3.39.3", "react-time-ago": "^7.2.1", @@ -21011,9 +21011,9 @@ } }, "node_modules/react-icons": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz", - "integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", + "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", "peerDependencies": { "react": "*" } @@ -41042,9 +41042,9 @@ } }, "react-icons": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz", - "integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.10.1.tgz", + "integrity": "sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==", "requires": {} }, "react-is": { diff --git a/package.json b/package.json index e6eea3d2f..4f81911a2 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "react-dom": "^18.1.0", "react-hotkeys-hook": "^3.4.6", "react-i18next": "^12.0.0", - "react-icons": "^4.7.1", + "react-icons": "^4.10.1", "react-markdown": "^8.0.3", "react-query": "^3.39.3", "react-time-ago": "^7.2.1", diff --git a/src/common/common-types.ts b/src/common/common-types.ts index 275291c47..766b16994 100644 --- a/src/common/common-types.ts +++ b/src/common/common-types.ts @@ -6,6 +6,7 @@ export interface JsonObject { export type JsonValue = null | string | number | boolean | JsonObject | JsonValue[]; export type Mutable = { -readonly [P in keyof T]: T[P] }; +export type PartialBy = Omit & Partial>; export interface Size { width: number; diff --git a/src/common/locales/en/translation.json b/src/common/locales/en/translation.json index ccf03f620..bac3e8d3f 100644 --- a/src/common/locales/en/translation.json +++ b/src/common/locales/en/translation.json @@ -32,6 +32,7 @@ "openInFileExplorer": "Open in File Explorer", "selectDirectory": "Select a directory..." }, + "extractValueIntoNode": "Extract value into node", "file": { "copyFileName": "Copy File Name", "copyFullFilePath": "Copy Full File Path", diff --git a/src/renderer/components/inputs/DirectoryInput.tsx b/src/renderer/components/inputs/DirectoryInput.tsx index 0eb658a8c..59d29559f 100644 --- a/src/renderer/components/inputs/DirectoryInput.tsx +++ b/src/renderer/components/inputs/DirectoryInput.tsx @@ -16,6 +16,7 @@ import { BsFolderPlus } from 'react-icons/bs'; import { MdContentCopy, MdFolder } from 'react-icons/md'; import { ipcRenderer } from '../../../common/safeIpc'; import { useContextMenu } from '../../hooks/useContextMenu'; +import { useInputRefactor } from '../../hooks/useInputRefactor'; import { useLastDirectory } from '../../hooks/useLastDirectory'; import { CopyOverrideIdSection } from './elements/CopyOverrideIdSection'; import { MaybeLabel } from './InputContainer'; @@ -65,6 +66,8 @@ export const DirectoryInput = memo( const displayDirectory = isConnected ? getDirectoryPath(inputType) : value; + const refactor = useInputRefactor(nodeId, input, value, isConnected); + const menu = useContextMenu(() => ( {t('inputs.directory.copyFullDirectoryPath', 'Copy Full Directory Path')} + {refactor} ( @@ -77,6 +79,7 @@ export const NumberInput = memo( > {t('inputs.number.paste', 'Paste')} + {refactor} ( @@ -165,6 +167,7 @@ export const SliderInput = memo( > {t('inputs.number.paste', 'Paste')} + {refactor} ( @@ -110,6 +112,7 @@ export const TextInput = memo( > {t('inputs.text.paste', 'Paste')} + {refactor} = Omit & Partial>; +import { Input, InputKind, OfKind, PartialBy, SchemaId, Size } from '../../../common/common-types'; export interface InputProps { readonly value: Value | undefined; diff --git a/src/renderer/components/outputs/DefaultImageOutput.tsx b/src/renderer/components/outputs/DefaultImageOutput.tsx index 823b32df0..fdffc577b 100644 --- a/src/renderer/components/outputs/DefaultImageOutput.tsx +++ b/src/renderer/components/outputs/DefaultImageOutput.tsx @@ -4,7 +4,7 @@ import { BsEyeFill } from 'react-icons/bs'; import { useReactFlow } from 'reactflow'; import { useContext } from 'use-context-selector'; import { EdgeData, InputId, NodeData, SchemaId } from '../../../common/common-types'; -import { createUniqueId, stringifySourceHandle, stringifyTargetHandle } from '../../../common/util'; +import { createUniqueId, stringifySourceHandle } from '../../../common/util'; import { GlobalContext } from '../../contexts/GlobalNodeState'; import { TypeTags } from '../TypeTag'; import { OutputProps } from './props'; @@ -12,7 +12,7 @@ import { OutputProps } from './props'; const VIEW_SCHEMA_ID = 'chainner:image:view' as SchemaId; export const DefaultImageOutput = memo(({ output, id, schema, type }: OutputProps) => { - const { selectNode, createNode, createConnection } = useContext(GlobalContext); + const { selectNode, createNode, createEdge } = useContext(GlobalContext); const { getNodes, getEdges } = useReactFlow(); return ( @@ -78,15 +78,10 @@ export const DefaultImageOutput = memo(({ output, id, schema, type }: OutputProp }, containingNode.parentNode ); - createConnection({ - source: id, - sourceHandle, - target: nodeId, - targetHandle: stringifyTargetHandle({ - nodeId, - inputId: 0 as InputId, - }), - }); + createEdge( + { nodeId: id, outputId: output.id }, + { nodeId, inputId: 0 as InputId } + ); } }} > diff --git a/src/renderer/contexts/GlobalNodeState.tsx b/src/renderer/contexts/GlobalNodeState.tsx index de2a0aa78..7c8d7fc38 100644 --- a/src/renderer/contexts/GlobalNodeState.tsx +++ b/src/renderer/contexts/GlobalNodeState.tsx @@ -38,12 +38,16 @@ import { import { withoutNull } from '../../common/types/util'; import { EMPTY_SET, + ParsedSourceHandle, + ParsedTargetHandle, createUniqueId, deepCopy, deriveUniqueId, lazy, parseSourceHandle, parseTargetHandle, + stringifySourceHandle, + stringifyTargetHandle, } from '../../common/util'; import { VALID, Validity, invalid } from '../../common/Validity'; import { @@ -122,6 +126,7 @@ interface Global { animate: (nodeIdsToAnimate: Iterable, animateEdges?: boolean) => void; unAnimate: (nodeIdsToAnimate?: Iterable) => void; createNode: (proto: NodeProto, parentId?: string) => void; + createEdge: (from: ParsedSourceHandle, to: ParsedTargetHandle) => void; createConnection: (connection: Connection) => void; setNodeInputValue: (nodeId: string, inputId: InputId, value: T) => void; setNodeInputSize: (nodeId: string, inputId: InputId, value: Readonly) => void; @@ -799,6 +804,17 @@ export const GlobalProvider = memo( }, [changeEdges] ); + const createEdge = useCallback( + (from: ParsedSourceHandle, to: ParsedTargetHandle): void => { + createConnection({ + source: from.nodeId, + sourceHandle: stringifySourceHandle(from), + target: to.nodeId, + targetHandle: stringifyTargetHandle(to), + }); + }, + [createConnection] + ); const releaseNodeFromParent = useCallback( (id: string) => { @@ -1351,6 +1367,7 @@ export const GlobalProvider = memo( animate, unAnimate, createNode, + createEdge, createConnection, setNodeInputValue, setNodeInputSize, diff --git a/src/renderer/hooks/useInputRefactor.tsx b/src/renderer/hooks/useInputRefactor.tsx new file mode 100644 index 000000000..33a9ffe5b --- /dev/null +++ b/src/renderer/hooks/useInputRefactor.tsx @@ -0,0 +1,102 @@ +import { MenuDivider, MenuItem } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { CgArrowsExpandUpLeft } from 'react-icons/cg'; +import { useReactFlow } from 'reactflow'; +import { useContext } from 'use-context-selector'; +import { + EdgeData, + Input, + InputId, + InputKind, + InputValue, + NodeData, + OutputId, + PartialBy, + SchemaId, +} from '../../common/common-types'; +import { createUniqueId } from '../../common/util'; +import { BackendContext } from '../contexts/BackendContext'; +import { GlobalContext } from '../contexts/GlobalNodeState'; + +const valueNodeMap = { + number: 'chainner:utility:number' as SchemaId, + slider: 'chainner:utility:number' as SchemaId, + text: 'chainner:utility:text' as SchemaId, + directory: 'chainner:utility:directory' as SchemaId, +} as const satisfies Partial>; + +export const useInputRefactor = ( + nodeId: string | undefined, + input: Omit, 'type' | 'conversion'>, + value: InputValue, + isConnected: boolean +): JSX.Element | null => { + const { t } = useTranslation(); + const { createNode, createEdge } = useContext(GlobalContext); + const { schemata } = useContext(BackendContext); + const { getNode } = useReactFlow(); + + const inputId = input.id; + if (nodeId === undefined || inputId === undefined) { + return null; + } + + const refactoringOptions: JSX.Element[] = []; + const specificInput = input as Input; + + if ( + specificInput.hasHandle && + ((specificInput.kind === 'text' && !specificInput.multiline) || + specificInput.kind === 'number' || + specificInput.kind === 'slider' || + specificInput.kind === 'directory') + ) { + refactoringOptions.push( + } + isDisabled={isConnected || value === undefined} + key="extract text" + onClick={() => { + const containingNode = getNode(nodeId); + const valueNodeId = createUniqueId(); + + let inputIndex = 0; + if (containingNode) { + const schema = schemata.get(containingNode.data.schemaId); + inputIndex = schema.inputs.findIndex((i) => i.id === inputId); + } + + createNode( + { + id: valueNodeId, + position: { + x: (containingNode?.position.x ?? 0) - 300, + y: (containingNode?.position.y ?? 0) - 30 + inputIndex * 50, + }, + data: { + schemaId: valueNodeMap[specificInput.kind], + inputData: { [0 as InputId]: value }, + }, + nodeType: 'regularNode', + }, + containingNode?.parentNode + ); + createEdge( + { nodeId: valueNodeId, outputId: 0 as OutputId }, + { nodeId, inputId } + ); + }} + > + {t('inputs.extractValueIntoNode', 'Extract value into node')} + + ); + } + + if (refactoringOptions.length === 0) return null; + return ( + <> + + {refactoringOptions} + + ); +};