Skip to content

Commit ae5733a

Browse files
joshistoastpsychedelicious
authored andcommitted
feat(model manager): add ModelFooter component and reusable ModelDeleteButton
1 parent 60fc55f commit ae5733a

File tree

5 files changed

+172
-62
lines changed

5 files changed

+172
-62
lines changed

invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx

Lines changed: 7 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import type { SystemStyleObject } from '@invoke-ai/ui-library';
2-
import { ConfirmationAlertDialog, Flex, IconButton, Spacer, Text, useDisclosure } from '@invoke-ai/ui-library';
2+
import { Flex, Spacer, Text } from '@invoke-ai/ui-library';
33
import { createSelector } from '@reduxjs/toolkit';
44
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
55
import { selectModelManagerV2Slice, setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
66
import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge';
77
import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge';
8-
import { toast } from 'features/toast/toast';
8+
import { ModelDeleteButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelDeleteButton';
99
import { filesize } from 'filesize';
10-
import type { MouseEvent } from 'react';
1110
import { memo, useCallback, useMemo } from 'react';
12-
import { useTranslation } from 'react-i18next';
13-
import { PiTrashSimpleBold } from 'react-icons/pi';
14-
import { useDeleteModelsMutation } from 'services/api/endpoints/models';
1511
import type { AnyModelConfig } from 'services/api/types';
1612

1713
import ModelImage from './ModelImage';
@@ -46,6 +42,7 @@ const sx: SystemStyleObject = {
4642
bg: 'base.850',
4743
'& .delete-button': { opacity: 1 },
4844
},
45+
'& .delete-button': { opacity: 0 },
4946
"&[aria-selected='false']:hover:before": { bg: 'base.750' },
5047
"&[aria-selected='true']": {
5148
bg: 'base.800',
@@ -55,7 +52,6 @@ const sx: SystemStyleObject = {
5552
};
5653

5754
const ModelListItem = ({ model }: ModelListItemProps) => {
58-
const { t } = useTranslation();
5955
const dispatch = useAppDispatch();
6056
const selectIsSelected = useMemo(
6157
() =>
@@ -66,49 +62,18 @@ const ModelListItem = ({ model }: ModelListItemProps) => {
6662
[model.key]
6763
);
6864
const isSelected = useAppSelector(selectIsSelected);
69-
const [deleteModel] = useDeleteModelsMutation();
70-
const { isOpen, onOpen, onClose } = useDisclosure();
7165

7266
const handleSelectModel = useCallback(() => {
7367
dispatch(setSelectedModelKey(model.key));
7468
}, [model.key, dispatch]);
7569

76-
const onClickDeleteButton = useCallback(
77-
(e: MouseEvent<HTMLButtonElement>) => {
78-
e.stopPropagation();
79-
onOpen();
80-
},
81-
[onOpen]
82-
);
83-
const handleModelDelete = useCallback(() => {
84-
deleteModel({ key: model.key })
85-
.unwrap()
86-
.then((_) => {
87-
toast({
88-
id: 'MODEL_DELETED',
89-
title: `${t('modelManager.modelDeleted')}: ${model.name}`,
90-
status: 'success',
91-
});
92-
})
93-
.catch((error) => {
94-
if (error) {
95-
toast({
96-
id: 'MODEL_DELETE_FAILED',
97-
title: `${t('modelManager.modelDeleteFailed')}: ${model.name}`,
98-
status: 'error',
99-
});
100-
}
101-
});
102-
dispatch(setSelectedModelKey(null));
103-
}, [deleteModel, model, dispatch, t]);
104-
10570
return (
10671
<Flex
10772
sx={sx}
10873
aria-selected={isSelected}
10974
justifyContent="flex-start"
11075
w="full"
111-
alignItems="center"
76+
alignItems="flex-start"
11277
gap={2}
11378
cursor="pointer"
11479
onClick={handleSelectModel}
@@ -134,29 +99,9 @@ const ModelListItem = ({ model }: ModelListItemProps) => {
13499
</Flex>
135100
</Flex>
136101
</Flex>
137-
<IconButton
138-
className="delete-button"
139-
onClick={onClickDeleteButton}
140-
icon={<PiTrashSimpleBold size={16} />}
141-
aria-label={t('modelManager.deleteConfig')}
142-
colorScheme="error"
143-
opacity={0}
144-
mt={1}
145-
alignSelf="flex-start"
146-
/>
147-
<ConfirmationAlertDialog
148-
isOpen={isOpen}
149-
onClose={onClose}
150-
title={t('modelManager.deleteModel')}
151-
acceptCallback={handleModelDelete}
152-
acceptButtonText={t('modelManager.delete')}
153-
useInert={false}
154-
>
155-
<Flex rowGap={4} flexDirection="column">
156-
<Text fontWeight="bold">{t('modelManager.deleteMsg1')}</Text>
157-
<Text>{t('modelManager.deleteMsg2')}</Text>
158-
</Flex>
159-
</ConfirmationAlertDialog>
102+
<Flex mt={1}>
103+
<ModelDeleteButton modelConfig={model} showLabel={false} />
104+
</Flex>
160105
</Flex>
161106
);
162107
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Button, ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library';
2+
import { logger } from 'app/logging/logger';
3+
import { useAppDispatch } from 'app/store/storeHooks';
4+
import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice';
5+
import { toast } from 'features/toast/toast';
6+
import { memo, type MouseEvent, useCallback } from 'react';
7+
import { useTranslation } from 'react-i18next';
8+
import { PiTrashSimpleBold } from 'react-icons/pi';
9+
import { useDeleteModelsMutation } from 'services/api/endpoints/models';
10+
import type { AnyModelConfig } from 'services/api/types';
11+
12+
type Props = {
13+
showLabel?: boolean;
14+
modelConfig: AnyModelConfig;
15+
};
16+
17+
export const ModelDeleteButton = memo(({ showLabel = true, modelConfig }: Props) => {
18+
const { t } = useTranslation();
19+
const dispatch = useAppDispatch();
20+
21+
const log = logger('models');
22+
23+
const [deleteModel] = useDeleteModelsMutation();
24+
const { isOpen, onOpen, onClose } = useDisclosure();
25+
26+
const onClickDeleteButton = useCallback(
27+
(e: MouseEvent<HTMLButtonElement>) => {
28+
e.stopPropagation();
29+
onOpen();
30+
},
31+
[onOpen]
32+
);
33+
34+
const handleModelDelete = useCallback(() => {
35+
deleteModel({ key: modelConfig.key })
36+
.unwrap()
37+
.then(() => {
38+
dispatch(setSelectedModelKey(null));
39+
toast({
40+
id: 'MODEL_DELETED',
41+
title: `${t('modelManager.modelDeleted')}: ${modelConfig.name}`,
42+
status: 'success',
43+
});
44+
})
45+
.catch((error) => {
46+
log.error('Error deleting model', error);
47+
toast({
48+
id: 'MODEL_DELETE_FAILED',
49+
title: `${t('modelManager.modelDeleteFailed')}: ${modelConfig.name}`,
50+
status: 'error',
51+
});
52+
});
53+
}, [deleteModel, modelConfig.key, modelConfig.name, dispatch, t, log]);
54+
55+
return (
56+
<>
57+
{showLabel ? (
58+
<Button
59+
className="delete-button"
60+
size="sm"
61+
leftIcon={<PiTrashSimpleBold />}
62+
colorScheme="error"
63+
onClick={onClickDeleteButton}
64+
flexShrink={0}
65+
>
66+
{t('modelManager.delete')}
67+
</Button>
68+
) : (
69+
<IconButton
70+
className="delete-button"
71+
onClick={onClickDeleteButton}
72+
icon={<PiTrashSimpleBold size={16} />}
73+
aria-label={t('modelManager.deleteConfig')}
74+
colorScheme="error"
75+
/>
76+
)}
77+
78+
<ConfirmationAlertDialog
79+
isOpen={isOpen}
80+
onClose={onClose}
81+
title={t('modelManager.deleteModel')}
82+
acceptCallback={handleModelDelete}
83+
acceptButtonText={t('modelManager.delete')}
84+
useInert={false}
85+
>
86+
<Flex rowGap={4} flexDirection="column">
87+
<Text fontWeight="bold">{t('modelManager.deleteMsg1')}</Text>
88+
<Text>{t('modelManager.deleteMsg2')}</Text>
89+
</Flex>
90+
</ConfirmationAlertDialog>
91+
</>
92+
);
93+
});
94+
95+
ModelDeleteButton.displayName = 'ModelDeleteButton';

invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { AnyModelConfig } from 'services/api/types';
2424
import BaseModelSelect from './Fields/BaseModelSelect';
2525
import ModelVariantSelect from './Fields/ModelVariantSelect';
2626
import PredictionTypeSelect from './Fields/PredictionTypeSelect';
27+
import { ModelFooter } from './ModelFooter';
2728

2829
type Props = {
2930
modelConfig: AnyModelConfig;
@@ -158,6 +159,7 @@ export const ModelEdit = memo(({ modelConfig }: Props) => {
158159
</Flex>
159160
</form>
160161
</Flex>
162+
<ModelFooter modelConfig={modelConfig} isEditing={true} />
161163
</Flex>
162164
);
163165
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Flex, Heading, type SystemStyleObject } from '@invoke-ai/ui-library';
2+
import { memo } from 'react';
3+
import { useTranslation } from 'react-i18next';
4+
import type { AnyModelConfig } from 'services/api/types';
5+
6+
import { ModelConvertButton } from './ModelConvertButton';
7+
import { ModelDeleteButton } from './ModelDeleteButton';
8+
import { ModelEditButton } from './ModelEditButton';
9+
10+
const footerRowSx: SystemStyleObject = {
11+
justifyContent: 'space-between',
12+
alignItems: 'center',
13+
gap: 3,
14+
'&:not(:last-of-type)': {
15+
borderBottomWidth: '1px',
16+
borderBottomStyle: 'solid',
17+
borderBottomColor: 'border',
18+
},
19+
p: 3,
20+
};
21+
22+
type Props = {
23+
modelConfig: AnyModelConfig;
24+
isEditing: boolean;
25+
};
26+
27+
export const ModelFooter = memo(({ modelConfig, isEditing }: Props) => {
28+
const { t } = useTranslation();
29+
30+
const shouldShowConvertOption = !isEditing && modelConfig.format === 'checkpoint' && modelConfig.type === 'main';
31+
32+
return (
33+
<Flex flexDirection="column" borderWidth="1px" borderRadius="base">
34+
{shouldShowConvertOption && (
35+
<Flex sx={footerRowSx}>
36+
<Heading size="sm" color="base.100">
37+
{t('modelManager.convertToDiffusers')}
38+
</Heading>
39+
<Flex py={1}>
40+
<ModelConvertButton modelConfig={modelConfig} />
41+
</Flex>
42+
</Flex>
43+
)}
44+
{!isEditing && (
45+
<Flex sx={footerRowSx}>
46+
<Heading size="sm" color="base.100">
47+
{t('modelManager.edit')}
48+
</Heading>
49+
<Flex py={1}>
50+
<ModelEditButton />
51+
</Flex>
52+
</Flex>
53+
)}
54+
<Flex sx={footerRowSx}>
55+
<Heading size="sm" color="error.200">
56+
{t('modelManager.deleteModel')}
57+
</Heading>
58+
<Flex py={1}>
59+
<ModelDeleteButton modelConfig={modelConfig} />
60+
</Flex>
61+
</Flex>
62+
</Flex>
63+
);
64+
});
65+
66+
ModelFooter.displayName = 'ModelFooter';

invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { AnyModelConfig } from 'services/api/types';
1212

1313
import { MainModelDefaultSettings } from './MainModelDefaultSettings/MainModelDefaultSettings';
1414
import { ModelAttrView } from './ModelAttrView';
15+
import { ModelFooter } from './ModelFooter';
1516
import { RelatedModels } from './RelatedModels';
1617

1718
type Props = {
@@ -100,6 +101,7 @@ export const ModelView = memo(({ modelConfig }: Props) => {
100101
<RelatedModels modelConfig={modelConfig} />
101102
</Box>
102103
</Flex>
104+
<ModelFooter modelConfig={modelConfig} isEditing={false} />
103105
</Flex>
104106
);
105107
});

0 commit comments

Comments
 (0)