|
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