From c5970bb610fbcfcea01a2a1b931b1cf5bf427ac5 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 4 Jul 2025 12:13:22 +0100 Subject: [PATCH 01/28] chore: add ii8n and keywords --- src/locales/en/main.json | 8 +++++++- src/locales/en/settings.json | 9 +++++++++ src/locales/es/main.json | 8 +++++++- src/locales/es/settings.json | 9 +++++++++ src/locales/fr/main.json | 8 +++++++- src/locales/fr/settings.json | 9 +++++++++ src/locales/ja/main.json | 8 +++++++- src/locales/ja/settings.json | 9 +++++++++ src/locales/ko/main.json | 8 +++++++- src/locales/ko/settings.json | 9 +++++++++ src/locales/ru/main.json | 8 +++++++- src/locales/ru/settings.json | 9 +++++++++ src/locales/zh/main.json | 8 +++++++- src/locales/zh/settings.json | 9 +++++++++ 14 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 10192509c9..82e4a43882 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -437,6 +437,10 @@ } }, "modelLibrary": "Model Library", + "modelLibraryTab": { + "recentlyAddedModels": "Recently Added Models", + "recentlyUsedModels": "Recently Used Models" + }, "downloads": "Downloads", "queueTab": { "showFlatList": "Show Flat List", @@ -464,7 +468,9 @@ "workflowTreeType": { "browse": "Browse", "bookmarks": "Bookmarks", - "open": "Open" + "open": "Open", + "recentlyAddedWorkflows": "Recently Added Workflows", + "recentlyUsedWorkflows": "Recently Used Workflows" } } }, diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index ffe13f2259..6624bb704e 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -303,6 +303,15 @@ "Comfy_Sidebar_UnifiedWidth": { "name": "Unified sidebar width" }, + "Comfy_Sidebar_RecentItems_MaxCount": { + "name": "Sidebar recent items maximum count" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyAdded": { + "name": "Sidebar show recently added items" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyUsed": { + "name": "Sidebar show recently used items" + }, "Comfy_SnapToGrid_GridSize": { "name": "Snap to grid size", "tooltip": "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid." diff --git a/src/locales/es/main.json b/src/locales/es/main.json index c2bb16342c..ced75b1ea4 100644 --- a/src/locales/es/main.json +++ b/src/locales/es/main.json @@ -1081,6 +1081,10 @@ "downloads": "Descargas", "logout": "Cerrar sesión", "modelLibrary": "Biblioteca de modelos", + "modelLibraryTab": { + "recentlyAddedModels": "Modelos añadidos recientemente", + "recentlyUsedModels": "Modelos usados recientemente" + }, "newBlankWorkflow": "Crear un nuevo flujo de trabajo en blanco", "nodeLibrary": "Biblioteca de nodos", "nodeLibraryTab": { @@ -1131,7 +1135,9 @@ "workflowTreeType": { "bookmarks": "Marcadores", "browse": "Explorar", - "open": "Abrir" + "open": "Abrir", + "recentlyAddedWorkflows": "Flujos de trabajo añadidos recientemente", + "recentlyUsedWorkflows": "Flujos de trabajo usados recientemente" } }, "workflows": "Flujos de trabajo" diff --git a/src/locales/es/settings.json b/src/locales/es/settings.json index 15dbfd1ac1..fe00399867 100644 --- a/src/locales/es/settings.json +++ b/src/locales/es/settings.json @@ -293,6 +293,15 @@ "right": "derecha" } }, + "Comfy_Sidebar_RecentItems_MaxCount": { + "name": "Cantidad máxima de elementos recientes en la barra lateral" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyAdded": { + "name": "Barra lateral mostrar elementos añadidos recientemente" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyUsed": { + "name": "Barra lateral mostrar elementos usados recientemente" + }, "Comfy_Sidebar_Size": { "name": "Tamaño de la barra lateral", "options": { diff --git a/src/locales/fr/main.json b/src/locales/fr/main.json index 038c349a81..852a07f20c 100644 --- a/src/locales/fr/main.json +++ b/src/locales/fr/main.json @@ -1081,6 +1081,10 @@ "downloads": "Téléchargements", "logout": "Déconnexion", "modelLibrary": "Bibliothèque de modèles", + "modelLibraryTab": { + "recentlyAddedModels": "Modèles récemment ajoutés", + "recentlyUsedModels": "Modèles récemment utilisés" + }, "newBlankWorkflow": "Créer un nouveau flux de travail vierge", "nodeLibrary": "Bibliothèque de nœuds", "nodeLibraryTab": { @@ -1131,7 +1135,9 @@ "workflowTreeType": { "bookmarks": "Favoris", "browse": "Parcourir", - "open": "Ouvrir" + "open": "Ouvrir", + "recentlyAddedWorkflows": "Flux de travail récemment ajoutés", + "recentlyUsedWorkflows": "Flux de travail récemment utilisés" } }, "workflows": "Flux de travail" diff --git a/src/locales/fr/settings.json b/src/locales/fr/settings.json index e80f57a5da..4f274eb559 100644 --- a/src/locales/fr/settings.json +++ b/src/locales/fr/settings.json @@ -293,6 +293,15 @@ "right": "droite" } }, + "Comfy_Sidebar_RecentItems_MaxCount": { + "name": "Nombre maximal d’éléments récents dans la barre latérale" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyAdded": { + "name": "Barre latérale afficher les éléments récemment ajoutés" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyUsed": { + "name": "Barre latérale afficher les éléments récemment utilisés" + }, "Comfy_Sidebar_Size": { "name": "Taille de la barre latérale", "options": { diff --git a/src/locales/ja/main.json b/src/locales/ja/main.json index f2b31602fe..91adf03b07 100644 --- a/src/locales/ja/main.json +++ b/src/locales/ja/main.json @@ -1081,6 +1081,10 @@ "downloads": "ダウンロード", "logout": "ログアウト", "modelLibrary": "モデルライブラリ", + "modelLibraryTab": { + "recentlyAddedModels": "最近追加されたモデル", + "recentlyUsedModels": "最近使用したモデル" + }, "newBlankWorkflow": "新しい空のワークフローを作成", "nodeLibrary": "ノードライブラリ", "nodeLibraryTab": { @@ -1131,7 +1135,9 @@ "workflowTreeType": { "bookmarks": "ブックマーク", "browse": "ブラウズ", - "open": "開く" + "open": "開く", + "recentlyAddedWorkflows": "最近追加されたワークフロー", + "recentlyUsedWorkflows": "最近使用したワークフロー" } }, "workflows": "ワークフロー" diff --git a/src/locales/ja/settings.json b/src/locales/ja/settings.json index 3f4f68ed60..66e097ea49 100644 --- a/src/locales/ja/settings.json +++ b/src/locales/ja/settings.json @@ -293,6 +293,15 @@ "right": "右" } }, + "Comfy_Sidebar_RecentItems_MaxCount": { + "name": "サイドバーの最近使用した項目の最大数" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyAdded": { + "name": "サイドバーで最近追加された項目を表示" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyUsed": { + "name": "サイドバーで最近使用した項目を表示" + }, "Comfy_Sidebar_Size": { "name": "サイドバーのサイズ", "options": { diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index 53bc556617..fde642e268 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -1081,6 +1081,10 @@ "downloads": "다운로드", "logout": "로그아웃", "modelLibrary": "모델 라이브러리", + "modelLibraryTab": { + "recentlyAddedModels": "최근 추가된 모델", + "recentlyUsedModels": "최근 사용한 모델" + }, "newBlankWorkflow": "새 빈 워크플로 만들기", "nodeLibrary": "노드 라이브러리", "nodeLibraryTab": { @@ -1131,7 +1135,9 @@ "workflowTreeType": { "bookmarks": "북마크", "browse": "찾아보기", - "open": "열기" + "open": "열기", + "recentlyAddedWorkflows": "최근 추가된 워크플로우", + "recentlyUsedWorkflows": "최근 사용한 워크플로우" } }, "workflows": "워크플로" diff --git a/src/locales/ko/settings.json b/src/locales/ko/settings.json index c3b51c843c..448611fe88 100644 --- a/src/locales/ko/settings.json +++ b/src/locales/ko/settings.json @@ -293,6 +293,15 @@ "right": "오른쪽" } }, + "Comfy_Sidebar_RecentItems_MaxCount": { + "name": "사이드바 최근 항목 최대 개수" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyAdded": { + "name": "사이드바에 최근 추가된 항목 표시" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyUsed": { + "name": "사이드바에 최근 사용 항목 표시" + }, "Comfy_Sidebar_Size": { "name": "사이드바 크기", "options": { diff --git a/src/locales/ru/main.json b/src/locales/ru/main.json index b0f48ae98f..619a891581 100644 --- a/src/locales/ru/main.json +++ b/src/locales/ru/main.json @@ -1081,6 +1081,10 @@ "downloads": "Загрузки", "logout": "Выйти", "modelLibrary": "Библиотека моделей", + "modelLibraryTab": { + "recentlyAddedModels": "Недавно добавленные модели", + "recentlyUsedModels": "Недавно использованные модели" + }, "newBlankWorkflow": "Создайте новый пустой рабочий процесс", "nodeLibrary": "Библиотека нод", "nodeLibraryTab": { @@ -1131,7 +1135,9 @@ "workflowTreeType": { "bookmarks": "Закладки", "browse": "Просмотреть", - "open": "Открыть" + "open": "Открыть", + "recentlyAddedWorkflows": "Недавно добавленные рабочие процессы", + "recentlyUsedWorkflows": "Недавно использованные рабочие процессы" } }, "workflows": "Рабочие процессы" diff --git a/src/locales/ru/settings.json b/src/locales/ru/settings.json index 2fe9624981..2342246774 100644 --- a/src/locales/ru/settings.json +++ b/src/locales/ru/settings.json @@ -293,6 +293,15 @@ "right": "справа" } }, + "Comfy_Sidebar_RecentItems_MaxCount": { + "name": "Максимальное количество недавних элементов в боковой панели" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyAdded": { + "name": "Боковая панель: показывать недавно добавленные элементы" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyUsed": { + "name": "Боковая панель: показывать недавно использованные элементы" + }, "Comfy_Sidebar_Size": { "name": "Размер боковой панели", "options": { diff --git a/src/locales/zh/main.json b/src/locales/zh/main.json index 58e7550cd4..703a790b4c 100644 --- a/src/locales/zh/main.json +++ b/src/locales/zh/main.json @@ -1081,6 +1081,10 @@ "downloads": "下载", "logout": "登出", "modelLibrary": "模型库", + "modelLibraryTab": { + "recentlyAddedModels": "最近添加的模型", + "recentlyUsedModels": "最近使用的模型" + }, "newBlankWorkflow": "创建空白工作流", "nodeLibrary": "节点库", "nodeLibraryTab": { @@ -1131,7 +1135,9 @@ "workflowTreeType": { "bookmarks": "书签", "browse": "浏览", - "open": "打开" + "open": "打开", + "recentlyAddedWorkflows": "最近添加的工作流", + "recentlyUsedWorkflows": "最近使用的工作流" } }, "workflows": "工作流" diff --git a/src/locales/zh/settings.json b/src/locales/zh/settings.json index abb4627bf7..98e5a1cda4 100644 --- a/src/locales/zh/settings.json +++ b/src/locales/zh/settings.json @@ -293,6 +293,15 @@ "right": "右侧" } }, + "Comfy_Sidebar_RecentItems_MaxCount": { + "name": "侧边栏最近项目最大数量" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyAdded": { + "name": "侧边栏显示最近添加的项目" + }, + "Comfy_Sidebar_RecentItems_ShowRecentlyUsed": { + "name": "侧边栏显示最近使用的项目" + }, "Comfy_Sidebar_Size": { "name": "侧边栏大小", "options": { From c252d56be4f2ab1fa4a2805e4ba6c79dc3ae1ae2 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 4 Jul 2025 12:45:41 +0100 Subject: [PATCH 02/28] chore: add workflow and model sidebar sections for recently used/added --- .../sidebar/tabs/ModelLibrarySidebarTab.vue | 12 +- .../sidebar/tabs/WorkflowsSidebarTab.vue | 30 ++- .../tabs/modelLibrary/RecentModelsSection.vue | 202 ++++++++++++++++++ .../tabs/workflows/RecentWorkflowsSection.vue | 160 ++++++++++++++ 4 files changed, 392 insertions(+), 12 deletions(-) create mode 100644 src/components/sidebar/tabs/modelLibrary/RecentModelsSection.vue create mode 100644 src/components/sidebar/tabs/workflows/RecentWorkflowsSection.vue diff --git a/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue b/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue index 42f6a7d765..77f5dff51d 100644 --- a/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue +++ b/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue @@ -30,6 +30,10 @@ + + +
{ return } const lowerQuery = query.toLocaleLowerCase() - filteredWorkflows.value = workflowStore.workflows.filter((workflow) => { - return workflow.path.toLocaleLowerCase().includes(lowerQuery) - }) + filteredWorkflows.value = workflowStore.workflows.filter( + (workflow: ComfyWorkflow) => { + return workflow.path.toLocaleLowerCase().includes(lowerQuery) + } + ) await nextTick() expandNode(filteredRoot.value) } @@ -195,12 +205,6 @@ const handleCloseWorkflow = async (workflow?: ComfyWorkflow) => { } } -enum WorkflowTreeType { - Open = 'Open', - Bookmarks = 'Bookmarks', - Browse = 'Browse' -} - const buildWorkflowTree = (workflows: ComfyWorkflow[]) => { return buildTree(workflows, (workflow: ComfyWorkflow) => workflow.key.split('/') @@ -214,11 +218,15 @@ const workflowsTree = computed(() => ) // Bookmarked workflows tree is flat. const bookmarkedWorkflowsTree = computed(() => - buildTree(workflowStore.bookmarkedWorkflows, (workflow) => [workflow.key]) + buildTree(workflowStore.bookmarkedWorkflows, (workflow: ComfyWorkflow) => [ + workflow.key + ]) ) // Open workflows tree is flat. const openWorkflowsTree = computed(() => - buildTree(workflowStore.openWorkflows, (workflow) => [workflow.key]) + buildTree(workflowStore.openWorkflows, (workflow: ComfyWorkflow) => [ + workflow.key + ]) ) const renderTreeNode = ( diff --git a/src/components/sidebar/tabs/modelLibrary/RecentModelsSection.vue b/src/components/sidebar/tabs/modelLibrary/RecentModelsSection.vue new file mode 100644 index 0000000000..45bcc1ba17 --- /dev/null +++ b/src/components/sidebar/tabs/modelLibrary/RecentModelsSection.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/src/components/sidebar/tabs/workflows/RecentWorkflowsSection.vue b/src/components/sidebar/tabs/workflows/RecentWorkflowsSection.vue new file mode 100644 index 0000000000..dff99891af --- /dev/null +++ b/src/components/sidebar/tabs/workflows/RecentWorkflowsSection.vue @@ -0,0 +1,160 @@ + + + + + From 411eba86b61eee60b3c1c00c30bc39c60c8cfe10 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 4 Jul 2025 12:49:19 +0100 Subject: [PATCH 03/28] settings: add relevant sidebar settings for recently used/added --- src/constants/coreSettings.ts | 21 +++++++++++++++++++++ src/schemas/apiSchema.ts | 6 +++++- src/scripts/api.ts | 12 +++++++++--- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/constants/coreSettings.ts b/src/constants/coreSettings.ts index ec554f632b..0ee31c28cb 100644 --- a/src/constants/coreSettings.ts +++ b/src/constants/coreSettings.ts @@ -102,6 +102,27 @@ export const CORE_SETTINGS: SettingParams[] = [ defaultValue: true, versionAdded: '1.18.1' }, + { + id: 'Comfy.Sidebar.RecentItems.MaxCount', + category: ['Appearance', 'Sidebar', 'RecentItems', 'MaxCount'], + name: 'Max number of sidebar recent items', + type: 'number', + defaultValue: 5 + }, + { + id: 'Comfy.Sidebar.RecentItems.ShowRecentlyUsed', + category: ['Appearance', 'Sidebar', 'RecentItems', 'ShowRecentlyUsed'], + name: 'Show recently used items in sidebar', + type: 'boolean', + defaultValue: true + }, + { + id: 'Comfy.Sidebar.RecentItems.ShowRecentlyAdded', + category: ['Appearance', 'Sidebar', 'RecentItems', 'ShowRecentlyAdded'], + name: 'Show recently added items in sidebar', + type: 'boolean', + defaultValue: true + }, { id: 'Comfy.TextareaWidget.FontSize', category: ['Appearance', 'Node Widget', 'TextareaWidget', 'FontSize'], diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index c93e3e4511..a09425ed2b 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -334,7 +334,8 @@ const zUserData = z.array(z.array(z.string(), z.string())) const zUserDataFullInfo = z.object({ path: z.string(), size: z.number(), - modified: z.number() + modified: z.number(), + created: z.number().optional() }) const zBookmarkCustomization = z.object({ icon: z.string().optional(), @@ -404,6 +405,9 @@ const zSettings = z.object({ 'Comfy.Sidebar.Location': z.enum(['left', 'right']), 'Comfy.Sidebar.Size': z.enum(['small', 'normal']), 'Comfy.Sidebar.UnifiedWidth': z.boolean(), + 'Comfy.Sidebar.RecentItems.MaxCount': z.number(), + 'Comfy.Sidebar.RecentItems.ShowRecentlyAdded': z.boolean(), + 'Comfy.Sidebar.RecentItems.ShowRecentlyUsed': z.boolean(), 'Comfy.SwitchUser': z.any(), 'Comfy.SnapToGrid.GridSize': z.number(), 'Comfy.TextareaWidget.FontSize': z.number(), diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 85316a74a7..b90d9fc72b 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -623,9 +623,15 @@ export class ComfyApi extends EventTarget { * @param {string} folder The folder to list models from, such as 'checkpoints' * @returns The list of model filenames within the specified folder */ - async getModels( - folder: string - ): Promise<{ name: string; pathIndex: number }[]> { + async getModels(folder: string): Promise< + { + name: string + pathIndex: number + created: number + modified: number + size: number + }[] + > { const res = await this.fetchApi(`/experiment/models/${folder}`) if (res.status === 404) { return [] From a7be5c45a7d428c800eabd4c70a657ae14f9264b Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 4 Jul 2025 12:54:11 +0100 Subject: [PATCH 04/28] api: add relevant fields to track creation/modified time on stores --- src/stores/modelStore.ts | 23 +++++++++++++++++++++-- src/stores/userFileStore.ts | 9 +++++++-- src/stores/workflowStore.ts | 23 +++++++++++++++++++---- tests-ui/tests/store/modelStore.test.ts | 24 +++++++++++++++++++++--- 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/stores/modelStore.ts b/src/stores/modelStore.ts index dc03c61452..57a8d792fb 100644 --- a/src/stores/modelStore.ts +++ b/src/stores/modelStore.ts @@ -32,6 +32,12 @@ export class ComfyModelDef { readonly simplified_file_name: string /** Key for the model, used to uniquely identify the model. */ readonly key: string + /** The last time the model was modified: from backend os.mtime */ + readonly last_modified: number + /** The time the model was modified: from backend os.ctime */ + readonly date_created: number + /** The size of the model in bytes */ + readonly file_size: number /** Title / display name of the model, sometimes same as the name but not always */ title: string /** Metadata: architecture ID for the model, such as 'stable-diffusion-xl-v1-base' */ @@ -57,7 +63,14 @@ export class ComfyModelDef { /** A string full of auto-computed lowercase-only searchable text for this model */ searchable: string = '' - constructor(name: string, directory: string, pathIndex: number) { + constructor( + name: string, + directory: string, + pathIndex: number, + dateCreated: number, + lastModified: number, + fileSize: number + ) { this.path_index = pathIndex this.file_name = name this.normalized_file_name = name.replaceAll('\\', '/') @@ -72,6 +85,9 @@ export class ComfyModelDef { this.directory = directory this.key = `${directory}/${this.normalized_file_name}` this.updateSearchable() + this.last_modified = lastModified // from backend os.mtime + this.date_created = dateCreated // from backend os.mtime + this.file_size = fileSize } updateSearchable() { @@ -172,7 +188,10 @@ export class ModelFolder { this.models[`${model.pathIndex}/${model.name}`] = new ComfyModelDef( model.name, this.directory, - model.pathIndex + model.pathIndex, + model.created, + model.modified, + model.size ) } this.state = ResourceState.Loaded diff --git a/src/stores/userFileStore.ts b/src/stores/userFileStore.ts index e7c8728f05..0423920d8f 100644 --- a/src/stores/userFileStore.ts +++ b/src/stores/userFileStore.ts @@ -42,7 +42,11 @@ export class UserFile { /** * File size in bytes. -1 for temporary files. */ - public size: number + public size: number, + /** + * Created at timestamp. + */ + public created: number = Date.now() ) { const details = getPathDetails(path) this.path = path @@ -214,10 +218,11 @@ export const useUserFileStore = defineStore('userFile', () => { await syncEntities( dir, userFilesByPath.value, - (file) => new UserFile(file.path, file.modified, file.size), + (file) => new UserFile(file.path, file.modified, file.size, file.created), (existingFile, file) => { existingFile.lastModified = file.modified existingFile.size = file.size + existingFile.created = file.created existingFile.unload() } ) diff --git a/src/stores/workflowStore.ts b/src/stores/workflowStore.ts index 105eeadb0c..8d0e18cf4f 100644 --- a/src/stores/workflowStore.ts +++ b/src/stores/workflowStore.ts @@ -29,8 +29,13 @@ export class ComfyWorkflow extends UserFile { * @param options The path, modified, and size of the workflow. * Note: path is the full path, including the 'workflows/' prefix. */ - constructor(options: { path: string; modified: number; size: number }) { - super(options.path, options.modified, options.size) + constructor(options: { + path: string + modified: number + size: number + created: number + }) { + super(options.path, options.modified, options.size, options.created) } override get key() { @@ -164,6 +169,14 @@ export interface WorkflowStore { updateActiveGraph: () => void } +export enum WorkflowTreeType { + Open = 'Open', + Bookmarks = 'Bookmarks', + Browse = 'Browse', + RecentlyAddedWorkflows = 'RecentlyAddedWorkflows', + RecentlyUsedWorkflows = 'RecentlyUsedWorkflows' +} + export const useWorkflowStore = defineStore('workflow', () => { /** * Detach the workflow from the store. lightweight helper function. @@ -293,7 +306,8 @@ export const useWorkflowStore = defineStore('workflow', () => { const workflow = new ComfyWorkflow({ path: fullPath, modified: Date.now(), - size: -1 + size: -1, + created: Date.now() }) workflow.originalContent = workflow.content = workflowData @@ -346,7 +360,8 @@ export const useWorkflowStore = defineStore('workflow', () => { new ComfyWorkflow({ path: file.path, modified: file.modified, - size: file.size + size: file.size, + created: file.created }), (existingWorkflow, file) => { existingWorkflow.lastModified = file.modified diff --git a/tests-ui/tests/store/modelStore.test.ts b/tests-ui/tests/store/modelStore.test.ts index 6964b04e82..c21ae499cf 100644 --- a/tests-ui/tests/store/modelStore.test.ts +++ b/tests-ui/tests/store/modelStore.test.ts @@ -15,9 +15,27 @@ vi.mock('@/scripts/api', () => ({ function enableMocks() { vi.mocked(api.getModels).mockResolvedValue([ - { name: 'sdxl.safetensors', pathIndex: 0 }, - { name: 'sdv15.safetensors', pathIndex: 0 }, - { name: 'noinfo.safetensors', pathIndex: 0 } + { + name: 'sdxl.safetensors', + pathIndex: 0, + created: 1700000000, + modified: 1700000000, + size: 12345678 + }, + { + name: 'sdv15.safetensors', + pathIndex: 0, + created: 1700000000, + modified: 1700000000, + size: 23456789 + }, + { + name: 'noinfo.safetensors', + pathIndex: 0, + created: 1700000000, + modified: 1700000000, + size: 34567890 + } ]) vi.mocked(api.getModelFolders).mockResolvedValue([ { name: 'checkpoints', folders: ['/path/to/checkpoints'] }, From 04b674b5102c617d5f5ed0d49c43e50c5b3ff9b8 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 4 Jul 2025 12:57:14 +0100 Subject: [PATCH 05/28] feature: implement recentItems store --- src/stores/README.md | 1 + src/stores/recentItemsStore.ts | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/stores/recentItemsStore.ts diff --git a/src/stores/README.md b/src/stores/README.md index 3bf200aa60..bb90e21c25 100644 --- a/src/stores/README.md +++ b/src/stores/README.md @@ -121,6 +121,7 @@ The core stores include: | modelStore.ts | Manages AI models information | | nodeDefStore.ts | Manages node definitions | | queueStore.ts | Handles the execution queue | +| recentItemsStore.ts | Tracks recently used workflows and models | | settingStore.ts | Manages application settings | | userStore.ts | Manages user data and preferences | | workflowStore.ts | Handles workflow data and operations | diff --git a/src/stores/recentItemsStore.ts b/src/stores/recentItemsStore.ts new file mode 100644 index 0000000000..90781dccfa --- /dev/null +++ b/src/stores/recentItemsStore.ts @@ -0,0 +1,60 @@ +import { defineStore } from 'pinia' +import { computed } from 'vue' + +import { useModelStore } from './modelStore' +import { useSettingStore } from './settingStore' +import { useWorkflowStore } from './workflowStore' + +export const useRecentItemsStore = defineStore('recentItems', () => { + const workflowStore = useWorkflowStore() + const modelStore = useModelStore() + const settings = useSettingStore() + const maxRecentItemCount = settings.get('Comfy.Sidebar.RecentItems.MaxCount') + + // Computed properties for "Recently Added" based on file timestamps + const recentlyAddedWorkflows = computed(() => { + const workflows = workflowStore.workflows + if (workflows.length === 0) { + return [] + } + + // Sort by dateCreated and pick the first 5 + return workflows + .sort((a, b) => b.created - a.created) + .slice(0, maxRecentItemCount) + }) + + const recentlyAddedModels = computed(() => { + const models = modelStore.models + if (models.length === 0) { + return [] + } + // Sort by dateCreated and pick the first 5 + return models + .sort((a, b) => b.date_created - a.date_created) + .slice(0, maxRecentItemCount) + }) + + const recentlyUsedWorkflows = computed(() => { + return workflowStore.workflows + .sort((a, b) => { + return (b.lastModified ?? 0) - (a.lastModified ?? 0) + }) + .slice(0, maxRecentItemCount) + }) + + const recentlyUsedModels = computed(() => { + return modelStore.models + .sort((a, b) => { + return (b.last_modified ?? 0) - (a.last_modified ?? 0) + }) + .slice(0, maxRecentItemCount) + }) + + return { + recentlyUsedWorkflows, + recentlyUsedModels, + recentlyAddedWorkflows, + recentlyAddedModels + } +}) From 571d27f892d9ba3afb1f12825bd2029507b168a4 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 4 Jul 2025 14:43:25 +0100 Subject: [PATCH 06/28] refactor: update UserFile class to require created timestamp and adjust tests accordingly --- src/stores/userFileStore.ts | 4 ++-- tests-ui/tests/store/userFileStore.test.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/stores/userFileStore.ts b/src/stores/userFileStore.ts index 0423920d8f..8402cc1ede 100644 --- a/src/stores/userFileStore.ts +++ b/src/stores/userFileStore.ts @@ -46,7 +46,7 @@ export class UserFile { /** * Created at timestamp. */ - public created: number = Date.now() + public created: number ) { const details = getPathDetails(path) this.path = path @@ -66,7 +66,7 @@ export class UserFile { } static createTemporary(path: string): UserFile { - return new UserFile(path, Date.now(), -1) + return new UserFile(path, Date.now(), -1, Date.now()) } get isTemporary() { diff --git a/tests-ui/tests/store/userFileStore.test.ts b/tests-ui/tests/store/userFileStore.test.ts index dad0d3fa14..1ef061c6de 100644 --- a/tests-ui/tests/store/userFileStore.test.ts +++ b/tests-ui/tests/store/userFileStore.test.ts @@ -90,7 +90,7 @@ describe('useUserFileStore', () => { describe('UserFile', () => { describe('load', () => { it('should load file content', async () => { - const file = new UserFile('file1.txt', 123, 100) + const file = new UserFile('file1.txt', 123, 100, 123) vi.mocked(api.getUserData).mockResolvedValue({ status: 200, text: () => Promise.resolve('file content') @@ -105,7 +105,7 @@ describe('useUserFileStore', () => { }) it('should throw error on failed load', async () => { - const file = new UserFile('file1.txt', 123, 100) + const file = new UserFile('file1.txt', 123, 100, 123) vi.mocked(api.getUserData).mockResolvedValue({ status: 404, statusText: 'Not Found' @@ -119,7 +119,7 @@ describe('useUserFileStore', () => { describe('save', () => { it('should save modified file', async () => { - const file = new UserFile('file1.txt', 123, 100) + const file = new UserFile('file1.txt', 123, 100, 123) file.content = 'modified content' file.originalContent = 'original content' vi.mocked(api.storeUserData).mockResolvedValue({ @@ -139,7 +139,7 @@ describe('useUserFileStore', () => { }) it('should not save unmodified file', async () => { - const file = new UserFile('file1.txt', 123, 100) + const file = new UserFile('file1.txt', 123, 100, 123) file.content = 'content' file.originalContent = 'content' @@ -151,7 +151,7 @@ describe('useUserFileStore', () => { describe('delete', () => { it('should delete file', async () => { - const file = new UserFile('file1.txt', 123, 100) + const file = new UserFile('file1.txt', 123, 100, 123) vi.mocked(api.deleteUserData).mockResolvedValue({ status: 204 } as Response) @@ -164,7 +164,7 @@ describe('useUserFileStore', () => { describe('rename', () => { it('should rename file', async () => { - const file = new UserFile('file1.txt', 123, 100) + const file = new UserFile('file1.txt', 123, 100, 123) vi.mocked(api.moveUserData).mockResolvedValue({ status: 200, json: () => Promise.resolve({ modified: 456, size: 200 }) @@ -184,7 +184,7 @@ describe('useUserFileStore', () => { describe('saveAs', () => { it('should save file with new path', async () => { - const file = new UserFile('file1.txt', 123, 100) + const file = new UserFile('file1.txt', 123, 100, 123) file.content = 'file content' vi.mocked(api.storeUserData).mockResolvedValue({ status: 200, From 0ea60d765ad3fe667e5578f030ae61ca7ddc0b95 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Fri, 4 Jul 2025 14:43:45 +0100 Subject: [PATCH 07/28] test: add unit tests for useRecentItemsStore functionality --- tests-ui/tests/store/recentItemsStore.test.ts | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 tests-ui/tests/store/recentItemsStore.test.ts diff --git a/tests-ui/tests/store/recentItemsStore.test.ts b/tests-ui/tests/store/recentItemsStore.test.ts new file mode 100644 index 0000000000..561bb4ee42 --- /dev/null +++ b/tests-ui/tests/store/recentItemsStore.test.ts @@ -0,0 +1,361 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { api } from '@/scripts/api' +import { useModelStore } from '@/stores/modelStore' +import { useRecentItemsStore } from '@/stores/recentItemsStore' +import { useWorkflowStore } from '@/stores/workflowStore' + +// Mock the api +vi.mock('@/scripts/api', () => ({ + api: { + getModels: vi.fn(), + getModelFolders: vi.fn(), + viewMetadata: vi.fn(), + getUserData: vi.fn(), + storeUserData: vi.fn(), + listUserDataFullInfo: vi.fn(), + apiURL: vi.fn(), + addEventListener: vi.fn(), + getSettings: vi.fn(), + storeSetting: vi.fn() + } +})) + +// Mock the app +vi.mock('@/scripts/app', () => ({ + app: { + canvas: null, + ui: { + settings: { + dispatchChange: vi.fn() + } + } + } +})) + +// Mock the stores +vi.mock('@/stores/settingStore') + +describe('useRecentItemsStore', () => { + let recentItemsStore: ReturnType + let workflowStore: ReturnType + let modelStore: ReturnType + let mockSettingStore: any + + const mockModels = [ + { + name: 'model1.safetensors', + pathIndex: 0, + created: 1700000000, + modified: 1700000000, + size: 12345678, + date_created: 1700000000, + last_modified: 1700000100 + }, + { + name: 'model2.safetensors', + pathIndex: 0, + created: 1700000200, + modified: 1700000200, + size: 23456789, + date_created: 1700000200, + last_modified: 1700000300 + }, + { + name: 'model3.safetensors', + pathIndex: 0, + created: 1700000400, + modified: 1700000400, + size: 34567890, + date_created: 1700000400, + last_modified: 1700000050 + } + ] + + const mockWorkflows = [ + { + path: 'workflows/workflow1.json', + modified: 1700000000, + size: 1024, + created: 1700000000, + lastModified: 1700000100 + }, + { + path: 'workflows/workflow2.json', + modified: 1700000200, + size: 2048, + created: 1700000200, + lastModified: 1700000300 + }, + { + path: 'workflows/workflow3.json', + modified: 1700000400, + size: 4096, + created: 1700000400, + lastModified: 1700000050 + } + ] + + beforeEach(async () => { + setActivePinia(createPinia()) + + // Clear all mocks + vi.clearAllMocks() + + // Setup mock setting store + mockSettingStore = { + get: vi.fn((key: string) => { + if (key === 'Comfy.Sidebar.RecentItems.MaxCount') { + return 5 + } + return null + }), + set: vi.fn(), + loadSettingValues: vi.fn(), + addSetting: vi.fn() + } + + // Setup mock implementations + const { useSettingStore } = await import('@/stores/settingStore') + vi.mocked(useSettingStore).mockReturnValue(mockSettingStore) + + // Initialize stores + workflowStore = useWorkflowStore() + modelStore = useModelStore() + recentItemsStore = useRecentItemsStore() + + // Mock settings + vi.mocked(api.getSettings).mockResolvedValue({ + 'Comfy.Sidebar.RecentItems.MaxCount': 5 + } as any) + + // Setup setting store with max count setting + await mockSettingStore.loadSettingValues() + mockSettingStore.addSetting({ + id: 'Comfy.Sidebar.RecentItems.MaxCount', + name: 'Max Recent Items Count', + type: 'number', + defaultValue: 5 + }) + + // Mock model store + vi.mocked(api.getModels).mockResolvedValue(mockModels) + vi.mocked(api.getModelFolders).mockResolvedValue([ + { name: 'checkpoints', folders: ['/path/to/checkpoints'] } + ]) + vi.mocked(api.viewMetadata).mockResolvedValue({}) + + // Mock workflow store + vi.mocked(api.listUserDataFullInfo).mockResolvedValue( + mockWorkflows.map((workflow) => ({ + path: workflow.path, + modified: workflow.modified, + size: workflow.size, + created: workflow.created + })) + ) + vi.mocked(api.getUserData).mockResolvedValue({ + status: 200, + json: () => Promise.resolve({ favorites: [] }) + } as Response) + vi.mocked(api.storeUserData).mockResolvedValue({ + status: 200 + } as Response) + }) + + describe('recentlyAddedWorkflows', () => { + it('should return empty array when no workflows exist', () => { + expect(recentItemsStore.recentlyAddedWorkflows).toEqual([]) + }) + + it('should return workflows sorted by creation date (newest first)', async () => { + await workflowStore.syncWorkflows() + const recentWorkflows = recentItemsStore.recentlyAddedWorkflows + + expect(recentWorkflows).toHaveLength(3) + expect(recentWorkflows[0].created).toBe(1700000400) // workflow3 - newest + expect(recentWorkflows[1].created).toBe(1700000200) // workflow2 + expect(recentWorkflows[2].created).toBe(1700000000) // workflow1 - oldest + }) + + it('should limit results to maxRecentItemCount', async () => { + // Add more mock workflows than the limit + const manyWorkflows = Array.from({ length: 10 }, (_, i) => ({ + path: `workflows/workflow${i}.json`, + modified: 1700000000 + i * 1000, + size: 1024, + created: 1700000000 + i * 1000 + })) + + vi.mocked(api.listUserDataFullInfo).mockResolvedValue( + manyWorkflows.map((workflow) => ({ + path: workflow.path, + modified: workflow.modified, + size: workflow.size + })) + ) + + await workflowStore.syncWorkflows() + recentItemsStore = useRecentItemsStore() + const recentWorkflows = recentItemsStore.recentlyAddedWorkflows + + expect(recentWorkflows).toHaveLength(5) // Should be limited to maxRecentItemCount + }) + }) + + describe('recentlyAddedModels', () => { + it('should return empty array when no models exist', () => { + expect(recentItemsStore.recentlyAddedModels).toEqual([]) + }) + + it('should return models sorted by date_created (newest first)', async () => { + await modelStore.loadModelFolders() + await modelStore.getLoadedModelFolder('checkpoints') + const recentModels = recentItemsStore.recentlyAddedModels + + expect(recentModels).toHaveLength(3) + expect(recentModels[0].date_created).toBe(1700000400) // model3 - newest + expect(recentModels[1].date_created).toBe(1700000200) // model2 + expect(recentModels[2].date_created).toBe(1700000000) // model1 - oldest + }) + + it('should limit results to maxRecentItemCount', async () => { + // Mock more models than the limit + const manyModels = Array.from({ length: 10 }, (_, i) => ({ + name: `model${i}.safetensors`, + pathIndex: 0, + created: 1700000000 + i * 1000, + modified: 1700000000 + i * 1000, + size: 12345678, + date_created: 1700000000 + i * 1000, + last_modified: 1700000000 + i * 1000 + })) + + vi.mocked(api.getModels).mockResolvedValue(manyModels) + + await modelStore.loadModelFolders() + await modelStore.getLoadedModelFolder('checkpoints') + const recentModels = recentItemsStore.recentlyAddedModels + + expect(recentModels).toHaveLength(5) // Should be limited to maxRecentItemCount + }) + }) + + describe('recentlyUsedWorkflows', () => { + it('should return empty array when no workflows exist', () => { + expect(recentItemsStore.recentlyUsedWorkflows).toEqual([]) + }) + + it('should return workflows sorted by lastModified (most recent first)', async () => { + await workflowStore.syncWorkflows() + const recentWorkflows = recentItemsStore.recentlyUsedWorkflows + + expect(recentWorkflows).toHaveLength(3) + expect(recentWorkflows[0].lastModified).toBe(1700000400) // workflow2 - most recent + expect(recentWorkflows[1].lastModified).toBe(1700000200) // workflow1 + expect(recentWorkflows[2].lastModified).toBe(1700000000) // workflow3 - least recent + }) + + it('should handle workflows without lastModified property', async () => { + const workflowsWithoutLastModified = [ + { + path: 'workflows/workflow1.json', + modified: 1700000000, + size: 1024, + created: 1700000000 + // No lastModified property + }, + { + path: 'workflows/workflow2.json', + modified: 1700000200, + size: 2048, + created: 1700000200, + lastModified: 1700000300 + } + ] + + vi.mocked(api.listUserDataFullInfo).mockResolvedValue( + workflowsWithoutLastModified.map((workflow) => ({ + path: workflow.path, + modified: workflow.modified, + size: workflow.size, + created: workflow.created + })) + ) + + await workflowStore.syncWorkflows() + const recentWorkflows = recentItemsStore.recentlyUsedWorkflows + + expect(recentWorkflows).toHaveLength(2) + expect(recentWorkflows[0].lastModified).toBe(1700000200) + }) + }) + + describe('recentlyUsedModels', () => { + it('should return empty array when no models exist', () => { + expect(recentItemsStore.recentlyUsedModels).toEqual([]) + }) + + it('should return models sorted by last_modified (most recent first)', async () => { + await modelStore.loadModelFolders() + await modelStore.getLoadedModelFolder('checkpoints') + const recentModels = recentItemsStore.recentlyUsedModels + + expect(recentModels).toHaveLength(3) + expect(recentModels[0].last_modified).toBe(1700000400) + expect(recentModels[1].last_modified).toBe(1700000200) + expect(recentModels[2].last_modified).toBe(1700000000) + }) + + it('should handle models without last_modified property', async () => { + const modelsWithoutLastModified = [ + { + name: 'model1.safetensors', + pathIndex: 0, + created: 1700000000, + modified: 1700000000, + size: 12345678, + date_created: 1700000000 + // No last_modified property + }, + { + name: 'model2.safetensors', + pathIndex: 0, + created: 1700000200, + modified: 1700000200, + size: 23456789, + date_created: 1700000200, + last_modified: 1700000300 + } + ] + + vi.mocked(api.getModels).mockResolvedValue(modelsWithoutLastModified) + + await modelStore.loadModelFolders() + await modelStore.getLoadedModelFolder('checkpoints') + const recentModels = recentItemsStore.recentlyUsedModels + + expect(recentModels).toHaveLength(2) + expect(recentModels[0].last_modified).to.not.equal(1700000300) // shouldn't be model2 + expect(recentModels[1].last_modified).toBeDefined() // default to now() + }) + }) + + describe('store reactivity', () => { + it('should react to changes in underlying stores', async () => { + // Initially empty + expect(recentItemsStore.recentlyAddedWorkflows).toEqual([]) + expect(recentItemsStore.recentlyAddedModels).toEqual([]) + + // Load data + await workflowStore.syncWorkflows() + await modelStore.loadModelFolders() + await modelStore.loadModels() + + // Should now have data + expect(recentItemsStore.recentlyAddedWorkflows).toHaveLength(3) + expect(recentItemsStore.recentlyAddedModels).toHaveLength(3) + }) + }) +}) From 283823105d26b8a23509bf6a1b4aeccf3963c0df Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Sun, 6 Jul 2025 22:05:36 +0100 Subject: [PATCH 08/28] feat(sidebar): add RecentItemsSection component with functionality for displaying recently added and used items --- .../sidebar/tabs/RecentItemsSection.vue | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 src/components/sidebar/tabs/RecentItemsSection.vue diff --git a/src/components/sidebar/tabs/RecentItemsSection.vue b/src/components/sidebar/tabs/RecentItemsSection.vue new file mode 100644 index 0000000000..116d76b73c --- /dev/null +++ b/src/components/sidebar/tabs/RecentItemsSection.vue @@ -0,0 +1,285 @@ + + + + + From 45ad7b10eb06920c1ded7d47dcdd3f815e5ff53b Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Sun, 6 Jul 2025 22:06:58 +0100 Subject: [PATCH 09/28] refactor(sidebar): remove RecentModelsSection and RecentWorkflowsSection components --- .../tabs/modelLibrary/RecentModelsSection.vue | 202 ------------------ .../tabs/workflows/RecentWorkflowsSection.vue | 160 -------------- 2 files changed, 362 deletions(-) delete mode 100644 src/components/sidebar/tabs/modelLibrary/RecentModelsSection.vue delete mode 100644 src/components/sidebar/tabs/workflows/RecentWorkflowsSection.vue diff --git a/src/components/sidebar/tabs/modelLibrary/RecentModelsSection.vue b/src/components/sidebar/tabs/modelLibrary/RecentModelsSection.vue deleted file mode 100644 index 45bcc1ba17..0000000000 --- a/src/components/sidebar/tabs/modelLibrary/RecentModelsSection.vue +++ /dev/null @@ -1,202 +0,0 @@ - - - - - diff --git a/src/components/sidebar/tabs/workflows/RecentWorkflowsSection.vue b/src/components/sidebar/tabs/workflows/RecentWorkflowsSection.vue deleted file mode 100644 index dff99891af..0000000000 --- a/src/components/sidebar/tabs/workflows/RecentWorkflowsSection.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - From df436897b9fda69bc6fa2e83afacc6e52e661144 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Sun, 6 Jul 2025 22:07:13 +0100 Subject: [PATCH 10/28] feat(sidebar): replace RecentModelsSection and RecentWorkflowsSection with RecentItemsSection for improved item display --- .../sidebar/tabs/ModelLibrarySidebarTab.vue | 81 ++++++++++++++++--- .../sidebar/tabs/WorkflowsSidebarTab.vue | 43 +++++++++- 2 files changed, 111 insertions(+), 13 deletions(-) diff --git a/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue b/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue index 77f5dff51d..f7d7b45073 100644 --- a/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue +++ b/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue @@ -31,7 +31,29 @@
- + + +
diff --git a/src/composables/sidebarTabs/useModelPreview.ts b/src/composables/sidebarTabs/useModelPreview.ts new file mode 100644 index 0000000000..d5258665ab --- /dev/null +++ b/src/composables/sidebarTabs/useModelPreview.ts @@ -0,0 +1,75 @@ +import { CSSProperties, computed, nextTick, ref } from 'vue' + +import { ComfyModelDef } from '@/stores/modelStore' +import { useSettingStore } from '@/stores/settingStore' + +export function useModelPreview() { + const isHovered = ref(false) + const previewRef = ref(null) + const modelPreviewStyle = ref({ + position: 'absolute', + top: '0px', + left: '0px' + }) + + const settingStore = useSettingStore() + const sidebarLocation = computed<'left' | 'right'>(() => + settingStore.get('Comfy.Sidebar.Location') + ) + + const shouldShowPreview = (modelDef: ComfyModelDef) => { + return ( + isHovered.value && + modelDef && + modelDef.has_loaded_metadata && + (modelDef.author || + modelDef.simplified_file_name != modelDef.title || + modelDef.description || + modelDef.usage_hint || + modelDef.trigger_phrase || + modelDef.image) + ) + } + + const handleModelHover = async (targetElement: HTMLElement) => { + if (!targetElement) return + + const targetRect = targetElement.getBoundingClientRect() + const previewHeight = previewRef.value?.$el?.offsetHeight || 0 + const availableSpaceBelow = window.innerHeight - targetRect.bottom + + modelPreviewStyle.value.top = + previewHeight > availableSpaceBelow + ? `${Math.max(0, targetRect.top - (previewHeight - availableSpaceBelow) - 20)}px` + : `${targetRect.top - 40}px` + + if (sidebarLocation.value === 'left') { + modelPreviewStyle.value.left = `${targetRect.right}px` + } else { + modelPreviewStyle.value.left = `${targetRect.left - 400}px` + } + } + + const handleMouseEnter = async ( + targetElement: HTMLElement, + modelDef: ComfyModelDef + ) => { + isHovered.value = true + await modelDef.load() + await nextTick() + await handleModelHover(targetElement) + } + + const handleMouseLeave = () => { + isHovered.value = false + } + + return { + isHovered, + previewRef, + modelPreviewStyle, + shouldShowPreview, + handleMouseEnter, + handleMouseLeave + } +} From c227c43a95635124925bda968c8e131d7c0601dc Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Sun, 6 Jul 2025 22:07:59 +0100 Subject: [PATCH 12/28] fix(recentItemsStore): update sorting logic to use maxRecentItemCount and ensure proper filtering for created and modified dates --- src/stores/recentItemsStore.ts | 8 ++++++-- tests-ui/tests/store/recentItemsStore.test.ts | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/stores/recentItemsStore.ts b/src/stores/recentItemsStore.ts index 90781dccfa..14c4e460b0 100644 --- a/src/stores/recentItemsStore.ts +++ b/src/stores/recentItemsStore.ts @@ -18,8 +18,9 @@ export const useRecentItemsStore = defineStore('recentItems', () => { return [] } - // Sort by dateCreated and pick the first 5 + // Sort by dateCreated and pick the first {maxRecentItemCount} return workflows + .filter((a) => typeof a.created === 'number') .sort((a, b) => b.created - a.created) .slice(0, maxRecentItemCount) }) @@ -29,14 +30,16 @@ export const useRecentItemsStore = defineStore('recentItems', () => { if (models.length === 0) { return [] } - // Sort by dateCreated and pick the first 5 + // Sort by dateCreated and pick the first {maxRecentItemCount} return models + .filter((a) => typeof a.date_created === 'number') .sort((a, b) => b.date_created - a.date_created) .slice(0, maxRecentItemCount) }) const recentlyUsedWorkflows = computed(() => { return workflowStore.workflows + .filter((a) => typeof a.lastModified === 'number') .sort((a, b) => { return (b.lastModified ?? 0) - (a.lastModified ?? 0) }) @@ -45,6 +48,7 @@ export const useRecentItemsStore = defineStore('recentItems', () => { const recentlyUsedModels = computed(() => { return modelStore.models + .filter((a) => typeof a.last_modified === 'number') .sort((a, b) => { return (b.last_modified ?? 0) - (a.last_modified ?? 0) }) diff --git a/tests-ui/tests/store/recentItemsStore.test.ts b/tests-ui/tests/store/recentItemsStore.test.ts index 561bb4ee42..bd22425553 100644 --- a/tests-ui/tests/store/recentItemsStore.test.ts +++ b/tests-ui/tests/store/recentItemsStore.test.ts @@ -191,8 +191,9 @@ describe('useRecentItemsStore', () => { vi.mocked(api.listUserDataFullInfo).mockResolvedValue( manyWorkflows.map((workflow) => ({ path: workflow.path, + size: workflow.size, modified: workflow.modified, - size: workflow.size + created: workflow.created })) ) From 0ef5fc53b0e379ea9ea3cf8ca0959c4643db9da4 Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Tue, 8 Jul 2025 15:47:08 +0100 Subject: [PATCH 13/28] fix(useModelPreview): improve type safety for previewRef --- src/composables/sidebarTabs/useModelPreview.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/composables/sidebarTabs/useModelPreview.ts b/src/composables/sidebarTabs/useModelPreview.ts index d5258665ab..5e0123e827 100644 --- a/src/composables/sidebarTabs/useModelPreview.ts +++ b/src/composables/sidebarTabs/useModelPreview.ts @@ -3,9 +3,11 @@ import { CSSProperties, computed, nextTick, ref } from 'vue' import { ComfyModelDef } from '@/stores/modelStore' import { useSettingStore } from '@/stores/settingStore' -export function useModelPreview() { +export function useModelPreview< + T extends { $el: HTMLElement } = { $el: HTMLElement } +>() { const isHovered = ref(false) - const previewRef = ref(null) + const previewRef = ref(null) const modelPreviewStyle = ref({ position: 'absolute', top: '0px', From f4d520e0beb1e6ce70c515db35478968c234280b Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Tue, 8 Jul 2025 15:47:43 +0100 Subject: [PATCH 14/28] fix(recentItemsStore): update maxRecentItemCount to be a computed property for dynamic value access --- src/stores/recentItemsStore.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/stores/recentItemsStore.ts b/src/stores/recentItemsStore.ts index 14c4e460b0..8f78cfc64b 100644 --- a/src/stores/recentItemsStore.ts +++ b/src/stores/recentItemsStore.ts @@ -9,9 +9,11 @@ export const useRecentItemsStore = defineStore('recentItems', () => { const workflowStore = useWorkflowStore() const modelStore = useModelStore() const settings = useSettingStore() - const maxRecentItemCount = settings.get('Comfy.Sidebar.RecentItems.MaxCount') // Computed properties for "Recently Added" based on file timestamps + const maxRecentItemCount = computed(() => + settings.get('Comfy.Sidebar.RecentItems.MaxCount') + ) const recentlyAddedWorkflows = computed(() => { const workflows = workflowStore.workflows if (workflows.length === 0) { @@ -22,7 +24,7 @@ export const useRecentItemsStore = defineStore('recentItems', () => { return workflows .filter((a) => typeof a.created === 'number') .sort((a, b) => b.created - a.created) - .slice(0, maxRecentItemCount) + .slice(0, maxRecentItemCount.value) }) const recentlyAddedModels = computed(() => { @@ -34,7 +36,7 @@ export const useRecentItemsStore = defineStore('recentItems', () => { return models .filter((a) => typeof a.date_created === 'number') .sort((a, b) => b.date_created - a.date_created) - .slice(0, maxRecentItemCount) + .slice(0, maxRecentItemCount.value) }) const recentlyUsedWorkflows = computed(() => { @@ -43,7 +45,7 @@ export const useRecentItemsStore = defineStore('recentItems', () => { .sort((a, b) => { return (b.lastModified ?? 0) - (a.lastModified ?? 0) }) - .slice(0, maxRecentItemCount) + .slice(0, maxRecentItemCount.value) }) const recentlyUsedModels = computed(() => { @@ -52,7 +54,7 @@ export const useRecentItemsStore = defineStore('recentItems', () => { .sort((a, b) => { return (b.last_modified ?? 0) - (a.last_modified ?? 0) }) - .slice(0, maxRecentItemCount) + .slice(0, maxRecentItemCount.value) }) return { From 4414dbfdb4f676680a82d07281c5e14b638e5b9c Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Tue, 8 Jul 2025 17:27:37 +0100 Subject: [PATCH 15/28] leave image load duties to model preview --- .../sidebar/tabs/RecentItemsSection.vue | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/src/components/sidebar/tabs/RecentItemsSection.vue b/src/components/sidebar/tabs/RecentItemsSection.vue index 116d76b73c..0896dacf9d 100644 --- a/src/components/sidebar/tabs/RecentItemsSection.vue +++ b/src/components/sidebar/tabs/RecentItemsSection.vue @@ -28,14 +28,11 @@ @mouseleave="handleItemMouseLeave" >
- + @@ -77,14 +74,11 @@ @mouseleave="handleItemMouseLeave" >
- + @@ -116,7 +110,7 @@