mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): workflows panel redesign WIP
This commit is contained in:
@@ -11,7 +11,7 @@ export const ActiveWorkflowName = memo(() => {
|
||||
|
||||
if (workflowName) {
|
||||
return (
|
||||
<Text colorScheme="invokeBlue" fontWeight="semibold" fontSize="md" justifySelf="flex-start" noOfLines={1}>
|
||||
<Text fontWeight="semibold" fontSize="md" justifySelf="flex-start" noOfLines={1}>
|
||||
{workflowName}
|
||||
</Text>
|
||||
);
|
||||
@@ -19,7 +19,7 @@ export const ActiveWorkflowName = memo(() => {
|
||||
|
||||
// activeWorkflowName is always a string - if it is an empty string, it implies we do not have a workflow selected
|
||||
return (
|
||||
<Text fontSize="sm" fontWeight="semibold" color="base.300" noOfLines={1}>
|
||||
<Text fontSize="md" fontWeight="semibold" color="base.300" noOfLines={1}>
|
||||
{t('workflows.chooseWorkflowFromLibrary')}
|
||||
</Text>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { NewWorkflowButton } from 'features/nodes/components/sidePanel/NewWorkflowButton';
|
||||
import { ActiveWorkflowName } from 'features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowName';
|
||||
import { WorkflowListMenuTrigger } from 'features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger';
|
||||
import { WorkflowViewEditToggleButton } from 'features/nodes/components/sidePanel/WorkflowViewEditToggleButton';
|
||||
import { selectWorkflowMode } from 'features/nodes/store/workflowSlice';
|
||||
@@ -14,12 +13,11 @@ export const ActiveWorkflowNameAndActions = memo(() => {
|
||||
|
||||
return (
|
||||
<Flex w="full" alignItems="center" gap={2} minW={0}>
|
||||
<ActiveWorkflowName />
|
||||
<WorkflowListMenuTrigger />
|
||||
<Spacer />
|
||||
{mode === 'edit' && <SaveWorkflowButton />}
|
||||
<WorkflowViewEditToggleButton />
|
||||
<NewWorkflowButton />
|
||||
<WorkflowListMenuTrigger />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,19 +3,20 @@ import { useStore } from '@nanostores/react';
|
||||
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import UploadWorkflowButton from 'features/workflowLibrary/components/UploadWorkflowButton';
|
||||
import type { RefObject } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { WorkflowList } from './WorkflowList';
|
||||
import WorkflowSearch from './WorkflowSearch';
|
||||
import { WorkflowSearch } from './WorkflowSearch';
|
||||
import { WorkflowSortControl } from './WorkflowSortControl';
|
||||
|
||||
export const WorkflowListMenuContent = memo(() => {
|
||||
export const WorkflowListMenuContent = memo(({ searchInputRef }: { searchInputRef: RefObject<HTMLInputElement> }) => {
|
||||
const workflowCategories = useStore($workflowCategories);
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
|
||||
<WorkflowSearch />
|
||||
<WorkflowSearch searchInputRef={searchInputRef} />
|
||||
<WorkflowSortControl />
|
||||
<UploadWorkflowButton />
|
||||
</Flex>
|
||||
|
||||
@@ -1,28 +1,58 @@
|
||||
import { IconButton, Popover, PopoverBody, PopoverContent, PopoverTrigger, Portal } from '@invoke-ai/ui-library';
|
||||
import { WorkflowListMenuContent } from 'features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuContent';
|
||||
import { Box, Button, Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Portal } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||
import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu';
|
||||
import { selectWorkflowName } from 'features/nodes/store/workflowSlice';
|
||||
import UploadWorkflowButton from 'features/workflowLibrary/components/UploadWorkflowButton';
|
||||
import { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiFolderOpenFill } from 'react-icons/pi';
|
||||
|
||||
import { WorkflowList } from './WorkflowList';
|
||||
import { WorkflowSearch } from './WorkflowSearch';
|
||||
import { WorkflowSortControl } from './WorkflowSortControl';
|
||||
|
||||
export const WorkflowListMenuTrigger = () => {
|
||||
const workflowListMenu = useWorkflowListMenu();
|
||||
const { t } = useTranslation();
|
||||
const workflowCategories = useStore($workflowCategories);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const workflowName = useAppSelector(selectWorkflowName);
|
||||
|
||||
return (
|
||||
<Popover isOpen={workflowListMenu.isOpen} onClose={workflowListMenu.close} onOpen={workflowListMenu.open}>
|
||||
<Popover
|
||||
isOpen={workflowListMenu.isOpen}
|
||||
onClose={workflowListMenu.close}
|
||||
onOpen={workflowListMenu.open}
|
||||
isLazy
|
||||
lazyBehavior="unmount"
|
||||
placement="bottom-end"
|
||||
initialFocusRef={searchInputRef}
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label={t('workflows.openLibrary')}
|
||||
tooltip={t('workflows.openLibrary')}
|
||||
variant="ghost"
|
||||
icon={<PiFolderOpenFill />}
|
||||
size="sm"
|
||||
/>
|
||||
<Button variant="ghost" rightIcon={<PiFolderOpenFill />}>
|
||||
{workflowName || t('workflows.chooseWorkflowFromLibrary')}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<Portal appendToParentPortal={false}>
|
||||
<Portal>
|
||||
<PopoverContent p={4} w={512} maxW="full" minH={512} maxH="full">
|
||||
<PopoverBody flex="1 1 0">
|
||||
<WorkflowListMenuContent />
|
||||
<Flex w="full" h="full" flexDir="column" gap={2}>
|
||||
<Flex alignItems="center" gap={2} w="full" justifyContent="space-between">
|
||||
<WorkflowSearch searchInputRef={searchInputRef} />
|
||||
<WorkflowSortControl />
|
||||
<UploadWorkflowButton />
|
||||
</Flex>
|
||||
<Box position="relative" w="full" h="full">
|
||||
<ScrollableContent>
|
||||
{workflowCategories.map((category) => (
|
||||
<WorkflowList key={category} category={category} />
|
||||
))}
|
||||
</ScrollableContent>
|
||||
</Box>
|
||||
</Flex>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowSearchTerm, workflowSearchTermChanged } from 'features/nodes/store/workflowSlice';
|
||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import type { ChangeEvent, KeyboardEvent, RefObject } from 'react';
|
||||
import { memo, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiXBold } from 'react-icons/pi';
|
||||
|
||||
const WorkflowSearch = () => {
|
||||
export const WorkflowSearch = memo(({ searchInputRef }: { searchInputRef: RefObject<HTMLInputElement> }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const searchTerm = useAppSelector(selectWorkflowSearchTerm);
|
||||
const { t } = useTranslation();
|
||||
@@ -39,9 +39,16 @@ const WorkflowSearch = () => {
|
||||
[handleWorkflowSearch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchInputRef.current) {
|
||||
searchInputRef.current.focus();
|
||||
}
|
||||
}, [searchInputRef]);
|
||||
|
||||
return (
|
||||
<InputGroup>
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder={t('stylePresets.searchByName')}
|
||||
value={searchTerm}
|
||||
onKeyDown={handleKeydown}
|
||||
@@ -60,6 +67,6 @@ const WorkflowSearch = () => {
|
||||
)}
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default memo(WorkflowSearch);
|
||||
WorkflowSearch.displayName = 'WorkflowSearch';
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import {
|
||||
Combobox,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
@@ -9,6 +7,7 @@ import {
|
||||
PopoverBody,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
Select,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $projectId } from 'app/store/nanostores/projectId';
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
workflowOrderByChanged,
|
||||
workflowOrderDirectionChanged,
|
||||
} from 'features/nodes/store/workflowSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiSortAscendingBold, PiSortDescendingBold } from 'react-icons/pi';
|
||||
@@ -39,62 +39,54 @@ export const WorkflowSortControl = () => {
|
||||
const orderBy = useAppSelector(selectWorkflowOrderBy);
|
||||
const direction = useAppSelector(selectWorkflowOrderDirection);
|
||||
|
||||
const ORDER_BY_OPTIONS: ComboboxOption[] = useMemo(
|
||||
() => [
|
||||
{ value: 'opened_at', label: t('workflows.opened') },
|
||||
{ value: 'created_at', label: t('workflows.created') },
|
||||
{ value: 'updated_at', label: t('workflows.updated') },
|
||||
{ value: 'name', label: t('workflows.name') },
|
||||
],
|
||||
const ORDER_BY_LABELS = useMemo(
|
||||
() => ({
|
||||
opened_at: t('workflows.opened'),
|
||||
created_at: t('workflows.created'),
|
||||
updated_at: t('workflows.updated'),
|
||||
name: t('workflows.name'),
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
const DIRECTION_OPTIONS: ComboboxOption[] = useMemo(
|
||||
() => [
|
||||
{ value: 'ASC', label: t('workflows.ascending') },
|
||||
{ value: 'DESC', label: t('workflows.descending') },
|
||||
],
|
||||
const DIRECTION_LABELS = useMemo(
|
||||
() => ({
|
||||
ASC: t('workflows.ascending'),
|
||||
DESC: t('workflows.descending'),
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const orderByOptions = useMemo(() => {
|
||||
return projectId ? ORDER_BY_OPTIONS.filter((option) => option.value !== 'opened_at') : ORDER_BY_OPTIONS;
|
||||
}, [projectId, ORDER_BY_OPTIONS]);
|
||||
|
||||
const onChangeOrderBy = useCallback<ComboboxOnChange>(
|
||||
(v) => {
|
||||
if (!isOrderBy(v?.value) || v.value === orderBy) {
|
||||
const onChangeOrderBy = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
if (!isOrderBy(e.target.value)) {
|
||||
return;
|
||||
}
|
||||
dispatch(workflowOrderByChanged(v.value));
|
||||
dispatch(workflowOrderByChanged(e.target.value));
|
||||
},
|
||||
[orderBy, dispatch]
|
||||
[dispatch]
|
||||
);
|
||||
const valueOrderBy = useMemo(() => {
|
||||
return orderByOptions.find((o) => o.value === orderBy) || orderByOptions[0];
|
||||
}, [orderBy, orderByOptions]);
|
||||
|
||||
const onChangeDirection = useCallback<ComboboxOnChange>(
|
||||
(v) => {
|
||||
if (!isDirection(v?.value) || v.value === direction) {
|
||||
const onChangeDirection = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
if (!isDirection(e.target.value)) {
|
||||
return;
|
||||
}
|
||||
dispatch(workflowOrderDirectionChanged(v.value));
|
||||
dispatch(workflowOrderDirectionChanged(e.target.value));
|
||||
},
|
||||
[direction, dispatch]
|
||||
);
|
||||
const valueDirection = useMemo(
|
||||
() => DIRECTION_OPTIONS.find((o) => o.value === direction),
|
||||
[direction, DIRECTION_OPTIONS]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// In OSS, we don't have the concept of "opened_at" for workflows. This is only available in the Enterprise version.
|
||||
const defaultOrderBy = projectId !== undefined ? 'opened_at' : 'created_at';
|
||||
|
||||
return (
|
||||
<Popover placement="bottom">
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
tooltip={`Sorting by ${valueOrderBy?.label} ${valueDirection?.label}`}
|
||||
tooltip={`Sorting by ${ORDER_BY_LABELS[orderBy ?? defaultOrderBy]} ${DIRECTION_LABELS[direction]}`}
|
||||
aria-label="Sort Workflow Library"
|
||||
icon={direction === 'ASC' ? <PiSortAscendingBold /> : <PiSortDescendingBold />}
|
||||
variant="ghost"
|
||||
@@ -106,11 +98,19 @@ export const WorkflowSortControl = () => {
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<FormControl orientation="horizontal" gap={1}>
|
||||
<FormLabel>{t('common.orderBy')}</FormLabel>
|
||||
<Combobox value={valueOrderBy} options={orderByOptions} onChange={onChangeOrderBy} />
|
||||
<Select value={orderBy ?? defaultOrderBy} onChange={onChangeOrderBy} size="sm">
|
||||
{projectId !== undefined && <option value="opened_at">{ORDER_BY_LABELS['opened_at']}</option>}
|
||||
<option value="created_at">{ORDER_BY_LABELS['created_at']}</option>
|
||||
<option value="updated_at">{ORDER_BY_LABELS['updated_at']}</option>
|
||||
<option value="name">{ORDER_BY_LABELS['name']}</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl orientation="horizontal" gap={1}>
|
||||
<FormLabel>{t('common.direction')}</FormLabel>
|
||||
<Combobox value={valueDirection} options={DIRECTION_OPTIONS} onChange={onChangeDirection} />
|
||||
<Select value={direction} onChange={onChangeDirection} size="sm">
|
||||
<option value="ASC">{DIRECTION_LABELS['ASC']}</option>
|
||||
<option value="DESC">{DIRECTION_LABELS['DESC']}</option>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</PopoverBody>
|
||||
|
||||
Reference in New Issue
Block a user