|
1 | | -/* eslint-disable i18next/no-literal-string */ |
2 | | -import { Button, Flex } from '@invoke-ai/ui-library'; |
| 1 | +import type { ButtonProps, CheckboxProps } from '@invoke-ai/ui-library'; |
| 2 | +import { Button, Checkbox, Flex, Text } from '@invoke-ai/ui-library'; |
3 | 3 | import { useStore } from '@nanostores/react'; |
4 | 4 | import { $workflowCategories } from 'app/store/nanostores/workflowCategories'; |
5 | | -import { useAppSelector } from 'app/store/storeHooks'; |
6 | | -import { selectWorkflowCategories, workflowCategoriesChanged } from 'features/nodes/store/workflowSlice'; |
7 | | -import type { WorkflowCategory } from 'features/nodes/types/workflow'; |
8 | | -import { useCallback, useMemo } from 'react'; |
9 | | -import { PiUsersBold } from 'react-icons/pi'; |
| 5 | +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; |
| 6 | +import { WORKFLOW_TAGS, type WorkflowTag } from 'features/nodes/store/types'; |
| 7 | +import { |
| 8 | + selectWorkflowLibrarySelectedTags, |
| 9 | + selectWorkflowSelectedCategories, |
| 10 | + workflowSelectedCategoriesChanged, |
| 11 | + workflowSelectedTagsRese, |
| 12 | + workflowSelectedTagToggled, |
| 13 | +} from 'features/nodes/store/workflowSlice'; |
| 14 | +import { memo, useCallback, useMemo } from 'react'; |
| 15 | +import { useTranslation } from 'react-i18next'; |
| 16 | +import { PiArrowCounterClockwiseBold, PiUsersBold } from 'react-icons/pi'; |
10 | 17 | import { useDispatch } from 'react-redux'; |
| 18 | +import { useGetCountsQuery } from 'services/api/endpoints/workflows'; |
11 | 19 |
|
12 | 20 | export const WorkflowLibrarySideNav = () => { |
| 21 | + const { t } = useTranslation(); |
13 | 22 | const dispatch = useDispatch(); |
14 | | - const categories = useAppSelector(selectWorkflowCategories); |
| 23 | + const categories = useAppSelector(selectWorkflowSelectedCategories); |
15 | 24 | const categoryOptions = useStore($workflowCategories); |
| 25 | + const selectedTags = useAppSelector(selectWorkflowLibrarySelectedTags); |
16 | 26 |
|
17 | | - const handleCategoryChange = useCallback( |
18 | | - (categories: WorkflowCategory[]) => { |
19 | | - dispatch(workflowCategoriesChanged(categories)); |
20 | | - }, |
21 | | - [dispatch] |
22 | | - ); |
| 27 | + const selectYourWorkflows = useCallback(() => { |
| 28 | + dispatch(workflowSelectedCategoriesChanged(categoryOptions.includes('project') ? ['user', 'project'] : ['user'])); |
| 29 | + }, [categoryOptions, dispatch]); |
23 | 30 |
|
24 | | - const handleSelectYourWorkflows = useCallback(() => { |
25 | | - if (categoryOptions.includes('project')) { |
26 | | - handleCategoryChange(['user', 'project']); |
27 | | - } else { |
28 | | - handleCategoryChange(['user']); |
29 | | - } |
30 | | - }, [categoryOptions, handleCategoryChange]); |
| 31 | + const selectPrivateWorkflows = useCallback(() => { |
| 32 | + dispatch(workflowSelectedCategoriesChanged(['user'])); |
| 33 | + }, [dispatch]); |
| 34 | + |
| 35 | + const selectSharedWorkflows = useCallback(() => { |
| 36 | + dispatch(workflowSelectedCategoriesChanged(['project'])); |
| 37 | + }, [dispatch]); |
31 | 38 |
|
32 | | - const isYourWorkflowsActive = useMemo(() => { |
| 39 | + const selectDefaultWorkflows = useCallback(() => { |
| 40 | + dispatch(workflowSelectedCategoriesChanged(['default'])); |
| 41 | + }, [dispatch]); |
| 42 | + |
| 43 | + const resetTags = useCallback(() => { |
| 44 | + dispatch(workflowSelectedTagsRese()); |
| 45 | + }, [dispatch]); |
| 46 | + |
| 47 | + const isYourWorkflowsSelected = useMemo(() => { |
33 | 48 | if (categoryOptions.includes('project')) { |
34 | 49 | return categories.includes('user') && categories.includes('project'); |
35 | 50 | } else { |
36 | 51 | return categories.includes('user'); |
37 | 52 | } |
38 | 53 | }, [categoryOptions, categories]); |
39 | 54 |
|
| 55 | + const isPrivateWorkflowsExclusivelySelected = useMemo(() => { |
| 56 | + return categories.length === 1 && categories.includes('user'); |
| 57 | + }, [categories]); |
| 58 | + |
| 59 | + const isSharedWorkflowsExclusivelySelected = useMemo(() => { |
| 60 | + return categories.length === 1 && categories.includes('project'); |
| 61 | + }, [categories]); |
| 62 | + |
| 63 | + const isDefaultWorkflowsExclusivelySelected = useMemo(() => { |
| 64 | + return categories.length === 1 && categories.includes('default'); |
| 65 | + }, [categories]); |
| 66 | + |
40 | 67 | return ( |
41 | 68 | <Flex flexDir="column" gap={2} h="full"> |
42 | | - <Button |
43 | | - variant="ghost" |
44 | | - fontWeight="bold" |
45 | | - justifyContent="flex-start" |
46 | | - size="md" |
47 | | - isActive={isYourWorkflowsActive} |
48 | | - onClick={handleSelectYourWorkflows} |
49 | | - _active={{ |
50 | | - bg: 'base.700', |
51 | | - color: 'base.100', |
52 | | - }} |
53 | | - > |
54 | | - Your Workflows |
55 | | - </Button> |
| 69 | + <CategoryButton isSelected={isYourWorkflowsSelected} onClick={selectYourWorkflows}> |
| 70 | + {t('workflows.yourWorkflows')} |
| 71 | + </CategoryButton> |
56 | 72 | {categoryOptions.includes('project') && ( |
57 | 73 | <Flex flexDir="column" gap={2} pl={4}> |
58 | | - <Button |
59 | | - variant="ghost" |
60 | | - fontWeight="bold" |
61 | | - justifyContent="flex-start" |
62 | | - size="sm" |
63 | | - isActive={categories.length === 1 && categories.includes('user')} |
64 | | - onClick={handleCategoryChange.bind(null, ['user'])} |
65 | | - _active={{ |
66 | | - bg: 'base.700', |
67 | | - color: 'base.100', |
68 | | - }} |
69 | | - > |
70 | | - Private |
71 | | - </Button> |
72 | | - <Button |
73 | | - variant="ghost" |
74 | | - fontWeight="bold" |
75 | | - justifyContent="flex-start" |
| 74 | + <CategoryButton size="sm" onClick={selectPrivateWorkflows} isSelected={isPrivateWorkflowsExclusivelySelected}> |
| 75 | + {t('workflows.private')} |
| 76 | + </CategoryButton> |
| 77 | + <CategoryButton |
76 | 78 | size="sm" |
77 | 79 | rightIcon={<PiUsersBold />} |
78 | | - isActive={categories.length === 1 && categories.includes('project')} |
79 | | - onClick={handleCategoryChange.bind(null, ['project'])} |
80 | | - _active={{ |
81 | | - bg: 'base.700', |
82 | | - color: 'base.100', |
83 | | - }} |
| 80 | + onClick={selectSharedWorkflows} |
| 81 | + isSelected={isSharedWorkflowsExclusivelySelected} |
84 | 82 | > |
85 | | - Shared |
86 | | - </Button> |
| 83 | + {t('workflows.shared')} |
| 84 | + </CategoryButton> |
87 | 85 | </Flex> |
88 | 86 | )} |
89 | | - <Button |
90 | | - variant="ghost" |
91 | | - fontWeight="bold" |
92 | | - justifyContent="flex-start" |
93 | | - size="md" |
94 | | - isActive={categories.includes('default')} |
95 | | - onClick={handleCategoryChange.bind(null, ['default'])} |
96 | | - _active={{ |
97 | | - bg: 'base.700', |
98 | | - color: 'base.100', |
99 | | - }} |
100 | | - > |
101 | | - Browse Workflows |
102 | | - </Button> |
103 | | - |
104 | | - {/* these are obviously placeholders - we need to figure out the best way to do this. leaning towards "tags" so that we can filter and/or have multiple selected eventually */} |
105 | | - <Flex flexDir="column" gap={2} pl={4}> |
| 87 | + <CategoryButton isSelected={isDefaultWorkflowsExclusivelySelected} onClick={selectDefaultWorkflows}> |
| 88 | + {t('workflows.browseWorkflows')} |
| 89 | + </CategoryButton> |
| 90 | + |
| 91 | + <Flex flexDir="column" gap={2} pl={4} overflow="hidden"> |
106 | 92 | <Button |
107 | | - variant="ghost" |
108 | | - fontWeight="bold" |
109 | | - justifyContent="flex-start" |
| 93 | + isDisabled={!isDefaultWorkflowsExclusivelySelected || selectedTags.length === 0} |
| 94 | + onClick={resetTags} |
110 | 95 | size="sm" |
111 | | - _active={{ |
112 | | - bg: 'base.700', |
113 | | - color: 'base.100', |
114 | | - }} |
115 | | - > |
116 | | - Architecture |
117 | | - </Button> |
118 | | - <Button |
119 | | - variant="ghost" |
| 96 | + variant="link" |
120 | 97 | fontWeight="bold" |
121 | 98 | justifyContent="flex-start" |
122 | | - size="sm" |
123 | | - _active={{ |
124 | | - bg: 'base.700', |
125 | | - color: 'base.100', |
126 | | - }} |
| 99 | + flexGrow={0} |
| 100 | + leftIcon={<PiArrowCounterClockwiseBold />} |
| 101 | + h={8} |
127 | 102 | > |
128 | | - Fashion |
| 103 | + {t('workflows.resetTags')} |
129 | 104 | </Button> |
| 105 | + <Flex flexDir="column" gap={2} overflow="auto"> |
| 106 | + {WORKFLOW_TAGS.map((tagCategory) => ( |
| 107 | + <TagCategory |
| 108 | + key={tagCategory.category} |
| 109 | + tagCategory={tagCategory} |
| 110 | + isDisabled={!isDefaultWorkflowsExclusivelySelected} |
| 111 | + /> |
| 112 | + ))} |
| 113 | + </Flex> |
130 | 114 | </Flex> |
131 | 115 | </Flex> |
132 | 116 | ); |
133 | 117 | }; |
| 118 | + |
| 119 | +const CategoryButton = memo(({ isSelected, ...rest }: ButtonProps & { isSelected: boolean }) => { |
| 120 | + return ( |
| 121 | + <Button |
| 122 | + colorScheme={isSelected ? 'invokeBlue' : 'base'} |
| 123 | + variant="ghost" |
| 124 | + fontWeight="bold" |
| 125 | + justifyContent="flex-start" |
| 126 | + size="md" |
| 127 | + {...rest} |
| 128 | + /> |
| 129 | + ); |
| 130 | +}); |
| 131 | +CategoryButton.displayName = 'NavButton'; |
| 132 | + |
| 133 | +const TagCategory = memo( |
| 134 | + ({ tagCategory, isDisabled }: { tagCategory: (typeof WORKFLOW_TAGS)[number]; isDisabled: boolean }) => { |
| 135 | + const { count } = useGetCountsQuery( |
| 136 | + { tags: [...tagCategory.tags], categories: ['default'] }, |
| 137 | + { selectFromResult: ({ data }) => ({ count: data ?? 0 }) } |
| 138 | + ); |
| 139 | + |
| 140 | + if (count === 0) { |
| 141 | + return null; |
| 142 | + } |
| 143 | + |
| 144 | + return ( |
| 145 | + <Flex flexDir="column" gap={2}> |
| 146 | + <Text fontWeight="semibold" color="base.300" opacity={isDisabled ? 0.5 : 1}> |
| 147 | + {tagCategory.category} |
| 148 | + </Text> |
| 149 | + <Flex flexDir="column" gap={2} pl={4}> |
| 150 | + {tagCategory.tags.map((tag) => ( |
| 151 | + <TagCheckbox key={tag} tag={tag} isDisabled={isDisabled} /> |
| 152 | + ))} |
| 153 | + </Flex> |
| 154 | + </Flex> |
| 155 | + ); |
| 156 | + } |
| 157 | +); |
| 158 | +TagCategory.displayName = 'TagCategory'; |
| 159 | + |
| 160 | +const TagCheckbox = memo(({ tag, ...rest }: CheckboxProps & { tag: WorkflowTag }) => { |
| 161 | + const dispatch = useAppDispatch(); |
| 162 | + const selectedTags = useAppSelector(selectWorkflowLibrarySelectedTags); |
| 163 | + const isSelected = selectedTags.includes(tag); |
| 164 | + |
| 165 | + const onChange = useCallback(() => { |
| 166 | + dispatch(workflowSelectedTagToggled(tag)); |
| 167 | + }, [dispatch, tag]); |
| 168 | + |
| 169 | + const { count } = useGetCountsQuery( |
| 170 | + { tags: [tag], categories: ['default'] }, |
| 171 | + { selectFromResult: ({ data }) => ({ count: data ?? 0 }) } |
| 172 | + ); |
| 173 | + |
| 174 | + if (count === 0) { |
| 175 | + return null; |
| 176 | + } |
| 177 | + |
| 178 | + return ( |
| 179 | + <Checkbox isChecked={isSelected} onChange={onChange} {...rest}> |
| 180 | + <Text>{`${tag} (${count})`}</Text> |
| 181 | + </Checkbox> |
| 182 | + ); |
| 183 | +}); |
| 184 | +TagCheckbox.displayName = 'TagCheckbox'; |
0 commit comments