From f041245b82bab37ba672652250345532a9926e4c Mon Sep 17 00:00:00 2001 From: joshistoast Date: Wed, 17 Dec 2025 20:46:35 -0700 Subject: [PATCH 1/6] feat(model manager): :lipstick: refactor model manager bulk actions UI --- invokeai/frontend/web/public/locales/en.json | 3 + .../subpanels/ModelManagerPanel/ModelList.tsx | 20 ++- .../ModelListBulkActions.tsx | 141 ++++++++++++++++++ .../ModelManagerPanel/ModelListNavigation.tsx | 140 ++++------------- 4 files changed, 182 insertions(+), 122 deletions(-) create mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index a50b0cb9efd..a6cd88a645c 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -863,6 +863,7 @@ }, "modelManager": { "active": "active", + "actions": "Bulk Actions", "addModel": "Add Model", "addModels": "Add Models", "advanced": "Advanced", @@ -898,6 +899,7 @@ "delete": "Delete", "deleteConfig": "Delete Config", "deleteModel": "Delete Model", + "deleteModels": "Delete Models", "deleteModelImage": "Delete Model Image", "deleteMsg1": "Are you sure you want to delete this model from InvokeAI?", "deleteMsg2": "This WILL delete the model from disk if it is in the InvokeAI root folder. If you are using a custom location, then the model WILL NOT be deleted from disk.", @@ -1029,6 +1031,7 @@ "triggerPhrases": "Trigger Phrases", "loraTriggerPhrases": "LoRA Trigger Phrases", "mainModelTriggerPhrases": "Main Model Trigger Phrases", + "selectAll": "Select All", "typePhraseHere": "Type phrase here", "t5Encoder": "T5 Encoder", "upcastAttention": "Upcast Attention", diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx index 0bf496b583b..ec9f53dc56c 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx @@ -1,7 +1,8 @@ -import { Flex, Text, useDisclosure, useToast } from '@invoke-ai/ui-library'; +import { Flex, Text, useToast } from '@invoke-ai/ui-library'; import { logger } from 'app/logging/logger'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; +import { buildUseDisclosure } from 'common/hooks/useBoolean' import { MODEL_CATEGORIES_AS_LIST } from 'features/modelManagerV2/models'; import { clearModelSelection, @@ -23,11 +24,12 @@ import type { AnyModelConfig } from 'services/api/types'; import { BulkDeleteModelsModal } from './BulkDeleteModelsModal'; import { FetchingModelsLoader } from './FetchingModelsLoader'; -import { ModelListHeader } from './ModelListHeader'; import { ModelListWrapper } from './ModelListWrapper'; const log = logger('models'); +export const [useBulkDeleteModal] = buildUseDisclosure(false); + const ModelList = () => { const dispatch = useAppDispatch(); const filteredModelType = useAppSelector(selectFilteredModelType); @@ -35,7 +37,7 @@ const ModelList = () => { const selectedModelKeys = useAppSelector(selectSelectedModelKeys); const { t } = useTranslation(); const toast = useToast(); - const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen, close } = useBulkDeleteModal(); const [isDeleting, setIsDeleting] = useState(false); const { data, isLoading } = useGetModelConfigsQuery(); @@ -62,10 +64,6 @@ const ModelList = () => { return { total, byCategory }; }, [data, filteredModelType, searchTerm]); - const handleBulkDelete = useCallback(() => { - onOpen(); - }, [onOpen]); - const handleConfirmBulkDelete = useCallback(async () => { setIsDeleting(true); try { @@ -74,7 +72,7 @@ const ModelList = () => { // Clear selection and close modal dispatch(clearModelSelection()); dispatch(setSelectedModelKey(null)); - onClose(); + close(); // Show success/failure toast if (result.failed.length === 0) { @@ -127,12 +125,11 @@ const ModelList = () => { } finally { setIsDeleting(false); } - }, [bulkDeleteModels, selectedModelKeys, dispatch, onClose, toast, t]); + }, [bulkDeleteModels, selectedModelKeys, dispatch, close, toast, t]); return ( <> - {isLoading && } @@ -147,9 +144,10 @@ const ModelList = () => { + { + const dispatch = useAppDispatch(); + const filteredModelType = useAppSelector(selectFilteredModelType); + const selectedModelKeys = useAppSelector(selectSelectedModelKeys); + const searchTerm = useAppSelector(selectSearchTerm); + const { data } = useGetModelConfigsQuery(); + const bulkDeleteModal = useBulkDeleteModal(); + + const handleBulkDelete = useCallback(() => { + bulkDeleteModal.open(); + }, [bulkDeleteModal]); + + // Calculate displayed (filtered) model keys + const displayedModelKeys = useMemo(() => { + const modelConfigs = modelConfigsAdapterSelectors.selectAll(data ?? { ids: [], entities: {} }); + const filteredModels = modelsFilter(modelConfigs, searchTerm, filteredModelType); + return filteredModels.map((m) => m.key); + }, [data, searchTerm, filteredModelType]); + + const { allSelected, someSelected } = useMemo(() => { + if (displayedModelKeys.length === 0) { + return { allSelected: false, someSelected: false }; + } + const selectedSet = new Set(selectedModelKeys); + const displayedSelectedCount = displayedModelKeys.filter((key) => selectedSet.has(key)).length; + return { + allSelected: displayedSelectedCount === displayedModelKeys.length, + someSelected: displayedSelectedCount > 0 && displayedSelectedCount < displayedModelKeys.length, + }; + }, [displayedModelKeys, selectedModelKeys]); + + const handleToggleAll = useCallback(() => { + if (allSelected) { + // Deselect all displayed models + const displayedSet = new Set(displayedModelKeys); + const newSelection = selectedModelKeys.filter((key) => !displayedSet.has(key)); + dispatch(modelSelectionChanged(newSelection)); + } else { + // Select all displayed models (merge with existing selection) + const selectedSet = new Set(selectedModelKeys); + displayedModelKeys.forEach((key) => selectedSet.add(key)); + dispatch(modelSelectionChanged(Array.from(selectedSet))); + } + }, [allSelected, displayedModelKeys, selectedModelKeys, dispatch]); + + const selectionCount = selectedModelKeys.length; + + return ( + + + + {t('modelManager.selectAll')} + + + + + + {selectionCount} {t('common.selected')} + + + } + flexShrink={0} + > + {t('modelManager.actions')} + + + } onClick={handleBulkDelete} color="error.300"> + {t('modelManager.deleteModels', { count: selectionCount })} + + + + + + ) +}) + +ModelListBulkActions.displayName = 'ModelListBulkActions' + +const modelsFilter = ( + data: T[], + nameFilter: string, + filteredModelType: FilterableModelType | null +): T[] => { + return data.filter((model) => { + const matchesFilter = + model.name.toLowerCase().includes(nameFilter.toLowerCase()) || + model.base.toLowerCase().includes(nameFilter.toLowerCase()) || + model.type.toLowerCase().includes(nameFilter.toLowerCase()) || + model.description?.toLowerCase().includes(nameFilter.toLowerCase()) || + model.format.toLowerCase().includes(nameFilter.toLowerCase()); + + const matchesType = getMatchesType(model, filteredModelType); + + return matchesFilter && matchesType; + }); +}; + +const getMatchesType = (modelConfig: AnyModelConfig, filteredModelType: FilterableModelType | null): boolean => { + if (filteredModelType === 'refiner') { + return modelConfig.base === 'sdxl-refiner'; + } + + if (filteredModelType === 'main' && modelConfig.base === 'sdxl-refiner') { + return false; + } + + return filteredModelType ? modelConfig.type === filteredModelType : true; +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx index 23081f68cf3..35f5bfe987b 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx @@ -1,48 +1,20 @@ -import { Checkbox, Flex, IconButton, Input, InputGroup, InputRightElement, Text } from '@invoke-ai/ui-library'; +import { Flex, IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { - type FilterableModelType, - modelSelectionChanged, - selectFilteredModelType, selectSearchTerm, - selectSelectedModelKeys, setSearchTerm, } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { t } from 'i18next'; import type { ChangeEventHandler } from 'react'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback } from 'react'; import { PiXBold } from 'react-icons/pi'; -import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models'; -import type { AnyModelConfig } from 'services/api/types'; +import { ModelListBulkActions } from './ModelListBulkActions' import { ModelTypeFilter } from './ModelTypeFilter'; export const ModelListNavigation = memo(() => { const dispatch = useAppDispatch(); const searchTerm = useAppSelector(selectSearchTerm); - const filteredModelType = useAppSelector(selectFilteredModelType); - const selectedModelKeys = useAppSelector(selectSelectedModelKeys); - const { data } = useGetModelConfigsQuery(); - - // Calculate displayed (filtered) model keys - const displayedModelKeys = useMemo(() => { - const modelConfigs = modelConfigsAdapterSelectors.selectAll(data ?? { ids: [], entities: {} }); - const filteredModels = modelsFilter(modelConfigs, searchTerm, filteredModelType); - return filteredModels.map((m) => m.key); - }, [data, searchTerm, filteredModelType]); - - // Calculate checkbox state - const { allSelected, someSelected } = useMemo(() => { - if (displayedModelKeys.length === 0) { - return { allSelected: false, someSelected: false }; - } - const selectedSet = new Set(selectedModelKeys); - const displayedSelectedCount = displayedModelKeys.filter((key) => selectedSet.has(key)).length; - return { - allSelected: displayedSelectedCount === displayedModelKeys.length, - someSelected: displayedSelectedCount > 0 && displayedSelectedCount < displayedModelKeys.length, - }; - }, [displayedModelKeys, selectedModelKeys]); const handleSearch: ChangeEventHandler = useCallback( (event) => { @@ -55,92 +27,38 @@ export const ModelListNavigation = memo(() => { dispatch(setSearchTerm('')); }, [dispatch]); - const handleToggleAll = useCallback(() => { - if (allSelected) { - // Deselect all displayed models - const displayedSet = new Set(displayedModelKeys); - const newSelection = selectedModelKeys.filter((key) => !displayedSet.has(key)); - dispatch(modelSelectionChanged(newSelection)); - } else { - // Select all displayed models (merge with existing selection) - const selectedSet = new Set(selectedModelKeys); - displayedModelKeys.forEach((key) => selectedSet.add(key)); - dispatch(modelSelectionChanged(Array.from(selectedSet))); - } - }, [allSelected, displayedModelKeys, selectedModelKeys, dispatch]); - return ( - + - - - - {t('modelManager.selectAll')} - + + + + + {!!searchTerm?.length && ( + + } + onClick={clearSearch} + /> + + )} + + + + - - - - {!!searchTerm?.length && ( - - } - onClick={clearSearch} - /> - - )} - - - - + ); }); ModelListNavigation.displayName = 'ModelListNavigation'; - -const modelsFilter = ( - data: T[], - nameFilter: string, - filteredModelType: FilterableModelType | null -): T[] => { - return data.filter((model) => { - const matchesFilter = - model.name.toLowerCase().includes(nameFilter.toLowerCase()) || - model.base.toLowerCase().includes(nameFilter.toLowerCase()) || - model.type.toLowerCase().includes(nameFilter.toLowerCase()) || - model.description?.toLowerCase().includes(nameFilter.toLowerCase()) || - model.format.toLowerCase().includes(nameFilter.toLowerCase()); - - const matchesType = getMatchesType(model, filteredModelType); - - return matchesFilter && matchesType; - }); -}; - -const getMatchesType = (modelConfig: AnyModelConfig, filteredModelType: FilterableModelType | null): boolean => { - if (filteredModelType === 'refiner') { - return modelConfig.base === 'sdxl-refiner'; - } - - if (filteredModelType === 'main' && modelConfig.base === 'sdxl-refiner') { - return false; - } - - return filteredModelType ? modelConfig.type === filteredModelType : true; -}; From 2a244d67e2174f6a6107366973ba0556455a343a Mon Sep 17 00:00:00 2001 From: joshistoast Date: Wed, 17 Dec 2025 21:22:22 -0700 Subject: [PATCH 2/6] feat(model manager): :lipstick: tweak model list item ui for checkbox selects --- .../ModelManagerPanel/ModelListItem.tsx | 125 +++++++++--------- 1 file changed, 66 insertions(+), 59 deletions(-) diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx index d6dda98e80e..16329ac7d22 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -1,5 +1,5 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; -import { Checkbox, Flex, Spacer, Text } from '@invoke-ai/ui-library'; +import { chakra, Checkbox, Flex, Spacer, Text } from '@invoke-ai/ui-library'; import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { @@ -12,19 +12,24 @@ import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge'; import { ModelDeleteButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelDeleteButton'; import { filesize } from 'filesize'; -import type { MouseEvent } from 'react'; +import type { ChangeEvent, MouseEvent } from 'react'; import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import type { AnyModelConfig } from 'services/api/types'; import ModelImage from './ModelImage'; +const StyledLabel = chakra('label'); + type ModelListItemProps = { model: AnyModelConfig; }; const sx: SystemStyleObject = { - paddingInline: 3, + paddingInlineStart: 10, + paddingInlineEnd: 3, paddingBlock: 2, + zIndex: 1, position: 'relative', rounded: 'base', '&:after,&:before': { @@ -38,12 +43,6 @@ const sx: SystemStyleObject = { insetInline: 3, bg: 'base.850', }, - '&:before': { - left: 1, - w: 1, - insetBlock: 2, - rounded: 'base', - }, _hover: { bg: 'base.850', '& .delete-button': { opacity: 1 }, @@ -58,6 +57,7 @@ const sx: SystemStyleObject = { }; const ModelListItem = ({ model }: ModelListItemProps) => { + const { t } = useTranslation(); const dispatch = useAppDispatch(); const selectIsSelected = useMemo( () => @@ -71,16 +71,9 @@ const ModelListItem = ({ model }: ModelListItemProps) => { const selectedModelKeys = useAppSelector(selectSelectedModelKeys); const isChecked = selectedModelKeys.includes(model.key); - const handleSelectModel = useCallback( + const handleRowClick = useCallback( (e: MouseEvent) => { - // Check if clicked on checkbox or delete button - if so, don't handle selection - const target = e.target as HTMLElement; - if (target.closest('input[type="checkbox"]') || target.closest('button')) { - return; - } - - // Clicking the row opens detail view (single select) - // Ctrl/Cmd+Click still works as a power user feature for multi-select + // Ctrl/Cmd+Click toggles multi-selection if (e.ctrlKey || e.metaKey) { dispatch(toggleModelSelection(model.key)); } else { @@ -90,55 +83,69 @@ const ModelListItem = ({ model }: ModelListItemProps) => { [model.key, dispatch] ); - const handleCheckboxChange = useCallback(() => { - dispatch(toggleModelSelection(model.key)); - }, [model.key, dispatch]); + const handleCheckboxChange = useCallback( + (e: ChangeEvent) => { + e.stopPropagation(); + dispatch(toggleModelSelection(model.key)); + }, + [model.key, dispatch] + ); - const handleCheckboxClick = useCallback((e: MouseEvent) => { + const stopPropagation = useCallback((e: MouseEvent) => { e.stopPropagation(); }, []); return ( - - - - - - - - {model.name} - - - {filesize(model.file_size)} + + + + + + + + + + + {model.name} + + + {filesize(model.file_size)} + + + + + {model.description || 'No Description'} - - - - {model.description || 'No Description'} - - - - + + + + - - - + + + ); From 1365612586b2172d6701ea908fcead52db904db2 Mon Sep 17 00:00:00 2001 From: joshistoast Date: Wed, 17 Dec 2025 21:23:09 -0700 Subject: [PATCH 3/6] style(model manager): :rotating_light: satisfy the linter --- .../subpanels/ModelManagerPanel/ModelList.tsx | 2 +- .../ModelListBulkActions.tsx | 37 +++++++++++-------- .../ModelManagerPanel/ModelListNavigation.tsx | 7 +--- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx index ec9f53dc56c..2159d538bee 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx @@ -2,7 +2,7 @@ import { Flex, Text, useToast } from '@invoke-ai/ui-library'; import { logger } from 'app/logging/logger'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import { buildUseDisclosure } from 'common/hooks/useBoolean' +import { buildUseDisclosure } from 'common/hooks/useBoolean'; import { MODEL_CATEGORIES_AS_LIST } from 'features/modelManagerV2/models'; import { clearModelSelection, diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx index 73fcde5c35a..b7061b0aa1f 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx @@ -1,25 +1,30 @@ -import type { SystemStyleObject} from '@invoke-ai/ui-library'; -import { Button, Checkbox, Flex, Menu, MenuButton, MenuItem, MenuList, Text } from '@invoke-ai/ui-library' -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks' -import type { FilterableModelType} from 'features/modelManagerV2/store/modelManagerV2Slice'; -import { modelSelectionChanged, selectFilteredModelType, selectSearchTerm, selectSelectedModelKeys } from 'features/modelManagerV2/store/modelManagerV2Slice' +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import { Button, Checkbox, Flex, Menu, MenuButton, MenuItem, MenuList, Text } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import type { FilterableModelType } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { + modelSelectionChanged, + selectFilteredModelType, + selectSearchTerm, + selectSelectedModelKeys, +} from 'features/modelManagerV2/store/modelManagerV2Slice'; import { t } from 'i18next'; -import { memo, useCallback, useMemo } from 'react' -import { PiCaretDownBold, PiTrashSimpleBold } from 'react-icons/pi' -import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models' -import type { AnyModelConfig } from 'services/api/types' +import { memo, useCallback, useMemo } from 'react'; +import { PiCaretDownBold, PiTrashSimpleBold } from 'react-icons/pi'; +import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models'; +import type { AnyModelConfig } from 'services/api/types'; -import { useBulkDeleteModal } from './ModelList' +import { useBulkDeleteModal } from './ModelList'; const ModelListBulkActionsSx: SystemStyleObject = { alignItems: 'center', justifyContent: 'space-between', width: '100%', -} +}; type ModelListBulkActionsProps = { - sx?: SystemStyleObject -} + sx?: SystemStyleObject; +}; export const ModelListBulkActions = memo(({ sx }: ModelListBulkActionsProps) => { const dispatch = useAppDispatch(); @@ -104,10 +109,10 @@ export const ModelListBulkActions = memo(({ sx }: ModelListBulkActionsProps) => - ) -}) + ); +}); -ModelListBulkActions.displayName = 'ModelListBulkActions' +ModelListBulkActions.displayName = 'ModelListBulkActions'; const modelsFilter = ( data: T[], diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx index 35f5bfe987b..78bed8ab830 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx @@ -1,15 +1,12 @@ import { Flex, IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { - selectSearchTerm, - setSearchTerm, -} from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { selectSearchTerm, setSearchTerm } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { t } from 'i18next'; import type { ChangeEventHandler } from 'react'; import { memo, useCallback } from 'react'; import { PiXBold } from 'react-icons/pi'; -import { ModelListBulkActions } from './ModelListBulkActions' +import { ModelListBulkActions } from './ModelListBulkActions'; import { ModelTypeFilter } from './ModelTypeFilter'; export const ModelListNavigation = memo(() => { From 47014b8991db1a13a83e7c6c89288d087373ee0f Mon Sep 17 00:00:00 2001 From: joshistoast Date: Wed, 17 Dec 2025 21:34:07 -0700 Subject: [PATCH 4/6] feat(model manager): :lipstick: tweak search and actions dropdown placement --- .../subpanels/ModelManagerPanel/ModelListBulkActions.tsx | 3 ++- .../subpanels/ModelManagerPanel/ModelTypeFilter.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx index b7061b0aa1f..2442bd02162 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx @@ -91,13 +91,14 @@ export const ModelListBulkActions = memo(({ sx }: ModelListBulkActionsProps) => {selectionCount} {t('common.selected')} - + } flexShrink={0} + variant="outline" > {t('modelManager.actions')} diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx index 0ee479e86b2..dcb22071482 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx @@ -17,8 +17,8 @@ export const ModelTypeFilter = memo(() => { }, [dispatch]); return ( - - }> + + }> {filteredModelType ? t(MODEL_CATEGORIES[filteredModelType].i18nKey) : t('modelManager.allModels')} From c36eee7029c249f95d80023b3177e2ce1e53fcb1 Mon Sep 17 00:00:00 2001 From: joshistoast Date: Wed, 17 Dec 2025 21:35:31 -0700 Subject: [PATCH 5/6] refactor(model manager): :fire: remove unused `ModelListHeader` component --- .../ModelManagerPanel/ModelListHeader.tsx | 70 ------------------- 1 file changed, 70 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListHeader.tsx diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListHeader.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListHeader.tsx deleted file mode 100644 index 3d0d71029b8..00000000000 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListHeader.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { - Button, - Flex, - Menu, - MenuButton, - MenuItem, - MenuList, - Tag, - TagCloseButton, - TagLabel, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { clearModelSelection, selectSelectedModelKeys } from 'features/modelManagerV2/store/modelManagerV2Slice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiCaretDownBold, PiTrashSimpleBold } from 'react-icons/pi'; - -type ModelListHeaderProps = { - onBulkDelete: () => void; -}; - -export const ModelListHeader = memo(({ onBulkDelete }: ModelListHeaderProps) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const selectedModelKeys = useAppSelector(selectSelectedModelKeys); - const selectionCount = selectedModelKeys.length; - - const handleClearSelection = useCallback(() => { - dispatch(clearModelSelection()); - }, [dispatch]); - - if (selectionCount === 0) { - return null; - } - - return ( - - - - {selectionCount} {t('common.selected')} - - - - - } flexShrink={0}> - {t('modelManager.actions')} - - - } onClick={onBulkDelete} color="error.300"> - {t('modelManager.deleteModels', { count: selectionCount })} - - - - - ); -}); - -ModelListHeader.displayName = 'ModelListHeader'; From c0c9172aa89b80a4c110eda95edaf0fefed661be Mon Sep 17 00:00:00 2001 From: joshistoast Date: Mon, 22 Dec 2025 19:40:58 -0700 Subject: [PATCH 6/6] fix(model manager): :bug: list items overlapping sticky headers --- .../modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx | 1 - .../subpanels/ModelManagerPanel/ModelListWrapper.tsx | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx index 16329ac7d22..5719752ff01 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -29,7 +29,6 @@ const sx: SystemStyleObject = { paddingInlineStart: 10, paddingInlineEnd: 3, paddingBlock: 2, - zIndex: 1, position: 'relative', rounded: 'base', '&:after,&:before': { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx index 9783a88b062..74b26dce386 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListWrapper.tsx @@ -21,6 +21,7 @@ const contentSx = { p: 0, bg: 'base.900', borderRadius: '0', + zIndex: 0, } satisfies SystemStyleObject; export const ModelListWrapper = memo((props: ModelListWrapperProps) => {