From 6a62854e7dc1ef3836ffd69135a40c9e62e5d0fc Mon Sep 17 00:00:00 2001
From: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
Date: Wed, 28 Aug 2024 21:32:10 +1000
Subject: [PATCH] feat(ui): migrate add node popover to cmdk
Put this together as a way to figure out the library before moving on to the full app cmdk. Works great.
---
invokeai/frontend/web/package.json | 2 +-
invokeai/frontend/web/pnpm-lock.yaml | 329 +++++++++++++-
.../features/nodes/components/NodeEditor.tsx | 4 +-
.../flow/AddNodeCmdk/AddNodeCmdk.tsx | 420 ++++++++++++++++++
.../flow/AddNodePopover/AddNodePopover.tsx | 267 -----------
.../features/nodes/components/flow/Flow.tsx | 4 +-
.../flow/panels/TopPanel/AddNodeButton.tsx | 5 +-
.../src/features/nodes/hooks/useConnection.ts | 4 +-
.../src/features/nodes/store/nodesSlice.ts | 11 +-
9 files changed, 755 insertions(+), 291 deletions(-)
create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx
delete mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx
diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json
index 4fdb81ea67..4e98e28770 100644
--- a/invokeai/frontend/web/package.json
+++ b/invokeai/frontend/web/package.json
@@ -64,6 +64,7 @@
"@roarr/browser-log-writer": "^1.3.0",
"async-mutex": "^0.5.0",
"chakra-react-select": "^4.9.1",
+ "cmdk": "^1.0.0",
"compare-versions": "^6.1.1",
"dateformat": "^5.0.3",
"fracturedjsonjs": "^4.0.2",
@@ -92,7 +93,6 @@
"react-icons": "^5.2.1",
"react-redux": "9.1.2",
"react-resizable-panels": "^2.0.23",
- "react-select": "5.8.0",
"react-use": "^17.5.1",
"react-virtuoso": "^4.9.0",
"reactflow": "^11.11.4",
diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml
index a487b8d6f3..2dad5456f4 100644
--- a/invokeai/frontend/web/pnpm-lock.yaml
+++ b/invokeai/frontend/web/pnpm-lock.yaml
@@ -41,6 +41,9 @@ dependencies:
chakra-react-select:
specifier: ^4.9.1
version: 4.9.1(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/layout@2.3.1)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@emotion/react@11.13.3)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ cmdk:
+ specifier: ^1.0.0
+ version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
compare-versions:
specifier: ^6.1.1
version: 6.1.1
@@ -125,9 +128,6 @@ dependencies:
react-resizable-panels:
specifier: ^2.0.23
version: 2.0.23(react-dom@18.3.1)(react@18.3.1)
- react-select:
- specifier: 5.8.0
- version: 5.8.0(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
react-use:
specifier: ^17.5.1
version: 17.5.1(react-dom@18.3.1)(react@18.3.1)
@@ -2053,7 +2053,7 @@ packages:
dependencies:
'@chakra-ui/dom-utils': 2.1.0
react: 18.3.1
- react-focus-lock: 2.12.1(@types/react@18.3.3)(react@18.3.1)
+ react-focus-lock: 2.13.0(@types/react@18.3.3)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
dev: false
@@ -3784,6 +3784,288 @@ packages:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
+ /@radix-ui/primitive@1.0.1:
+ resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
+ dependencies:
+ '@babel/runtime': 7.25.4
+ dev: false
+
+ /@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ aria-hidden: 1.2.4
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@radix-ui/primitive': 1.0.1
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ '@types/react-dom': 18.3.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ dev: false
+
+ /@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1)
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
+ /@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@types/react': 18.3.3
+ react: 18.3.1
+ dev: false
+
/@reactflow/background@11.3.14(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
peerDependencies:
@@ -5210,7 +5492,6 @@ packages:
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
dependencies:
'@types/react': 18.3.3
- dev: true
/@types/react-transition-group@4.4.10:
resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
@@ -6268,6 +6549,21 @@ packages:
requiresBuild: true
dev: true
+ /cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ dependencies:
+ '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ transitivePeerDependencies:
+ - '@types/react'
+ - '@types/react-dom'
+ dev: false
+
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
@@ -9712,8 +10008,8 @@ packages:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
dev: false
- /react-focus-lock@2.12.1(@types/react@18.3.3)(react@18.3.1):
- resolution: {integrity: sha512-lfp8Dve4yJagkHiFrC1bGtib3mF2ktqwPJw4/WGcgPW+pJ/AVQA5X2vI7xgp13FcxFEpYBBHpXai/N2DBNC0Jw==}
+ /react-focus-lock@2.13.0(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-w7aIcTwZwNzUp2fYQDMICy+khFwVmKmOrLF8kNsPS+dz4Oi/oxoVJ2wCMVvX6rWGriM/+mYaTyp1MRmkcs2amw==}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -9867,6 +10163,25 @@ packages:
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
dev: false
+ /react-remove-scroll@2.5.5(@types/react@18.3.3)(react@18.3.1):
+ resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ dependencies:
+ '@types/react': 18.3.3
+ react: 18.3.1
+ react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1)
+ react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
+ tslib: 2.7.0
+ use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1)
+ use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
+ dev: false
+
/react-resizable-panels@2.0.23(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-8ZKTwTU11t/FYwiwhMdtZYYyFxic5U5ysRu2YwfkAgDbUJXFvnWSJqhnzkSlW+mnDoNAzDCrJhdOSXBPA76wug==}
peerDependencies:
diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
index 27e2006b08..18ac2abdc4 100644
--- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx
@@ -2,6 +2,7 @@ import 'reactflow/dist/style.css';
import { Flex } from '@invoke-ai/ui-library';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk';
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
@@ -10,7 +11,6 @@ import { useTranslation } from 'react-i18next';
import { MdDeviceHub } from 'react-icons/md';
import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo';
-import AddNodePopover from './flow/AddNodePopover/AddNodePopover';
import { Flow } from './flow/Flow';
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
@@ -31,7 +31,7 @@ const NodeEditor = () => {
{data && (
<>
-
+
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx
new file mode 100644
index 0000000000..207b7cf0aa
--- /dev/null
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx
@@ -0,0 +1,420 @@
+import type { SystemStyleObject } from '@invoke-ai/ui-library';
+import {
+ Box,
+ Flex,
+ Icon,
+ Input,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalOverlay,
+ Spacer,
+ Text,
+} from '@invoke-ai/ui-library';
+import { useStore } from '@nanostores/react';
+import { useAppStore } from 'app/store/storeHooks';
+import { CommandEmpty, CommandItem, CommandList, CommandRoot } from 'cmdk';
+import { IAINoContentFallback } from 'common/components/IAIImageFallback';
+import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
+import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
+import {
+ $addNodeCmdk,
+ $cursorPos,
+ $edgePendingUpdate,
+ $pendingConnection,
+ $templates,
+ edgesChanged,
+ nodesChanged,
+ useAddNodeCmdk,
+} from 'features/nodes/store/nodesSlice';
+import { selectNodesSlice } from 'features/nodes/store/selectors';
+import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
+import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
+import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
+import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
+import { isInvocationNode } from 'features/nodes/types/invocation';
+import { toast } from 'features/toast/toast';
+import { memoize } from 'lodash-es';
+import { computed } from 'nanostores';
+import type { ChangeEvent } from 'react';
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useHotkeys } from 'react-hotkeys-hook';
+import { useTranslation } from 'react-i18next';
+import { PiFlaskBold, PiHammerBold } from 'react-icons/pi';
+import type { EdgeChange, NodeChange } from 'reactflow';
+import type { S } from 'services/api/types';
+
+const useThrottle = (value: T, limit: number) => {
+ const [throttledValue, setThrottledValue] = useState(value);
+ const lastRan = useRef(Date.now());
+
+ useEffect(() => {
+ const handler = setTimeout(
+ function () {
+ if (Date.now() - lastRan.current >= limit) {
+ setThrottledValue(value);
+ lastRan.current = Date.now();
+ }
+ },
+ limit - (Date.now() - lastRan.current)
+ );
+
+ return () => {
+ clearTimeout(handler);
+ };
+ }, [value, limit]);
+
+ return throttledValue;
+};
+
+const useAddNode = () => {
+ const { t } = useTranslation();
+ const store = useAppStore();
+ const buildInvocation = useBuildNode();
+ const templates = useStore($templates);
+ const pendingConnection = useStore($pendingConnection);
+
+ const addNode = useCallback(
+ (nodeType: string): void => {
+ const node = buildInvocation(nodeType);
+ if (!node) {
+ const errorMessage = t('nodes.unknownNode', {
+ nodeType: nodeType,
+ });
+ toast({
+ status: 'error',
+ title: errorMessage,
+ });
+ return;
+ }
+
+ // Find a cozy spot for the node
+ const cursorPos = $cursorPos.get();
+ const { nodes, edges } = selectNodesSlice(store.getState());
+ node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y);
+ node.selected = true;
+
+ // Deselect all other nodes and edges
+ const nodeChanges: NodeChange[] = [{ type: 'add', item: node }];
+ const edgeChanges: EdgeChange[] = [];
+ nodes.forEach(({ id, selected }) => {
+ if (selected) {
+ nodeChanges.push({ type: 'select', id, selected: false });
+ }
+ });
+ edges.forEach(({ id, selected }) => {
+ if (selected) {
+ edgeChanges.push({ type: 'select', id, selected: false });
+ }
+ });
+
+ // Onwards!
+ if (nodeChanges.length > 0) {
+ store.dispatch(nodesChanged(nodeChanges));
+ }
+ if (edgeChanges.length > 0) {
+ store.dispatch(edgesChanged(edgeChanges));
+ }
+
+ // Auto-connect an edge if we just added a node and have a pending connection
+ if (pendingConnection && isInvocationNode(node)) {
+ const edgePendingUpdate = $edgePendingUpdate.get();
+ const { handleType } = pendingConnection;
+
+ const source = handleType === 'source' ? pendingConnection.nodeId : node.id;
+ const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null;
+ const target = handleType === 'target' ? pendingConnection.nodeId : node.id;
+ const targetHandle = handleType === 'target' ? pendingConnection.handleId : null;
+
+ const { nodes, edges } = selectNodesSlice(store.getState());
+ const connection = getFirstValidConnection(
+ source,
+ sourceHandle,
+ target,
+ targetHandle,
+ nodes,
+ edges,
+ templates,
+ edgePendingUpdate
+ );
+ if (connection) {
+ const newEdge = connectionToEdge(connection);
+ store.dispatch(edgesChanged([{ type: 'add', item: newEdge }]));
+ }
+ }
+ },
+ [buildInvocation, pendingConnection, store, t, templates]
+ );
+
+ return addNode;
+};
+
+const cmdkRootSx: SystemStyleObject = {
+ '[cmdk-root]': {
+ w: 'full',
+ h: 'full',
+ },
+ '[cmdk-list]': {
+ w: 'full',
+ h: 'full',
+ },
+};
+
+export const AddNodeCmdk = memo(() => {
+ const { t } = useTranslation();
+ const addNodeCmdk = useAddNodeCmdk();
+ const addNodeCmdkIsOpen = useStore(addNodeCmdk.$boolean);
+ const inputRef = useRef(null);
+ const [searchTerm, setSearchTerm] = useState('');
+ const addNode = useAddNode();
+ const throttledSearchTerm = useThrottle(searchTerm, 100);
+
+ useHotkeys(['shift+a', 'space'], addNodeCmdk.setTrue, { preventDefault: true });
+
+ const onChange = useCallback((e: ChangeEvent) => {
+ setSearchTerm(e.target.value);
+ }, []);
+
+ const onSelect = useCallback(
+ (value: string) => {
+ addNode(value);
+ $addNodeCmdk.set(false);
+ setSearchTerm('');
+ },
+ [addNode]
+ );
+
+ const onClose = useCallback(() => {
+ addNodeCmdk.setFalse();
+ setSearchTerm('');
+ $pendingConnection.set(null);
+ }, [addNodeCmdk]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+AddNodeCmdk.displayName = 'AddNodeCmdk';
+
+const cmdkItemSx: SystemStyleObject = {
+ '&[data-selected="true"]': {
+ bg: 'base.700',
+ },
+};
+
+type NodeCommandItemData = {
+ value: string;
+ label: string;
+ description: string;
+ classification: S['Classification'];
+ nodePack: string;
+};
+
+const $templatesArray = computed($templates, (templates) => Object.values(templates));
+
+const createRegex = memoize(
+ (inputValue: string) =>
+ new RegExp(
+ inputValue
+ .trim()
+ .replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
+ .split(' ')
+ .join('.*'),
+ 'gi'
+ )
+);
+
+// Filterable items are a subset of Invocation template - we also want to filter for notes or current image node,
+// so we are using a less specific type instead of `InvocationTemplate`
+type FilterableItem = {
+ type: string;
+ title: string;
+ description: string;
+ tags: string[];
+ classification: S['Classification'];
+ nodePack: string;
+};
+
+const filter = memoize(
+ (item: FilterableItem, searchTerm: string) => {
+ const regex = createRegex(searchTerm);
+
+ if (!searchTerm) {
+ return true;
+ }
+
+ if (item.title.includes(searchTerm) || regex.test(item.title)) {
+ return true;
+ }
+
+ if (item.type.includes(searchTerm) || regex.test(item.type)) {
+ return true;
+ }
+
+ if (item.description.includes(searchTerm) || regex.test(item.description)) {
+ return true;
+ }
+
+ if (item.nodePack.includes(searchTerm) || regex.test(item.nodePack)) {
+ return true;
+ }
+
+ if (item.classification.includes(searchTerm) || regex.test(item.classification)) {
+ return true;
+ }
+
+ for (const tag of item.tags) {
+ if (tag.includes(searchTerm) || regex.test(tag)) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+ (item: FilterableItem, searchTerm: string) => `${item.type}-${searchTerm}`
+);
+
+const NodeCommandList = memo(({ searchTerm, onSelect }: { searchTerm: string; onSelect: (value: string) => void }) => {
+ const { t } = useTranslation();
+ const templatesArray = useStore($templatesArray);
+ const pendingConnection = useStore($pendingConnection);
+ const currentImageFilterItem = useMemo(
+ () => ({
+ type: 'current_image',
+ title: t('nodes.currentImage'),
+ description: t('nodes.currentImageDescription'),
+ tags: ['progress', 'image', 'current'],
+ classification: 'stable',
+ nodePack: 'invokeai',
+ }),
+ [t]
+ );
+ const notesFilterItem = useMemo(
+ () => ({
+ type: 'notes',
+ title: t('nodes.notes'),
+ description: t('nodes.notesDescription'),
+ tags: ['notes'],
+ classification: 'stable',
+ nodePack: 'invokeai',
+ }),
+ [t]
+ );
+
+ const items = useMemo(() => {
+ // If we have a connection in progress, we need to filter the node choices
+ const _items: NodeCommandItemData[] = [];
+
+ if (!pendingConnection) {
+ for (const template of templatesArray) {
+ if (filter(template, searchTerm)) {
+ _items.push({
+ label: template.title,
+ value: template.type,
+ description: template.description,
+ classification: template.classification,
+ nodePack: template.nodePack,
+ });
+ }
+ }
+
+ for (const item of [currentImageFilterItem, notesFilterItem]) {
+ if (filter(item, searchTerm)) {
+ _items.push({
+ label: item.title,
+ value: item.type,
+ description: item.description,
+ classification: item.classification,
+ nodePack: item.nodePack,
+ });
+ }
+ }
+ } else {
+ for (const template of templatesArray) {
+ if (filter(template, searchTerm)) {
+ const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs;
+
+ for (const field of Object.values(candidateFields)) {
+ const sourceType =
+ pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type;
+ const targetType =
+ pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type;
+
+ if (validateConnectionTypes(sourceType, targetType)) {
+ _items.push({
+ label: template.title,
+ value: template.type,
+ description: template.description,
+ classification: template.classification,
+ nodePack: template.nodePack,
+ });
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return _items;
+ }, [pendingConnection, currentImageFilterItem, searchTerm, notesFilterItem, templatesArray]);
+
+ return (
+ <>
+ {items.map((item) => (
+
+
+
+ {item.classification === 'beta' && }
+ {item.classification === 'prototype' && }
+ {item.label}
+
+
+ {item.nodePack}
+
+
+ {item.description && {item.description}}
+
+
+ ))}
+ >
+ );
+});
+
+NodeCommandList.displayName = 'CommandListItems';
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx
deleted file mode 100644
index cb6516efd9..0000000000
--- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx
+++ /dev/null
@@ -1,267 +0,0 @@
-import 'reactflow/dist/style.css';
-
-import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
-import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } from '@invoke-ai/ui-library';
-import { useStore } from '@nanostores/react';
-import { useAppDispatch, useAppStore } from 'app/store/storeHooks';
-import type { SelectInstance } from 'chakra-react-select';
-import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
-import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
-import {
- $cursorPos,
- $edgePendingUpdate,
- $isAddNodePopoverOpen,
- $pendingConnection,
- $templates,
- closeAddNodePopover,
- edgesChanged,
- nodesChanged,
- openAddNodePopover,
-} from 'features/nodes/store/nodesSlice';
-import { selectNodesSlice } from 'features/nodes/store/selectors';
-import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition';
-import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection';
-import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil';
-import { validateConnectionTypes } from 'features/nodes/store/util/validateConnectionTypes';
-import type { AnyNode } from 'features/nodes/types/invocation';
-import { isInvocationNode } from 'features/nodes/types/invocation';
-import { toast } from 'features/toast/toast';
-import { filter, map, memoize, some } from 'lodash-es';
-import { memo, useCallback, useMemo, useRef } from 'react';
-import { flushSync } from 'react-dom';
-import { useHotkeys } from 'react-hotkeys-hook';
-import type { HotkeyCallback } from 'react-hotkeys-hook/dist/types';
-import { useTranslation } from 'react-i18next';
-import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters';
-import type { EdgeChange, NodeChange } from 'reactflow';
-
-const createRegex = memoize(
- (inputValue: string) =>
- new RegExp(
- inputValue
- .trim()
- .replace(/[-[\]{}()*+!<=:?./\\^$|#,]/g, '')
- .split(' ')
- .join('.*'),
- 'gi'
- )
-);
-
-const filterOption = memoize((option: FilterOptionOption, inputValue: string) => {
- if (!inputValue) {
- return true;
- }
- const regex = createRegex(inputValue);
- return (
- regex.test(option.label) ||
- regex.test(option.data.description ?? '') ||
- (option.data.tags ?? []).some((tag) => regex.test(tag))
- );
-});
-
-const AddNodePopover = () => {
- const dispatch = useAppDispatch();
- const buildInvocation = useBuildNode();
- const { t } = useTranslation();
- const selectRef = useRef | null>(null);
- const inputRef = useRef(null);
- const templates = useStore($templates);
- const pendingConnection = useStore($pendingConnection);
- const isOpen = useStore($isAddNodePopoverOpen);
- const store = useAppStore();
- const isWorkflowsActive = useStore(INTERACTION_SCOPES.workflows.$isActive);
-
- const filteredTemplates = useMemo(() => {
- // If we have a connection in progress, we need to filter the node choices
- const templatesArray = map(templates);
- if (!pendingConnection) {
- return templatesArray;
- }
-
- return filter(templates, (template) => {
- const candidateFields = pendingConnection.handleType === 'source' ? template.inputs : template.outputs;
- return some(candidateFields, (field) => {
- const sourceType =
- pendingConnection.handleType === 'source' ? field.type : pendingConnection.fieldTemplate.type;
- const targetType =
- pendingConnection.handleType === 'target' ? field.type : pendingConnection.fieldTemplate.type;
- return validateConnectionTypes(sourceType, targetType);
- });
- });
- }, [templates, pendingConnection]);
-
- const options = useMemo(() => {
- const _options: ComboboxOption[] = map(filteredTemplates, (template) => {
- return {
- label: template.title,
- value: template.type,
- description: template.description,
- tags: template.tags,
- };
- });
-
- //We only want these nodes if we're not filtered
- if (!pendingConnection) {
- _options.push({
- label: t('nodes.currentImage'),
- value: 'current_image',
- description: t('nodes.currentImageDescription'),
- tags: ['progress'],
- });
-
- _options.push({
- label: t('nodes.notes'),
- value: 'notes',
- description: t('nodes.notesDescription'),
- tags: ['notes'],
- });
- }
-
- _options.sort((a, b) => a.label.localeCompare(b.label));
-
- return _options;
- }, [filteredTemplates, pendingConnection, t]);
-
- const addNode = useCallback(
- (nodeType: string): AnyNode | null => {
- const node = buildInvocation(nodeType);
- if (!node) {
- const errorMessage = t('nodes.unknownNode', {
- nodeType: nodeType,
- });
- toast({
- status: 'error',
- title: errorMessage,
- });
- return null;
- }
-
- // Find a cozy spot for the node
- const cursorPos = $cursorPos.get();
- const { nodes, edges } = selectNodesSlice(store.getState());
- node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y);
- node.selected = true;
-
- // Deselect all other nodes and edges
- const nodeChanges: NodeChange[] = [{ type: 'add', item: node }];
- const edgeChanges: EdgeChange[] = [];
- nodes.forEach(({ id, selected }) => {
- if (selected) {
- nodeChanges.push({ type: 'select', id, selected: false });
- }
- });
- edges.forEach(({ id, selected }) => {
- if (selected) {
- edgeChanges.push({ type: 'select', id, selected: false });
- }
- });
-
- // Onwards!
- if (nodeChanges.length > 0) {
- dispatch(nodesChanged(nodeChanges));
- }
- if (edgeChanges.length > 0) {
- dispatch(edgesChanged(edgeChanges));
- }
- return node;
- },
- [buildInvocation, store, dispatch, t]
- );
-
- const onChange = useCallback(
- (v) => {
- if (!v) {
- return;
- }
- const node = addNode(v.value);
-
- // Auto-connect an edge if we just added a node and have a pending connection
- if (pendingConnection && isInvocationNode(node)) {
- const edgePendingUpdate = $edgePendingUpdate.get();
- const { handleType } = pendingConnection;
-
- const source = handleType === 'source' ? pendingConnection.nodeId : node.id;
- const sourceHandle = handleType === 'source' ? pendingConnection.handleId : null;
- const target = handleType === 'target' ? pendingConnection.nodeId : node.id;
- const targetHandle = handleType === 'target' ? pendingConnection.handleId : null;
-
- const { nodes, edges } = selectNodesSlice(store.getState());
- const connection = getFirstValidConnection(
- source,
- sourceHandle,
- target,
- targetHandle,
- nodes,
- edges,
- templates,
- edgePendingUpdate
- );
- if (connection) {
- const newEdge = connectionToEdge(connection);
- dispatch(edgesChanged([{ type: 'add', item: newEdge }]));
- }
- }
-
- closeAddNodePopover();
- },
- [addNode, dispatch, pendingConnection, store, templates]
- );
-
- const handleHotkeyOpen: HotkeyCallback = useCallback((e) => {
- if (!$isAddNodePopoverOpen.get()) {
- e.preventDefault();
- openAddNodePopover();
- flushSync(() => {
- selectRef.current?.inputRef?.focus();
- });
- }
- }, []);
-
- useHotkeys(['shift+a', 'space'], handleHotkeyOpen, { enabled: isWorkflowsActive }, [isWorkflowsActive]);
-
- const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]);
-
- return (
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default memo(AddNodePopover);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx
index f85131a133..ad631d3125 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx
@@ -8,10 +8,10 @@ import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
import {
+ $addNodeCmdk,
$cursorPos,
$didUpdateEdge,
$edgePendingUpdate,
- $isAddNodePopoverOpen,
$lastEdgeUpdateMouseEvent,
$pendingConnection,
$viewport,
@@ -281,7 +281,7 @@ export const Flow = memo(() => {
const onEscapeHotkey = useCallback(() => {
if (!$edgePendingUpdate.get()) {
$pendingConnection.set(null);
- $isAddNodePopoverOpen.set(false);
+ $addNodeCmdk.set(false);
cancelConnection();
}
}, [cancelConnection]);
diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx
index c7eb9bdbb0..72beff084c 100644
--- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx
+++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/AddNodeButton.tsx
@@ -1,10 +1,11 @@
import { IconButton } from '@invoke-ai/ui-library';
-import { openAddNodePopover } from 'features/nodes/store/nodesSlice';
+import { useAddNodeCmdk } from 'features/nodes/store/nodesSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
const AddNodeButton = () => {
+ const addNodeCmdk = useAddNodeCmdk();
const { t } = useTranslation();
return (
@@ -12,7 +13,7 @@ const AddNodeButton = () => {
tooltip={t('nodes.addNodeToolTip')}
aria-label={t('nodes.addNode')}
icon={}
- onClick={openAddNodePopover}
+ onClick={addNodeCmdk.setTrue}
pointerEvents="auto"
/>
);
diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts
index f2cc8690bb..c41ef9f689 100644
--- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts
+++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts
@@ -2,9 +2,9 @@ import { useStore } from '@nanostores/react';
import { useAppStore } from 'app/store/storeHooks';
import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
import {
+ $addNodeCmdk,
$didUpdateEdge,
$edgePendingUpdate,
- $isAddNodePopoverOpen,
$pendingConnection,
$templates,
edgesChanged,
@@ -107,7 +107,7 @@ export const useConnection = () => {
$pendingConnection.set(null);
} else {
// The mouse is not over a node - we should open the add node popover
- $isAddNodePopoverOpen.set(true);
+ $addNodeCmdk.set(true);
}
}, [store, templates, updateNodeInternals]);
diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
index d0b144dfa2..bdcc7ae815 100644
--- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
+++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts
@@ -1,6 +1,7 @@
import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit';
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import type { PersistConfig } from 'app/store/store';
+import { buildUseBoolean } from 'common/hooks/useBoolean';
import { workflowLoaded } from 'features/nodes/store/actions';
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
import type {
@@ -443,14 +444,8 @@ export const $didUpdateEdge = atom(false);
export const $lastEdgeUpdateMouseEvent = atom(null);
export const $viewport = atom({ x: 0, y: 0, zoom: 1 });
-export const $isAddNodePopoverOpen = atom(false);
-export const closeAddNodePopover = () => {
- $isAddNodePopoverOpen.set(false);
- $pendingConnection.set(null);
-};
-export const openAddNodePopover = () => {
- $isAddNodePopoverOpen.set(true);
-};
+export const $addNodeCmdk = atom(false);
+export const useAddNodeCmdk = buildUseBoolean($addNodeCmdk);
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrateNodesState = (state: any): any => {