Skip to content

Commit d872176

Browse files
christian-byrnegithub-actions
and
github-actions
authored
Add custom nodes manager UI (#2923)
Co-authored-by: github-actions <github-actions@github.com>
1 parent f53c048 commit d872176

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1792
-19
lines changed

public/assets/images/fallback-gradient-avatar.svg

+13
Loading
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<template>
2+
<hr
3+
:class="{
4+
'm-0': true,
5+
'border-t': orientation === 'horizontal',
6+
'border-l': orientation === 'vertical',
7+
'h-full': orientation === 'vertical',
8+
'w-full': orientation === 'horizontal'
9+
}"
10+
:style="{
11+
borderColor: isLightTheme ? '#DCDAE1' : '#2C2C2C',
12+
borderWidth: `${width}px !important`
13+
}"
14+
/>
15+
</template>
16+
17+
<script setup lang="ts">
18+
import { computed } from 'vue'
19+
20+
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
21+
22+
const colorPaletteStore = useColorPaletteStore()
23+
const { orientation = 'horizontal', width = 0.3 } = defineProps<{
24+
orientation?: 'horizontal' | 'vertical'
25+
width?: number
26+
}>()
27+
28+
const isLightTheme = computed(
29+
() => colorPaletteStore.completedActivePalette.light_theme
30+
)
31+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<template>
2+
<div
3+
class="flex flex-col mx-auto overflow-hidden h-[83vh] relative"
4+
:aria-label="$t('manager.title')"
5+
>
6+
<Button
7+
v-if="isSmallScreen"
8+
:icon="isSideNavOpen ? 'pi pi-chevron-left' : 'pi pi-chevron-right'"
9+
text
10+
class="absolute top-1/2 -translate-y-1/2 z-10"
11+
:class="isSideNavOpen ? 'left-[19rem]' : 'left-2'"
12+
@click="toggleSideNav"
13+
/>
14+
<div class="flex flex-1 relative overflow-hidden">
15+
<ManagerNavSidebar
16+
v-if="isSideNavOpen"
17+
:tabs="tabs"
18+
:selected-tab="selectedTab"
19+
@update:selected-tab="handleTabSelection"
20+
/>
21+
<div
22+
class="flex-1 overflow-auto"
23+
:class="{
24+
'transition-all duration-300': isSmallScreen,
25+
'pl-80': isSideNavOpen || !isSmallScreen,
26+
'pl-8': !isSideNavOpen && isSmallScreen,
27+
'pr-80': showInfoPanel
28+
}"
29+
>
30+
<div class="px-6 pt-6 flex flex-col h-full">
31+
<RegistrySearchBar
32+
v-if="!hideSearchBar"
33+
v-model:searchQuery="searchQuery"
34+
:searchResults="searchResults"
35+
@update:sortBy="handleSortChange"
36+
@update:filterBy="handleFilterChange"
37+
/>
38+
<div class="flex-1 overflow-auto">
39+
<NoResultsPlaceholder
40+
v-if="error || searchResults.length === 0"
41+
:title="
42+
error
43+
? $t('manager.errorConnecting')
44+
: $t('manager.noResultsFound')
45+
"
46+
:message="
47+
error
48+
? $t('manager.tryAgainLater')
49+
: $t('manager.tryDifferentSearch')
50+
"
51+
/>
52+
<div
53+
v-else-if="isLoading"
54+
class="flex justify-center items-center h-full"
55+
>
56+
<ProgressSpinner />
57+
</div>
58+
<div v-else class="h-full" @click="handleGridContainerClick">
59+
<VirtualGrid
60+
:items="resultsWithKeys"
61+
:defaultItemSize="DEFAULT_CARD_SIZE"
62+
class="p-0 m-0 max-w-full"
63+
:buffer-rows="2"
64+
:gridStyle="{
65+
display: 'grid',
66+
gridTemplateColumns: `repeat(auto-fill, minmax(${DEFAULT_CARD_SIZE}px, 1fr))`,
67+
padding: '0.5rem',
68+
gap: '1.125rem 1.25rem',
69+
justifyContent: 'stretch'
70+
}"
71+
>
72+
<template #item="{ item }">
73+
<div
74+
class="relative w-full aspect-square cursor-pointer"
75+
@click.stop="(event) => selectNodePack(item, event)"
76+
>
77+
<PackCard
78+
:node-pack="item"
79+
:is-selected="
80+
selectedNodePacks.some((pack) => pack.id === item.id)
81+
"
82+
/>
83+
</div>
84+
</template>
85+
</VirtualGrid>
86+
</div>
87+
</div>
88+
</div>
89+
</div>
90+
<div
91+
v-if="showInfoPanel"
92+
class="w-80 border-l-0 border-surface-border absolute right-0 top-0 bottom-0 flex z-20"
93+
>
94+
<ContentDivider orientation="vertical" :width="0.2" />
95+
<div class="flex-1 flex flex-col isolate">
96+
<InfoPanel
97+
v-if="!hasMultipleSelections"
98+
:node-pack="selectedNodePack"
99+
/>
100+
<InfoPanelMultiItem v-else :node-packs="selectedNodePacks" />
101+
</div>
102+
</div>
103+
</div>
104+
</div>
105+
</template>
106+
107+
<script setup lang="ts">
108+
import Button from 'primevue/button'
109+
import ProgressSpinner from 'primevue/progressspinner'
110+
import { computed, ref } from 'vue'
111+
112+
import ContentDivider from '@/components/common/ContentDivider.vue'
113+
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
114+
import VirtualGrid from '@/components/common/VirtualGrid.vue'
115+
import ManagerNavSidebar from '@/components/dialog/content/manager/ManagerNavSidebar.vue'
116+
import InfoPanel from '@/components/dialog/content/manager/infoPanel/InfoPanel.vue'
117+
import InfoPanelMultiItem from '@/components/dialog/content/manager/infoPanel/InfoPanelMultiItem.vue'
118+
import PackCard from '@/components/dialog/content/manager/packCard/PackCard.vue'
119+
import RegistrySearchBar from '@/components/dialog/content/manager/registrySearchBar/RegistrySearchBar.vue'
120+
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
121+
import { useRegistrySearch } from '@/composables/useRegistrySearch'
122+
import type { NodeField, TabItem } from '@/types/comfyManagerTypes'
123+
import { components } from '@/types/comfyRegistryTypes'
124+
125+
const DEFAULT_CARD_SIZE = 512
126+
127+
const {
128+
isSmallScreen,
129+
isOpen: isSideNavOpen,
130+
toggle: toggleSideNav
131+
} = useResponsiveCollapse()
132+
const hideSearchBar = computed(() => isSmallScreen.value && showInfoPanel.value)
133+
134+
const tabs = ref<TabItem[]>([
135+
{ id: 'all', label: 'All', icon: 'pi-list' },
136+
{ id: 'community', label: 'Community', icon: 'pi-globe' },
137+
{ id: 'installed', label: 'Installed', icon: 'pi-box' }
138+
])
139+
const selectedTab = ref<TabItem>(tabs.value[0])
140+
const handleTabSelection = (tab: TabItem) => {
141+
selectedTab.value = tab
142+
}
143+
144+
const { searchQuery, pageNumber, sortField, isLoading, error, searchResults } =
145+
useRegistrySearch()
146+
pageNumber.value = 1
147+
const resultsWithKeys = computed(() =>
148+
searchResults.value.map((item) => ({
149+
...item,
150+
key: item.id || item.name
151+
}))
152+
)
153+
154+
const selectedNodePacks = ref<components['schemas']['Node'][]>([])
155+
const selectedNodePack = computed(() =>
156+
selectedNodePacks.value.length === 1 ? selectedNodePacks.value[0] : null
157+
)
158+
159+
const selectNodePack = (
160+
nodePack: components['schemas']['Node'],
161+
event: MouseEvent
162+
) => {
163+
// Handle multi-select with Shift or Ctrl/Cmd key
164+
if (event.shiftKey || event.ctrlKey || event.metaKey) {
165+
const index = selectedNodePacks.value.findIndex(
166+
(pack) => pack.id === nodePack.id
167+
)
168+
169+
if (index === -1) {
170+
// Add to selection if not already selected
171+
selectedNodePacks.value.push(nodePack)
172+
} else {
173+
// Remove from selection if already selected
174+
selectedNodePacks.value.splice(index, 1)
175+
}
176+
} else {
177+
// Single select behavior
178+
selectedNodePacks.value = [nodePack]
179+
}
180+
}
181+
182+
const unSelectItems = () => {
183+
selectedNodePacks.value = []
184+
}
185+
const handleGridContainerClick = (event: MouseEvent) => {
186+
const targetElement = event.target as HTMLElement
187+
if (targetElement && !targetElement.closest('[data-virtual-grid-item]')) {
188+
unSelectItems()
189+
}
190+
}
191+
192+
const showInfoPanel = computed(() => selectedNodePacks.value.length > 0)
193+
const hasMultipleSelections = computed(() => selectedNodePacks.value.length > 1)
194+
195+
const currentFilterBy = ref('all')
196+
const handleSortChange = (sortBy: NodeField) => {
197+
sortField.value = sortBy
198+
}
199+
const handleFilterChange = (filterBy: NodeField) => {
200+
currentFilterBy.value = filterBy
201+
}
202+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<template>
2+
<div class="w-full">
3+
<div class="px-6 py-4">
4+
<h2 class="text-lg font-normal text-left">
5+
{{ $t('manager.discoverCommunityContent') }}
6+
</h2>
7+
</div>
8+
<ContentDivider :width="0.3" />
9+
</div>
10+
</template>
11+
12+
<script setup lang="ts">
13+
import ContentDivider from '@/components/common/ContentDivider.vue'
14+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<template>
2+
<aside
3+
class="absolute translate-x-0 top-0 left-0 h-full w-80 shadow-md z-5 transition-transform duration-300 ease-in-out flex"
4+
>
5+
<ScrollPanel class="w-80 mt-7">
6+
<Listbox
7+
:model-value="selectedTab"
8+
:options="tabs"
9+
optionLabel="label"
10+
listStyle="max-height:unset"
11+
:pt="{
12+
root: { class: 'w-full border-0 bg-transparent' },
13+
list: { class: 'p-5' },
14+
option: { class: 'px-8 py-3 text-lg rounded-xl' },
15+
optionGroup: { class: 'p-0 text-left text-inherit' }
16+
}"
17+
@update:model-value="handleTabSelection"
18+
>
19+
<template #option="slotProps">
20+
<div class="text-left flex items-center">
21+
<i :class="['pi', slotProps.option.icon, 'mr-3']"></i>
22+
<span class="text-lg">{{ slotProps.option.label }}</span>
23+
</div>
24+
</template>
25+
</Listbox>
26+
</ScrollPanel>
27+
<ContentDivider orientation="vertical" />
28+
</aside>
29+
</template>
30+
31+
<script setup lang="ts">
32+
import Listbox from 'primevue/listbox'
33+
import ScrollPanel from 'primevue/scrollpanel'
34+
35+
import ContentDivider from '@/components/common/ContentDivider.vue'
36+
import type { TabItem } from '@/types/comfyManagerTypes'
37+
38+
defineProps<{
39+
tabs: TabItem[]
40+
selectedTab: TabItem
41+
}>()
42+
43+
const emit = defineEmits<{
44+
'update:selectedTab': [value: TabItem]
45+
}>()
46+
47+
const handleTabSelection = (tab: TabItem) => {
48+
emit('update:selectedTab', tab)
49+
}
50+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<template>
2+
<Button
3+
outlined
4+
class="m-0 p-0 rounded-lg border-neutral-700"
5+
severity="secondary"
6+
:class="{
7+
'w-full': fullWidth,
8+
'w-min-content': !fullWidth
9+
}"
10+
>
11+
<span class="py-2.5 px-3">
12+
{{ multi ? $t('manager.installSelected') : $t('g.install') }}
13+
</span>
14+
</Button>
15+
</template>
16+
17+
<script setup lang="ts">
18+
import Button from 'primevue/button'
19+
20+
defineProps<{
21+
fullWidth?: boolean
22+
multi?: boolean
23+
}>()
24+
</script>

0 commit comments

Comments
 (0)