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:
Alexander Brown
2026-01-23 20:41:35 -08:00
committed by GitHub
parent e5583fe955
commit 15655ddb76
19 changed files with 143 additions and 207 deletions

View File

@@ -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()

View File

@@ -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>

View File

@@ -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'

View File

@@ -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>

View File

@@ -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()

View File

@@ -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'

View File

@@ -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>
` `
}) })

View File

@@ -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

View File

@@ -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>

View File

@@ -1,5 +0,0 @@
<template>
<div class="size-full bg-modal-panel-background pr-6 pb-8 pl-4">
<slot></slot>
</div>
</template>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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'

View File

@@ -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]

View File

@@ -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">

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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()

View File

@@ -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>