-
Notifications
You must be signed in to change notification settings - Fork 512
feat: right side panel favorites, no selection state, and more... #7812
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
069e2fd
296a9e1
0d71d53
4bdb441
3013780
9d83cd3
65a57d1
1fa6e13
8293de2
a032ac4
b224e98
b4d86d5
ea8c41c
ab06fcd
87ed0c1
bd48922
4f95f0d
cbe4d59
8597e06
5b9a279
7506e65
c8a8256
99095cc
7e501f4
c5563d4
fd16859
9650e85
ebb3cc3
5f066b4
86dac8c
8f0ed7d
d6b6ae3
59b561b
193fa0a
acda492
486cad6
81200d5
7310683
e01e5e1
308b489
e48894c
19e0a33
8134e12
2637255
37f1e94
8a0217c
932ca80
0135805
a6f9f91
d4a6fde
de9d3a4
ebf8731
61ba51e
55d4e02
59b75ce
40faf68
37e4d4d
c5f95bb
34eeb37
8bbb44e
6bd8711
17808d6
7d2c3af
11f3a72
e00be30
924d956
f7be62a
920072e
950739e
88c1380
20350c4
8a38897
1516dba
d2b59e4
f850fc5
30f19df
838e006
6e2308a
f8c2aa8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,22 @@ | ||
| <script lang="ts" setup> | ||
| import { watch } from 'vue' | ||
|
|
||
| import { cn } from '@/utils/tailwindUtil' | ||
|
|
||
| defineProps<{ | ||
| const props = defineProps<{ | ||
| isEmpty?: boolean | ||
| defaultCollapse?: boolean | ||
| }>() | ||
|
Comment on lines
+8
to
13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider Vue 3.5 reactive props destructuring. Per coding guidelines, prefer destructuring props directly. This also allows setting defaults inline: ♻️ Suggested refactor-const props = defineProps<{
+const {
+ disabled = false,
+ label,
+ enableEmptyState = false,
+ tooltip
+} = defineProps<{
disabled?: boolean
label?: string
enableEmptyState?: boolean
tooltip?: string
-}>()
+}>()Then update line 17: -const isExpanded = computed(() => !isCollapse.value && !props.disabled)
+const isExpanded = computed(() => !isCollapse.value && !disabled)And lines 19-21: const tooltipConfig = computed(() => {
- if (!props.tooltip) return undefined
- return { value: props.tooltip, showDelay: 1000 }
+ if (!tooltip) return undefined
+ return { value: tooltip, showDelay: 1000 }
})🤖 Prompt for AI Agents |
||
|
|
||
| const isCollapse = defineModel<boolean>('collapse', { default: false }) | ||
|
|
||
| if (props.defaultCollapse) { | ||
| isCollapse.value = true | ||
| } | ||
| watch( | ||
| () => props.defaultCollapse, | ||
| (value) => (isCollapse.value = value) | ||
| ) | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
LittleSound marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </script> | ||
|
|
||
| <template> | ||
|
|
@@ -32,7 +43,7 @@ const isCollapse = defineModel<boolean>('collapse', { default: false }) | |
| :disabled="isEmpty" | ||
| @click="isCollapse = !isCollapse" | ||
|
Comment on lines
30
to
40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add The accordion button should communicate its expanded state to assistive technologies. ♿ Proposed fix <button
v-tooltip="tooltipConfig"
type="button"
+ :aria-expanded="isExpanded"
:class="
cn(
'group min-h-12 bg-transparent border-0 outline-0 ring-0 w-full text-left flex items-center justify-between pl-4 pr-3',
!disabled && 'cursor-pointer'
)
"
:disabled="disabled"
@click="isCollapse = !isCollapse"
>🤖 Prompt for AI Agents |
||
| > | ||
| <span class="text-sm font-semibold line-clamp-2"> | ||
| <span class="text-sm font-semibold line-clamp-2 flex-1"> | ||
| <slot name="label" /> | ||
| </span> | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,29 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Searches widgets in a list and returns search results. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Filters by name, localized label, type, and user-input value. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Performs basic tokenization of the query string. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function searchWidgets<T extends { widget: IBaseWidget }[]>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| list: T, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| query: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): T { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (query.trim() === '') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return list | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const words = query.trim().toLowerCase().split(' ') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return list.filter(({ widget }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const label = widget.label?.toLowerCase() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const name = widget.name.toLowerCase() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const type = widget.type.toLowerCase() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const value = widget.value?.toString().toLowerCase() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return words.every( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (word) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name.includes(word) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label?.includes(word) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type?.includes(word) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value?.includes(word) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) as T | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function searchWidgets<T extends { widget: IBaseWidget }[]>( | |
| list: T, | |
| query: string | |
| ): T { | |
| if (query.trim() === '') { | |
| return list | |
| } | |
| const words = query.trim().toLowerCase().split(' ') | |
| return list.filter(({ widget }) => { | |
| const label = widget.label?.toLowerCase() | |
| const name = widget.name.toLowerCase() | |
| const type = widget.type.toLowerCase() | |
| const value = widget.value?.toString().toLowerCase() | |
| return words.every( | |
| (word) => | |
| name.includes(word) || | |
| label?.includes(word) || | |
| type?.includes(word) || | |
| value?.includes(word) | |
| ) | |
| }) as T | |
| } | |
| export function searchWidgets<T extends { widget: IBaseWidget }>( | |
| list: T[], | |
| query: string | |
| ): T[] { | |
| if (query.trim() === '') { | |
| return list | |
| } | |
| const words = query.trim().toLowerCase().split(' ') | |
| return list.filter(({ widget }) => { | |
| const label = widget.label?.toLowerCase() | |
| const name = widget.name.toLowerCase() | |
| const type = widget.type.toLowerCase() | |
| const value = widget.value?.toString().toLowerCase() | |
| return words.every( | |
| (word) => | |
| name.includes(word) || | |
| label?.includes(word) || | |
| type?.includes(word) || | |
| value?.includes(word) | |
| ) | |
| }) | |
| } |
🤖 Prompt for AI Agents
In src/components/rightSidePanel/layout/index.ts around lines 8 to 29, the
function uses an unsafe "as T" cast on the filtered result which loses type
safety; change the generic to represent the element type rather than the array
type (e.g. use a generic U extends { widget: IBaseWidget } and accept list:
U[]), have the function return U[] and let TypeScript infer the filter result as
U[] (remove the "as T" cast), and update the function signature and any call
sites accordingly so the return type correctly matches the element type of the
input array.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,56 @@ | ||
| <script setup lang="ts"> | ||
| import { computed, provide } from 'vue' | ||
| import { | ||
| computed, | ||
| provide, | ||
| ref, | ||
| shallowRef, | ||
| triggerRef, | ||
| watchEffect | ||
| } from 'vue' | ||
| import { useI18n } from 'vue-i18n' | ||
|
|
||
| import type { LGraphNode } from '@/lib/litegraph/src/litegraph' | ||
| import Button from '@/components/ui/button/Button.vue' | ||
| import type { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph' | ||
| import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' | ||
| import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' | ||
| import WidgetLegacy from '@/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue' | ||
| import { | ||
| getComponent, | ||
| shouldExpand | ||
| } from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry' | ||
| import { cn } from '@/utils/tailwindUtil' | ||
|
|
||
| import PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue' | ||
| import WidgetItem from './WidgetItem.vue' | ||
|
|
||
| const { label, widgets } = defineProps<{ | ||
| const { | ||
| label, | ||
| widgets: widgetsProp, | ||
| showLocateButton = false, | ||
| isDraggable = false, | ||
| hiddenFavoriteIndicator = false, | ||
| showNodeName = false, | ||
| defaultCollapse = false, | ||
| parents = [], | ||
| isShownOnParents = false | ||
| } = defineProps<{ | ||
| label?: string | ||
| parents?: SubgraphNode[] | ||
| widgets: { widget: IBaseWidget; node: LGraphNode }[] | ||
| showLocateButton?: boolean | ||
| isDraggable?: boolean | ||
| hiddenFavoriteIndicator?: boolean | ||
| showNodeName?: boolean | ||
| defaultCollapse?: boolean | ||
| isShownOnParents?: boolean | ||
| }>() | ||
|
|
||
| const collapse = defineModel<boolean>('collapse') | ||
|
|
||
| const widgetsContainer = ref<HTMLElement>() | ||
|
|
||
| const widgets = shallowRef(widgetsProp) | ||
| watchEffect(() => (widgets.value = widgetsProp)) | ||
LittleSound marked this conversation as resolved.
Show resolved
Hide resolved
LittleSound marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| provide('hideLayoutField', true) | ||
|
|
||
| const canvasStore = useCanvasStore() | ||
| const { t } = useI18n() | ||
|
|
||
| function getWidgetComponent(widget: IBaseWidget) { | ||
| const component = getComponent(widget.type, widget.name) | ||
| return component || WidgetLegacy | ||
| } | ||
|
|
||
| function onWidgetValueChange( | ||
| widget: IBaseWidget, | ||
| value: string | number | boolean | object | ||
|
|
@@ -38,7 +60,11 @@ function onWidgetValueChange( | |
| canvasStore.canvas?.setDirty(true, true) | ||
| } | ||
|
|
||
| const isEmpty = computed(() => widgets.length === 0) | ||
| function onWidgetUpdate() { | ||
| triggerRef(widgets) | ||
| } | ||
|
Comment on lines
+62
to
83
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Harden proxyWidgets parsing and avoid |
||
|
|
||
| const isEmpty = computed(() => widgets.value.length === 0) | ||
|
|
||
| const displayLabel = computed( | ||
| () => | ||
|
|
@@ -47,40 +73,78 @@ const displayLabel = computed( | |
| ? t('rightSidePanel.inputsNone') | ||
| : t('rightSidePanel.inputs')) | ||
| ) | ||
|
|
||
| const targetNode = computed<LGraphNode | null>(() => { | ||
| if (!showLocateButton || isEmpty.value) return null | ||
|
|
||
| const firstNodeId = widgets.value[0]?.node.id | ||
| const allSameNode = widgets.value.every(({ node }) => node.id === firstNodeId) | ||
|
|
||
| return allSameNode ? widgets.value[0].node : null | ||
| }) | ||
LittleSound marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const canShowLocateButton = computed( | ||
| () => showLocateButton && targetNode.value !== null | ||
| ) | ||
|
|
||
| function handleLocateNode() { | ||
| if (!targetNode.value || !canvasStore.canvas) return | ||
|
|
||
| const graphNode = canvasStore.canvas.graph?.getNodeById(targetNode.value.id) | ||
| if (graphNode) { | ||
| canvasStore.canvas.animateToBounds(graphNode.boundingRect) | ||
| } | ||
| } | ||
|
|
||
| defineExpose({ | ||
| widgetsContainer | ||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <PropertiesAccordionItem :is-empty> | ||
| <PropertiesAccordionItem | ||
| v-model:collapse="collapse" | ||
| :is-empty | ||
| :default-collapse="defaultCollapse" | ||
| > | ||
| <template #label> | ||
| <slot name="label"> | ||
| {{ displayLabel }} | ||
| </slot> | ||
| <div class="flex items-center gap-2 flex-1 min-w-0"> | ||
| <span class="truncate flex-1"> | ||
| <slot name="label"> | ||
| {{ displayLabel }} | ||
| </slot> | ||
| </span> | ||
| <Button | ||
| v-if="canShowLocateButton" | ||
| variant="textonly" | ||
| size="icon-sm" | ||
| class="shrink-0 mr-3" | ||
| :title="t('rightSidePanel.locateNode')" | ||
| @click.stop="handleLocateNode" | ||
| > | ||
| <i class="icon-[lucide--locate] size-4 text-muted-foreground" /> | ||
| </Button> | ||
LittleSound marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </div> | ||
| </template> | ||
|
|
||
| <div v-if="!isEmpty" class="space-y-4 rounded-lg bg-interface-surface px-4"> | ||
| <div | ||
| <div | ||
| v-if="!isEmpty" | ||
| ref="widgetsContainer" | ||
| class="space-y-2 rounded-lg px-4 pt-1" | ||
| > | ||
| <WidgetItem | ||
| v-for="({ widget, node }, index) in widgets" | ||
| :key="`widget-${index}-${widget.name}`" | ||
| class="widget-item gap-1.5 col-span-full grid grid-cols-subgrid" | ||
| > | ||
| <div class="min-h-8"> | ||
| <p v-if="widget.name" class="text-sm leading-8 p-0 m-0 line-clamp-1"> | ||
| {{ widget.label || widget.name }} | ||
| </p> | ||
| </div> | ||
| <component | ||
| :is="getWidgetComponent(widget)" | ||
| :widget="widget" | ||
| :model-value="widget.value" | ||
| :node-id="String(node.id)" | ||
| :node-type="node.type" | ||
| :class="cn('col-span-1', shouldExpand(widget.type) && 'min-h-36')" | ||
| @update:model-value=" | ||
| (value: string | number | boolean | object) => | ||
| onWidgetValueChange(widget, value) | ||
| " | ||
| /> | ||
| </div> | ||
| :widget="widget" | ||
| :node="node" | ||
| :is-draggable="isDraggable" | ||
| :hidden-favorite-indicator="hiddenFavoriteIndicator" | ||
| :show-node-name="showNodeName" | ||
| :parents="parents" | ||
| :is-shown-on-parents="isShownOnParents" | ||
| @value-change="onWidgetValueChange" | ||
| @widget-update="onWidgetUpdate" | ||
| /> | ||
LittleSound marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| </div> | ||
| </PropertiesAccordionItem> | ||
| </template> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: The selection state logic seems like it might be worth refactoring into a separate module