mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
Updates: More Modal Modification (#8256)
Refactors modal dialog layouts for improved flexibility and consistency. **Changes:** - Add dedicated slot for left panel header title with dynamic content/icons - Consolidate side panel rendering within `BaseModalLayout` - Remove redundant `PanelHeader` and `RightSidePanel` components - Apply `select-none` to text elements to prevent accidental selection --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -82,9 +82,7 @@ test.describe('Templates', () => {
|
|||||||
await expect(comfyPage.templates.content).toBeVisible()
|
await expect(comfyPage.templates.content).toBeVisible()
|
||||||
|
|
||||||
await comfyPage.page
|
await comfyPage.page
|
||||||
.locator(
|
.getByRole('button', { name: 'Getting Started' })
|
||||||
'nav > div:nth-child(3) > div > span:has-text("Getting Started")'
|
|
||||||
)
|
|
||||||
.click()
|
.click()
|
||||||
await comfyPage.templates.loadTemplate('default')
|
await comfyPage.templates.loadTemplate('default')
|
||||||
await expect(comfyPage.templates.content).toBeHidden()
|
await expect(comfyPage.templates.content).toBeHidden()
|
||||||
|
|||||||
@@ -3,17 +3,14 @@
|
|||||||
:content-title="$t('templateWorkflows.title', 'Workflow Templates')"
|
:content-title="$t('templateWorkflows.title', 'Workflow Templates')"
|
||||||
class="workflow-template-selector-dialog"
|
class="workflow-template-selector-dialog"
|
||||||
>
|
>
|
||||||
|
<template #leftPanelHeaderTitle>
|
||||||
|
<i class="icon-[comfy--template]" />
|
||||||
|
<h2 class="text-neutral text-base">
|
||||||
|
{{ $t('sideToolbar.templates', 'Templates') }}
|
||||||
|
</h2>
|
||||||
|
</template>
|
||||||
<template #leftPanel>
|
<template #leftPanel>
|
||||||
<LeftSidePanel v-model="selectedNavItem" :nav-items="navItems">
|
<LeftSidePanel v-model="selectedNavItem" :nav-items="navItems" />
|
||||||
<template #header-icon>
|
|
||||||
<i class="icon-[comfy--template]" />
|
|
||||||
</template>
|
|
||||||
<template #header-title>
|
|
||||||
<span class="text-neutral text-base">{{
|
|
||||||
$t('sideToolbar.templates', 'Templates')
|
|
||||||
}}</span>
|
|
||||||
</template>
|
|
||||||
</LeftSidePanel>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #rightPanel>
|
<template #rightPanel>
|
||||||
<RightSidePanel></RightSidePanel>
|
<div class="size-full bg-modal-panel-background pr-6 pb-8 pl-4"></div>
|
||||||
</template>
|
</template>
|
||||||
</BaseModalLayout>
|
</BaseModalLayout>
|
||||||
</template>
|
</template>
|
||||||
@@ -136,7 +136,6 @@ import SingleSelect from '@/components/input/SingleSelect.vue'
|
|||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||||
import RightSidePanel from '@/components/widget/panel/RightSidePanel.vue'
|
|
||||||
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||||
import { OnCloseKey } from '@/types/widgetTypes'
|
import { OnCloseKey } from '@/types/widgetTypes'
|
||||||
import { createGridStyle } from '@/utils/gridUtil'
|
import { createGridStyle } from '@/utils/gridUtil'
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { OnCloseKey } from '@/types/widgetTypes'
|
|||||||
import { createGridStyle } from '@/utils/gridUtil'
|
import { createGridStyle } from '@/utils/gridUtil'
|
||||||
|
|
||||||
import LeftSidePanel from '../panel/LeftSidePanel.vue'
|
import LeftSidePanel from '../panel/LeftSidePanel.vue'
|
||||||
import RightSidePanel from '../panel/RightSidePanel.vue'
|
|
||||||
import BaseModalLayout from './BaseModalLayout.vue'
|
import BaseModalLayout from './BaseModalLayout.vue'
|
||||||
|
|
||||||
interface StoryArgs {
|
interface StoryArgs {
|
||||||
@@ -69,7 +68,6 @@ const createStoryTemplate = (args: StoryArgs) => ({
|
|||||||
components: {
|
components: {
|
||||||
BaseModalLayout,
|
BaseModalLayout,
|
||||||
LeftSidePanel,
|
LeftSidePanel,
|
||||||
RightSidePanel,
|
|
||||||
SearchBox,
|
SearchBox,
|
||||||
MultiSelect,
|
MultiSelect,
|
||||||
SingleSelect,
|
SingleSelect,
|
||||||
@@ -175,16 +173,15 @@ const createStoryTemplate = (args: StoryArgs) => ({
|
|||||||
template: `
|
template: `
|
||||||
<div>
|
<div>
|
||||||
<BaseModalLayout v-if="!args.hasRightPanel" :content-title="args.contentTitle || 'Content Title'">
|
<BaseModalLayout v-if="!args.hasRightPanel" :content-title="args.contentTitle || 'Content Title'">
|
||||||
|
<!-- Left Panel Header Title -->
|
||||||
|
<template v-if="args.hasLeftPanel" #leftPanelHeaderTitle>
|
||||||
|
<i class="icon-[lucide--puzzle] size-4 text-neutral" />
|
||||||
|
<span class="text-neutral text-base">Title</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Left Panel -->
|
<!-- Left Panel -->
|
||||||
<template v-if="args.hasLeftPanel" #leftPanel>
|
<template v-if="args.hasLeftPanel" #leftPanel>
|
||||||
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
|
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation" />
|
||||||
<template #header-icon>
|
|
||||||
<i class="icon-[lucide--puzzle] size-4 text-neutral" />
|
|
||||||
</template>
|
|
||||||
<template #header-title>
|
|
||||||
<span class="text-neutral text-base">Title</span>
|
|
||||||
</template>
|
|
||||||
</LeftSidePanel>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@@ -299,16 +296,15 @@ const createStoryTemplate = (args: StoryArgs) => ({
|
|||||||
|
|
||||||
<BaseModalLayout v-else :content-title="args.contentTitle || 'Content Title'">
|
<BaseModalLayout v-else :content-title="args.contentTitle || 'Content Title'">
|
||||||
<!-- Same content but WITH right panel -->
|
<!-- Same content but WITH right panel -->
|
||||||
|
<!-- Left Panel Header Title -->
|
||||||
|
<template v-if="args.hasLeftPanel" #leftPanelHeaderTitle>
|
||||||
|
<i class="icon-[lucide--puzzle] size-4 text-neutral" />
|
||||||
|
<span class="text-neutral text-base">Title</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Left Panel -->
|
<!-- Left Panel -->
|
||||||
<template v-if="args.hasLeftPanel" #leftPanel>
|
<template v-if="args.hasLeftPanel" #leftPanel>
|
||||||
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
|
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation" />
|
||||||
<template #header-icon>
|
|
||||||
<i class="icon-[lucide--puzzle] size-4 text-neutral" />
|
|
||||||
</template>
|
|
||||||
<template #header-title>
|
|
||||||
<span class="text-neutral text-base">Title</span>
|
|
||||||
</template>
|
|
||||||
</LeftSidePanel>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@@ -415,7 +411,7 @@ const createStoryTemplate = (args: StoryArgs) => ({
|
|||||||
|
|
||||||
<!-- Right Panel - Only when hasRightPanel is true -->
|
<!-- Right Panel - Only when hasRightPanel is true -->
|
||||||
<template #rightPanel>
|
<template #rightPanel>
|
||||||
<RightSidePanel />
|
<div class="size-full bg-modal-panel-background pr-6 pb-8 pl-4"></div>
|
||||||
</template>
|
</template>
|
||||||
</BaseModalLayout>
|
</BaseModalLayout>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,13 +8,26 @@
|
|||||||
:style="gridStyle"
|
:style="gridStyle"
|
||||||
>
|
>
|
||||||
<nav
|
<nav
|
||||||
class="h-full overflow-hidden"
|
class="h-full overflow-hidden bg-modal-panel-background flex flex-col"
|
||||||
:inert="!showLeftPanel"
|
:inert="!showLeftPanel"
|
||||||
:aria-hidden="!showLeftPanel"
|
:aria-hidden="!showLeftPanel"
|
||||||
>
|
>
|
||||||
<div v-if="hasLeftPanel" class="h-full min-w-40 max-w-56">
|
<header
|
||||||
<slot name="leftPanel" />
|
data-component-id="LeftPanelHeader"
|
||||||
</div>
|
class="flex w-full h-18 shrink-0 gap-2 pl-6 pr-3 items-center-safe"
|
||||||
|
>
|
||||||
|
<slot name="leftPanelHeaderTitle" />
|
||||||
|
<Button
|
||||||
|
v-if="!notMobile && showLeftPanel"
|
||||||
|
size="lg"
|
||||||
|
class="w-10 p-0 ml-auto"
|
||||||
|
:aria-label="t('g.hideLeftPanel')"
|
||||||
|
@click="toggleLeftPanel"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--panel-left-close]" />
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
<slot name="leftPanel" />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="flex flex-col bg-base-background overflow-hidden">
|
<div class="flex flex-col bg-base-background overflow-hidden">
|
||||||
@@ -24,22 +37,13 @@
|
|||||||
>
|
>
|
||||||
<div class="flex flex-1 shrink-0 gap-2">
|
<div class="flex flex-1 shrink-0 gap-2">
|
||||||
<Button
|
<Button
|
||||||
v-if="!notMobile"
|
v-if="!notMobile && !showLeftPanel"
|
||||||
size="icon"
|
size="lg"
|
||||||
:aria-label="
|
class="w-10 p-0"
|
||||||
showLeftPanel ? t('g.hideLeftPanel') : t('g.showLeftPanel')
|
:aria-label="t('g.showLeftPanel')"
|
||||||
"
|
|
||||||
@click="toggleLeftPanel"
|
@click="toggleLeftPanel"
|
||||||
>
|
>
|
||||||
<i
|
<i class="icon-[lucide--panel-left]" />
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
showLeftPanel
|
|
||||||
? 'icon-[lucide--panel-left]'
|
|
||||||
: 'icon-[lucide--panel-left-close]'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +73,7 @@
|
|||||||
<slot name="contentFilter" />
|
<slot name="contentFilter" />
|
||||||
<h2
|
<h2
|
||||||
v-if="!hasLeftPanel"
|
v-if="!hasLeftPanel"
|
||||||
class="text-xxl m-0 px-6 pt-2 pb-6 capitalize"
|
class="text-xxl m-0 select-none px-6 pt-2 pb-6 capitalize"
|
||||||
>
|
>
|
||||||
{{ contentTitle }}
|
{{ contentTitle }}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -94,7 +98,10 @@
|
|||||||
data-component-id="RightPanelHeader"
|
data-component-id="RightPanelHeader"
|
||||||
class="flex h-18 shrink-0 items-center gap-2 px-6"
|
class="flex h-18 shrink-0 items-center gap-2 px-6"
|
||||||
>
|
>
|
||||||
<h2 v-if="rightPanelTitle" class="flex-1 text-base font-semibold">
|
<h2
|
||||||
|
v-if="rightPanelTitle"
|
||||||
|
class="flex-1 select-none text-base font-semibold"
|
||||||
|
>
|
||||||
{{ rightPanelTitle }}
|
{{ rightPanelTitle }}
|
||||||
</h2>
|
</h2>
|
||||||
<div v-else class="flex-1">
|
<div v-else class="flex-1">
|
||||||
@@ -134,7 +141,6 @@ import { useI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { OnCloseKey } from '@/types/widgetTypes'
|
import { OnCloseKey } from '@/types/widgetTypes'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
disabled: !isOverflowing,
|
disabled: !isOverflowing,
|
||||||
pt: { text: { class: 'whitespace-nowrap' } }
|
pt: { text: { class: 'whitespace-nowrap' } }
|
||||||
}"
|
}"
|
||||||
class="flex cursor-pointer items-center-safe gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground"
|
class="flex cursor-pointer select-none items-center-safe gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground"
|
||||||
:class="
|
:class="
|
||||||
active
|
active
|
||||||
? 'bg-interface-menu-component-surface-selected'
|
? 'bg-interface-menu-component-surface-selected'
|
||||||
|
|||||||
@@ -7,20 +7,6 @@ const meta: Meta<typeof LeftSidePanel> = {
|
|||||||
title: 'Components/Widget/Panel/LeftSidePanel',
|
title: 'Components/Widget/Panel/LeftSidePanel',
|
||||||
component: LeftSidePanel,
|
component: LeftSidePanel,
|
||||||
argTypes: {
|
argTypes: {
|
||||||
'header-icon': {
|
|
||||||
table: {
|
|
||||||
type: { summary: 'slot' },
|
|
||||||
defaultValue: { summary: 'undefined' }
|
|
||||||
},
|
|
||||||
control: false
|
|
||||||
},
|
|
||||||
'header-title': {
|
|
||||||
table: {
|
|
||||||
type: { summary: 'slot' },
|
|
||||||
defaultValue: { summary: 'undefined' }
|
|
||||||
},
|
|
||||||
control: false
|
|
||||||
},
|
|
||||||
'onUpdate:modelValue': {
|
'onUpdate:modelValue': {
|
||||||
table: { disable: true }
|
table: { disable: true }
|
||||||
}
|
}
|
||||||
@@ -59,14 +45,7 @@ export const Default: Story = {
|
|||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div style="height: 500px; width: 256px;">
|
<div style="height: 500px; width: 256px;">
|
||||||
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems">
|
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems" />
|
||||||
<template #header-icon>
|
|
||||||
<i class="icon-[lucide--puzzle] size-4 text-neutral" />
|
|
||||||
</template>
|
|
||||||
<template #header-title>
|
|
||||||
<span class="text-neutral text-base">Navigation</span>
|
|
||||||
</template>
|
|
||||||
</LeftSidePanel>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
@@ -126,14 +105,7 @@ export const WithGroups: Story = {
|
|||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div style="height: 500px; width: 256px;">
|
<div style="height: 500px; width: 256px;">
|
||||||
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems">
|
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems" />
|
||||||
<template #header-icon>
|
|
||||||
<i class="icon-[lucide--puzzle] size-4 text-neutral" />
|
|
||||||
</template>
|
|
||||||
<template #header-title>
|
|
||||||
<span class="text-neutral text-base">Model Selector</span>
|
|
||||||
</template>
|
|
||||||
</LeftSidePanel>
|
|
||||||
<div class="mt-4 p-2 text-sm">
|
<div class="mt-4 p-2 text-sm">
|
||||||
Selected: {{ selectedItem }}
|
Selected: {{ selectedItem }}
|
||||||
</div>
|
</div>
|
||||||
@@ -176,14 +148,7 @@ export const DefaultIcons: Story = {
|
|||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div style="height: 400px; width: 256px;">
|
<div style="height: 400px; width: 256px;">
|
||||||
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems">
|
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems" />
|
||||||
<template #header-icon>
|
|
||||||
<i class="icon-[lucide--folder] size-4 text-neutral" />
|
|
||||||
</template>
|
|
||||||
<template #header-title>
|
|
||||||
<span class="text-neutral text-base">Files</span>
|
|
||||||
</template>
|
|
||||||
</LeftSidePanel>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
@@ -228,14 +193,7 @@ export const LongLabels: Story = {
|
|||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div style="height: 500px; width: 256px;">
|
<div style="height: 500px; width: 256px;">
|
||||||
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems">
|
<LeftSidePanel v-model="selectedItem" :nav-items="args.navItems" />
|
||||||
<template #header-icon>
|
|
||||||
<i class="icon-[lucide--settings] size-4 text-neutral" />
|
|
||||||
</template>
|
|
||||||
<template #header-title>
|
|
||||||
<span class="text-neutral text-base">Settings</span>
|
|
||||||
</template>
|
|
||||||
</LeftSidePanel>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,47 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full w-full flex-col bg-modal-panel-background">
|
<div
|
||||||
<PanelHeader>
|
class="flex w-full flex-auto overflow-y-auto gap-1 min-h-0 flex-col bg-modal-panel-background scrollbar-hide px-3"
|
||||||
<template #icon>
|
>
|
||||||
<slot name="header-icon"></slot>
|
<template
|
||||||
</template>
|
v-for="item in navItems"
|
||||||
<slot name="header-title"></slot>
|
:key="'title' in item ? item.title : item.id"
|
||||||
</PanelHeader>
|
|
||||||
|
|
||||||
<nav
|
|
||||||
class="flex scrollbar-hide flex-1 flex-col gap-1 overflow-y-auto px-3 py-4"
|
|
||||||
>
|
>
|
||||||
<template v-for="(item, index) in navItems" :key="index">
|
<div v-if="'items' in item" class="flex flex-col gap-2">
|
||||||
<div v-if="'items' in item" class="flex flex-col gap-2">
|
<NavTitle
|
||||||
<NavTitle
|
v-model="collapsedGroups[item.title]"
|
||||||
v-model="collapsedGroups[item.title]"
|
:title="item.title"
|
||||||
:title="item.title"
|
:collapsible="item.collapsible"
|
||||||
:collapsible="item.collapsible"
|
/>
|
||||||
/>
|
<template v-if="!item.collapsible || !collapsedGroups[item.title]">
|
||||||
<template v-if="!item.collapsible || !collapsedGroups[item.title]">
|
|
||||||
<NavItem
|
|
||||||
v-for="subItem in item.items"
|
|
||||||
:key="subItem.id"
|
|
||||||
:icon="subItem.icon"
|
|
||||||
:badge="subItem.badge"
|
|
||||||
:active="activeItem === subItem.id"
|
|
||||||
@click="activeItem = subItem.id"
|
|
||||||
>
|
|
||||||
{{ subItem.label }}
|
|
||||||
</NavItem>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-col gap-2">
|
|
||||||
<NavItem
|
<NavItem
|
||||||
:icon="item.icon"
|
v-for="subItem in item.items"
|
||||||
:badge="item.badge"
|
:key="subItem.id"
|
||||||
:active="activeItem === item.id"
|
:icon="subItem.icon"
|
||||||
@click="activeItem = item.id"
|
:badge="subItem.badge"
|
||||||
|
:active="activeItem === subItem.id"
|
||||||
|
@click="activeItem = subItem.id"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
{{ subItem.label }}
|
||||||
</NavItem>
|
</NavItem>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
</nav>
|
<div v-else class="flex flex-col gap-2">
|
||||||
|
<NavItem
|
||||||
|
:icon="item.icon"
|
||||||
|
:badge="item.badge"
|
||||||
|
:active="activeItem === item.id"
|
||||||
|
@click="activeItem = item.id"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</NavItem>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -52,8 +46,6 @@ import NavItem from '@/components/widget/nav/NavItem.vue'
|
|||||||
import NavTitle from '@/components/widget/nav/NavTitle.vue'
|
import NavTitle from '@/components/widget/nav/NavTitle.vue'
|
||||||
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
import type { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||||
|
|
||||||
import PanelHeader from './PanelHeader.vue'
|
|
||||||
|
|
||||||
const { navItems = [], modelValue } = defineProps<{
|
const { navItems = [], modelValue } = defineProps<{
|
||||||
navItems?: (NavItemData | NavGroupData)[]
|
navItems?: (NavItemData | NavGroupData)[]
|
||||||
modelValue?: string | null
|
modelValue?: string | null
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<template>
|
|
||||||
<header class="flex h-16 items-center justify-between px-6">
|
|
||||||
<div class="flex items-center gap-2 pl-1">
|
|
||||||
<slot name="icon">
|
|
||||||
<i class="text-neutral icon-[lucide--puzzle] text-base" />
|
|
||||||
</slot>
|
|
||||||
<h2 class="text-neutral text-base font-bold">
|
|
||||||
<slot></slot>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="size-full bg-modal-panel-background pr-6 pb-8 pl-4">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute left-2 bottom-2 flex flex-wrap justify-start gap-1">
|
<div
|
||||||
|
class="absolute left-2 bottom-2 flex flex-wrap justify-start gap-1 select-none"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
v-for="badge in badges"
|
v-for="badge in badges"
|
||||||
:key="badge.label"
|
:key="badge.label"
|
||||||
|
|||||||
@@ -7,19 +7,18 @@
|
|||||||
:right-panel-title="$t('assetBrowser.modelInfo.title')"
|
:right-panel-title="$t('assetBrowser.modelInfo.title')"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
|
<template v-if="shouldShowLeftPanel" #leftPanelHeaderTitle>
|
||||||
|
<i class="icon-[comfy--ai-model] size-4" />
|
||||||
|
<h2 class="flex-auto select-none text-base font-semibold text-nowrap">
|
||||||
|
{{ displayTitle }}
|
||||||
|
</h2>
|
||||||
|
</template>
|
||||||
<template v-if="shouldShowLeftPanel" #leftPanel>
|
<template v-if="shouldShowLeftPanel" #leftPanel>
|
||||||
<LeftSidePanel
|
<LeftSidePanel
|
||||||
v-model="selectedNavItem"
|
v-model="selectedNavItem"
|
||||||
data-component-id="AssetBrowserModal-LeftSidePanel"
|
data-component-id="AssetBrowserModal-LeftSidePanel"
|
||||||
:nav-items
|
:nav-items
|
||||||
>
|
/>
|
||||||
<template #header-icon>
|
|
||||||
<div class="icon-[comfy--ai-model] size-4" />
|
|
||||||
</template>
|
|
||||||
<template #header-title>
|
|
||||||
<span class="capitalize">{{ displayTitle }}</span>
|
|
||||||
</template>
|
|
||||||
</LeftSidePanel>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
:tabindex="interactive ? 0 : -1"
|
:tabindex="interactive ? 0 : -1"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'rounded-2xl overflow-hidden transition-all duration-200 bg-modal-card-background p-2 gap-2 flex flex-col h-full',
|
'select-none rounded-2xl overflow-hidden transition-all duration-200 bg-modal-card-background p-2 gap-2 flex flex-col h-full',
|
||||||
interactive &&
|
interactive &&
|
||||||
'group appearance-none bg-transparent m-0 outline-none text-left hover:bg-secondary-background focus:bg-secondary-background border-none focus:outline-solid outline-base-foreground outline-4',
|
'group appearance-none bg-transparent m-0 outline-none text-left hover:bg-secondary-background focus:bg-secondary-background border-none focus:outline-solid outline-base-foreground outline-4',
|
||||||
focused && 'bg-secondary-background outline-solid'
|
focused && 'bg-secondary-background outline-solid'
|
||||||
|
|||||||
@@ -79,8 +79,9 @@ const fileFormats = ref<SelectOption[]>([])
|
|||||||
const baseModels = ref<SelectOption[]>([])
|
const baseModels = ref<SelectOption[]>([])
|
||||||
const sortBy = ref<SortOption>('recent')
|
const sortBy = ref<SortOption>('recent')
|
||||||
|
|
||||||
const { availableFileFormats, availableBaseModels } =
|
const { availableFileFormats, availableBaseModels } = useAssetFilterOptions(
|
||||||
useAssetFilterOptions(assets)
|
() => assets
|
||||||
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
filterChange: [filters: FilterState]
|
filterChange: [filters: FilterState]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="assets.length === 0"
|
v-else-if="assets.length === 0"
|
||||||
class="flex h-full flex-col items-center justify-center py-16 text-muted-foreground"
|
class="flex h-full select-none flex-col items-center justify-center py-16 text-muted-foreground"
|
||||||
>
|
>
|
||||||
<i class="mb-4 icon-[lucide--search] size-10" />
|
<i class="mb-4 icon-[lucide--search] size-10" />
|
||||||
<h3 class="mb-2 text-lg font-medium">
|
<h3 class="mb-2 text-lg font-medium">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2 px-4 py-2 text-sm text-base-foreground">
|
<div class="flex flex-col gap-2 px-4 py-2 text-sm text-base-foreground">
|
||||||
<div class="flex items-center justify-between relative">
|
<div class="flex items-center justify-between relative">
|
||||||
<span>{{ label }}</span>
|
<span class="select-none">{{ label }}</span>
|
||||||
<slot name="label-action" />
|
<slot name="label-action" />
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
>
|
>
|
||||||
<PropertiesAccordionItem :class="accordionClass">
|
<PropertiesAccordionItem :class="accordionClass">
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-xs uppercase font-inter">
|
<span class="text-xs uppercase font-inter select-none">
|
||||||
{{ t('assetBrowser.modelInfo.basicInfo') }}
|
{{ t('assetBrowser.modelInfo.basicInfo') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
<PropertiesAccordionItem :class="accordionClass">
|
<PropertiesAccordionItem :class="accordionClass">
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-xs uppercase font-inter">
|
<span class="text-xs uppercase font-inter select-none">
|
||||||
{{ t('assetBrowser.modelInfo.modelTagging') }}
|
{{ t('assetBrowser.modelInfo.modelTagging') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
|
|
||||||
<PropertiesAccordionItem :class="accordionClass">
|
<PropertiesAccordionItem :class="accordionClass">
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-xs uppercase font-inter">
|
<span class="text-xs uppercase font-inter select-none">
|
||||||
{{ t('assetBrowser.modelInfo.modelDescription') }}
|
{{ t('assetBrowser.modelInfo.modelDescription') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -279,12 +279,15 @@ export const useAssetsStore = defineStore('assets', () => {
|
|||||||
|
|
||||||
const pendingRequestByKey = new Map<string, ModelPaginationState>()
|
const pendingRequestByKey = new Map<string, ModelPaginationState>()
|
||||||
|
|
||||||
function createState(): ModelPaginationState {
|
function createState(
|
||||||
|
existingAssets?: Map<string, AssetItem>
|
||||||
|
): ModelPaginationState {
|
||||||
|
const assets = new Map(existingAssets)
|
||||||
return reactive({
|
return reactive({
|
||||||
assets: new Map(),
|
assets,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
isLoading: false
|
isLoading: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,8 +339,10 @@ export const useAssetsStore = defineStore('assets', () => {
|
|||||||
key: string,
|
key: string,
|
||||||
fetcher: (options: PaginationOptions) => Promise<AssetItem[]>
|
fetcher: (options: PaginationOptions) => Promise<AssetItem[]>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const state = createState()
|
const existingState = modelStateByKey.value.get(key)
|
||||||
state.isLoading = true
|
const state = createState(existingState?.assets)
|
||||||
|
|
||||||
|
const seenIds = new Set<string>()
|
||||||
|
|
||||||
const hasExistingData = modelStateByKey.value.has(key)
|
const hasExistingData = modelStateByKey.value.has(key)
|
||||||
if (hasExistingData) {
|
if (hasExistingData) {
|
||||||
@@ -363,19 +368,15 @@ export const useAssetsStore = defineStore('assets', () => {
|
|||||||
pendingRequestByKey.delete(key)
|
pendingRequestByKey.delete(key)
|
||||||
modelStateByKey.value.set(key, state)
|
modelStateByKey.value.set(key, state)
|
||||||
}
|
}
|
||||||
state.assets = new Map(newAssets.map((a) => [a.id, a]))
|
|
||||||
} else {
|
|
||||||
const assetsToAdd = newAssets.filter(
|
|
||||||
(a) => !state.assets.has(a.id)
|
|
||||||
)
|
|
||||||
if (assetsToAdd.length > 0) {
|
|
||||||
assetsArrayCache.delete(key)
|
|
||||||
for (const asset of assetsToAdd) {
|
|
||||||
state.assets.set(asset.id, asset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge new assets into existing map and track seen IDs
|
||||||
|
for (const asset of newAssets) {
|
||||||
|
seenIds.add(asset.id)
|
||||||
|
state.assets.set(asset.id, asset)
|
||||||
|
}
|
||||||
|
state.assets = new Map(state.assets)
|
||||||
|
|
||||||
state.offset += newAssets.length
|
state.offset += newAssets.length
|
||||||
state.hasMore = newAssets.length === MODEL_BATCH_SIZE
|
state.hasMore = newAssets.length === MODEL_BATCH_SIZE
|
||||||
|
|
||||||
@@ -388,17 +389,24 @@ export const useAssetsStore = defineStore('assets', () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isStale(key, state)) return
|
if (isStale(key, state)) return
|
||||||
|
console.error(`Error loading batch for ${key}:`, err)
|
||||||
|
|
||||||
state.error = err instanceof Error ? err : new Error(String(err))
|
state.error = err instanceof Error ? err : new Error(String(err))
|
||||||
state.hasMore = false
|
state.hasMore = false
|
||||||
console.error(`Error loading batch for ${key}:`, err)
|
state.isLoading = false
|
||||||
if (state.offset === 0) {
|
pendingRequestByKey.delete(key)
|
||||||
state.isLoading = false
|
|
||||||
pendingRequestByKey.delete(key)
|
|
||||||
// TODO: Add toast indicator for first-batch load failures
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const staleIds = [...state.assets.keys()].filter(
|
||||||
|
(id) => !seenIds.has(id)
|
||||||
|
)
|
||||||
|
for (const id of staleIds) {
|
||||||
|
state.assets.delete(id)
|
||||||
|
}
|
||||||
|
assetsArrayCache.delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadBatches()
|
await loadBatches()
|
||||||
|
|||||||
@@ -4,15 +4,12 @@
|
|||||||
:content-title="$t('manager.discoverCommunityContent')"
|
:content-title="$t('manager.discoverCommunityContent')"
|
||||||
class="manager-dialog"
|
class="manager-dialog"
|
||||||
>
|
>
|
||||||
|
<template #leftPanelHeaderTitle>
|
||||||
|
<i class="icon-[lucide--puzzle]" />
|
||||||
|
<h2 class="text-neutral text-base">{{ $t('manager.title') }}</h2>
|
||||||
|
</template>
|
||||||
<template #leftPanel>
|
<template #leftPanel>
|
||||||
<LeftSidePanel v-model="selectedNavId" :nav-items="navItems">
|
<LeftSidePanel v-model="selectedNavId" :nav-items="navItems" />
|
||||||
<template #header-icon>
|
|
||||||
<i class="icon-[lucide--puzzle]" />
|
|
||||||
</template>
|
|
||||||
<template #header-title>
|
|
||||||
<span class="text-neutral text-base">{{ $t('manager.title') }}</span>
|
|
||||||
</template>
|
|
||||||
</LeftSidePanel>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|||||||
Reference in New Issue
Block a user