(ui): clean up old workflow library

This commit is contained in:
Mary Hipp
2024-10-07 20:15:15 -04:00
committed by Mary Hipp Rogers
parent 9092280583
commit 1d32e70a75
11 changed files with 0 additions and 568 deletions

View File

@@ -1,26 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { NewWorkflowConfirmationAlertDialog } from 'features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFilePlusBold } from 'react-icons/pi';
export const NewWorkflowButton = memo(() => {
const { t } = useTranslation();
const renderButton = useCallback(
(onClick: () => void) => (
<IconButton
aria-label={t('nodes.newWorkflow')}
tooltip={t('nodes.newWorkflow')}
icon={<PiFilePlusBold />}
onClick={onClick}
pointerEvents="auto"
/>
),
[t]
);
return <NewWorkflowConfirmationAlertDialog renderButton={renderButton} />;
});
NewWorkflowButton.displayName = 'NewWorkflowButton';

View File

@@ -1,28 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useWorkflowLibraryModal } from 'features/workflowLibrary/store/isWorkflowLibraryModalOpen';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFolderOpenBold } from 'react-icons/pi';
import WorkflowLibraryModal from './WorkflowLibraryModal';
const WorkflowLibraryButton = () => {
const { t } = useTranslation();
const workflowLibraryModal = useWorkflowLibraryModal();
return (
<>
<IconButton
aria-label={t('workflows.workflowLibrary')}
tooltip={t('workflows.workflowLibrary')}
icon={<PiFolderOpenBold />}
onClick={workflowLibraryModal.setTrue}
pointerEvents="auto"
/>
<WorkflowLibraryModal />
</>
);
};
export default memo(WorkflowLibraryButton);

View File

@@ -1,13 +0,0 @@
import WorkflowLibraryList from 'features/workflowLibrary/components/WorkflowLibraryList';
import WorkflowLibraryListWrapper from 'features/workflowLibrary/components/WorkflowLibraryListWrapper';
import { memo } from 'react';
const WorkflowLibraryContent = () => {
return (
<WorkflowLibraryListWrapper>
<WorkflowLibraryList />
</WorkflowLibraryListWrapper>
);
};
export default memo(WorkflowLibraryContent);

View File

@@ -1,252 +0,0 @@
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
import {
Box,
Button,
ButtonGroup,
Combobox,
Divider,
Flex,
FormControl,
FormLabel,
IconButton,
Input,
InputGroup,
InputRightElement,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $projectId } from 'app/store/nanostores/projectId';
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import WorkflowLibraryListItem from 'features/workflowLibrary/components/WorkflowLibraryListItem';
import WorkflowLibraryPagination from 'features/workflowLibrary/components/WorkflowLibraryPagination';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXBold } from 'react-icons/pi';
import { useListWorkflowsQuery } from 'services/api/endpoints/workflows';
import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
import { useDebounce } from 'use-debounce';
import { z } from 'zod';
import UploadWorkflowButton from './UploadWorkflowButton';
const PER_PAGE = 10;
const zOrderBy = z.enum(['opened_at', 'created_at', 'updated_at', 'name']);
type OrderBy = z.infer<typeof zOrderBy>;
const isOrderBy = (v: unknown): v is OrderBy => zOrderBy.safeParse(v).success;
const zDirection = z.enum(['ASC', 'DESC']);
type Direction = z.infer<typeof zDirection>;
const isDirection = (v: unknown): v is Direction => zDirection.safeParse(v).success;
const WorkflowLibraryList = () => {
const { t } = useTranslation();
const workflowCategories = useStore($workflowCategories);
const [selectedCategory, setSelectedCategory] = useState<WorkflowCategory>('user');
const [page, setPage] = useState(0);
const [query, setQuery] = useState('');
const projectId = useStore($projectId);
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') },
],
[t]
);
const DIRECTION_OPTIONS: ComboboxOption[] = useMemo(
() => [
{ value: 'ASC', label: t('workflows.ascending') },
{ value: 'DESC', label: t('workflows.descending') },
],
[t]
);
const orderByOptions = useMemo(() => {
return projectId ? ORDER_BY_OPTIONS.filter((option) => option.value !== 'opened_at') : ORDER_BY_OPTIONS;
}, [projectId, ORDER_BY_OPTIONS]);
const [order_by, setOrderBy] = useState<WorkflowRecordOrderBy>(orderByOptions[0]?.value as WorkflowRecordOrderBy);
const [direction, setDirection] = useState<SQLiteDirection>('DESC');
const [debouncedQuery] = useDebounce(query, 500);
const queryArg = useMemo<Parameters<typeof useListWorkflowsQuery>[0]>(() => {
if (selectedCategory !== 'default') {
return {
page,
per_page: PER_PAGE,
order_by,
direction,
category: selectedCategory,
query: debouncedQuery,
};
}
return {
page,
per_page: PER_PAGE,
order_by: 'name' as const,
direction: 'ASC' as const,
category: selectedCategory,
query: debouncedQuery,
};
}, [selectedCategory, debouncedQuery, direction, order_by, page]);
const { data, isLoading, isError, isFetching } = useListWorkflowsQuery(queryArg);
const onChangeOrderBy = useCallback<ComboboxOnChange>(
(v) => {
if (!isOrderBy(v?.value) || v.value === order_by) {
return;
}
setOrderBy(v.value);
setPage(0);
},
[order_by]
);
const valueOrderBy = useMemo(() => orderByOptions.find((o) => o.value === order_by), [order_by, orderByOptions]);
const onChangeDirection = useCallback<ComboboxOnChange>(
(v) => {
if (!isDirection(v?.value) || v.value === direction) {
return;
}
setDirection(v.value);
setPage(0);
},
[direction]
);
const valueDirection = useMemo(
() => DIRECTION_OPTIONS.find((o) => o.value === direction),
[direction, DIRECTION_OPTIONS]
);
const resetFilterText = useCallback(() => {
setQuery('');
setPage(0);
}, []);
const handleKeydownFilterText = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
// exit search mode on escape
if (e.key === 'Escape') {
resetFilterText();
e.preventDefault();
setPage(0);
}
},
[resetFilterText]
);
const handleChangeFilterText = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
setPage(0);
}, []);
const handleSetCategory = useCallback((category: WorkflowCategory) => {
setSelectedCategory(category);
setPage(0);
}, []);
return (
<>
<Flex gap={4} alignItems="center" h={16} flexShrink={0} flexGrow={0} justifyContent="space-between">
<ButtonGroup alignSelf="flex-end">
{workflowCategories.map((category) => (
<Button
key={category}
variant={selectedCategory === category ? undefined : 'ghost'}
onClick={handleSetCategory.bind(null, category)}
isChecked={selectedCategory === category}
>
{t(`workflows.${category}Workflows`)}
</Button>
))}
</ButtonGroup>
{selectedCategory !== 'default' && (
<>
<FormControl
isDisabled={isFetching}
sx={{
flexDir: 'column',
alignItems: 'flex-start',
gap: 1,
maxW: 56,
}}
>
<FormLabel>{t('common.orderBy')}</FormLabel>
<Combobox value={valueOrderBy} options={orderByOptions} onChange={onChangeOrderBy} />
</FormControl>
<FormControl
isDisabled={isFetching}
sx={{
flexDir: 'column',
alignItems: 'flex-start',
gap: 1,
maxW: 56,
}}
>
<FormLabel>{t('common.direction')}</FormLabel>
<Combobox value={valueDirection} options={DIRECTION_OPTIONS} onChange={onChangeDirection} />
</FormControl>
</>
)}
<InputGroup w="20rem" alignSelf="flex-end">
<Input
placeholder={t('workflows.searchWorkflows')}
value={query}
onKeyDown={handleKeydownFilterText}
onChange={handleChangeFilterText}
data-testid="workflow-search-input"
minW={64}
/>
{query.trim().length && (
<InputRightElement h="full" pe={2}>
<IconButton
onClick={resetFilterText}
size="sm"
variant="link"
aria-label={t('workflows.clearWorkflowSearchFilter')}
icon={<PiXBold />}
/>
</InputRightElement>
)}
</InputGroup>
</Flex>
<Divider />
{isLoading ? (
<IAINoContentFallbackWithSpinner label={t('workflows.loading')} />
) : !data || isError ? (
<IAINoContentFallback label={t('workflows.problemLoading')} />
) : data.items.length ? (
<ScrollableContent>
<Flex w="full" h="full" flexDir="column">
{data.items.map((w) => (
<WorkflowLibraryListItem key={w.workflow_id} workflowDTO={w} />
))}
</Flex>
</ScrollableContent>
) : (
<IAINoContentFallback label={t('workflows.noWorkflows')} />
)}
<Divider />
<Flex w="full">
<Box flex="1">
<UploadWorkflowButton />
</Box>
<Box flex="1" textAlign="center">
{data && <WorkflowLibraryPagination data={data} page={page} setPage={setPage} />}
</Box>
<Box flex="1"></Box>
</Flex>
</>
);
};
export default memo(WorkflowLibraryList);

View File

@@ -1,98 +0,0 @@
import { Button, Flex, Heading, Spacer, Text } from '@invoke-ai/ui-library';
import { EMPTY_OBJECT } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import dateFormat, { masks } from 'dateformat';
import { selectWorkflowId } from 'features/nodes/store/workflowSlice';
import { useDeleteLibraryWorkflow } from 'features/workflowLibrary/hooks/useDeleteLibraryWorkflow';
import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow';
import { useWorkflowLibraryModal } from 'features/workflowLibrary/store/isWorkflowLibraryModalOpen';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import type { WorkflowRecordListItemDTO } from 'services/api/types';
type Props = {
workflowDTO: WorkflowRecordListItemDTO;
};
const WorkflowLibraryListItem = ({ workflowDTO }: Props) => {
const { t } = useTranslation();
const workflowId = useAppSelector(selectWorkflowId);
const workflowLibraryModal = useWorkflowLibraryModal();
const { deleteWorkflow, deleteWorkflowResult } = useDeleteLibraryWorkflow(EMPTY_OBJECT);
const { getAndLoadWorkflow, getAndLoadWorkflowResult } = useGetAndLoadLibraryWorkflow({
onSuccess: workflowLibraryModal.setFalse,
});
const handleDeleteWorkflow = useCallback(() => {
deleteWorkflow(workflowDTO.workflow_id);
}, [deleteWorkflow, workflowDTO.workflow_id]);
const handleGetAndLoadWorkflow = useCallback(() => {
getAndLoadWorkflow(workflowDTO.workflow_id);
}, [getAndLoadWorkflow, workflowDTO.workflow_id]);
const isOpen = useMemo(() => workflowId === workflowDTO.workflow_id, [workflowId, workflowDTO.workflow_id]);
return (
<Flex key={workflowDTO.workflow_id} w="full" p={2} borderRadius="base" _hover={{ bg: 'base.750' }}>
<Flex w="full" alignItems="center" gap={2} h={12}>
<Flex flexDir="column" flexGrow={1} h="full">
<Flex alignItems="center" w="full" h="50%">
<Heading size="sm" noOfLines={1} variant={isOpen ? 'invokeBlue' : undefined}>
{workflowDTO.name || t('workflows.unnamedWorkflow')}
</Heading>
<Spacer />
{workflowDTO.category !== 'default' && (
<Text fontSize="sm" variant="subtext" flexShrink={0} noOfLines={1}>
{t('common.updated')}: {dateFormat(workflowDTO.updated_at, masks.shortDate)}{' '}
{dateFormat(workflowDTO.updated_at, masks.shortTime)}
</Text>
)}
</Flex>
<Flex alignItems="center" w="full" h="50%">
{workflowDTO.description ? (
<Text fontSize="sm" noOfLines={1}>
{workflowDTO.description}
</Text>
) : (
<Text fontSize="sm" variant="subtext" fontStyle="italic" noOfLines={1}>
{t('workflows.noDescription')}
</Text>
)}
<Spacer />
{workflowDTO.category !== 'default' && (
<Text fontSize="sm" variant="subtext" flexShrink={0} noOfLines={1}>
{t('common.created')}: {dateFormat(workflowDTO.created_at, masks.shortDate)}{' '}
{dateFormat(workflowDTO.created_at, masks.shortTime)}
</Text>
)}
</Flex>
</Flex>
<Button
flexShrink={0}
isDisabled={isOpen}
onClick={handleGetAndLoadWorkflow}
isLoading={getAndLoadWorkflowResult.isLoading}
aria-label={t('workflows.openWorkflow')}
>
{t('common.load')}
</Button>
{workflowDTO.category !== 'default' && (
<Button
flexShrink={0}
colorScheme="error"
isDisabled={isOpen}
onClick={handleDeleteWorkflow}
isLoading={deleteWorkflowResult.isLoading}
aria-label={t('workflows.deleteWorkflow')}
>
{t('common.delete')}
</Button>
)}
</Flex>
</Flex>
);
};
export default memo(WorkflowLibraryListItem);

View File

@@ -1,13 +0,0 @@
import { Flex } from '@invoke-ai/ui-library';
import type { PropsWithChildren } from 'react';
import { memo } from 'react';
const WorkflowLibraryListWrapper = (props: PropsWithChildren) => {
return (
<Flex w="full" h="full" flexDir="column" layerStyle="second" py={2} px={4} gap={2} borderRadius="base">
{props.children}
</Flex>
);
};
export default memo(WorkflowLibraryListWrapper);

View File

@@ -1,34 +0,0 @@
import {
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@invoke-ai/ui-library';
import WorkflowLibraryContent from 'features/workflowLibrary/components/WorkflowLibraryContent';
import { useWorkflowLibraryModal } from 'features/workflowLibrary/store/isWorkflowLibraryModalOpen';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const WorkflowLibraryModal = () => {
const { t } = useTranslation();
const workflowLibraryModal = useWorkflowLibraryModal();
return (
<Modal isOpen={workflowLibraryModal.isTrue} onClose={workflowLibraryModal.setFalse} isCentered useInert={false}>
<ModalOverlay />
<ModalContent w="80%" h="80%" minW="unset" minH="unset" maxW="1200px" maxH="664px">
<ModalHeader>{t('workflows.workflowLibrary')}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<WorkflowLibraryContent />
</ModalBody>
<ModalFooter />
</ModalContent>
</Modal>
);
};
export default memo(WorkflowLibraryModal);

View File

@@ -1,80 +0,0 @@
import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library';
import type { Dispatch, SetStateAction } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
import type { paths } from 'services/api/schema';
const PAGES_TO_DISPLAY = 7;
type PageData = {
page: number;
onClick: () => void;
};
type Props = {
page: number;
setPage: Dispatch<SetStateAction<number>>;
data: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'];
};
const WorkflowLibraryPagination = ({ page, setPage, data }: Props) => {
const { t } = useTranslation();
const handlePrevPage = useCallback(() => {
setPage((p) => Math.max(p - 1, 0));
}, [setPage]);
const handleNextPage = useCallback(() => {
setPage((p) => Math.min(p + 1, data.pages - 1));
}, [data.pages, setPage]);
const pages: PageData[] = useMemo(() => {
const pages = [];
let first = data.pages > PAGES_TO_DISPLAY ? Math.max(0, page - Math.floor(PAGES_TO_DISPLAY / 2)) : 0;
const last = data.pages > PAGES_TO_DISPLAY ? Math.min(data.pages, first + PAGES_TO_DISPLAY) : data.pages;
if (last - first < PAGES_TO_DISPLAY && data.pages > PAGES_TO_DISPLAY) {
first = last - PAGES_TO_DISPLAY;
}
for (let i = first; i < last; i++) {
pages.push({
page: i,
onClick: () => setPage(i),
});
}
return pages;
}, [data.pages, page, setPage]);
return (
<ButtonGroup>
<IconButton
variant="ghost"
onClick={handlePrevPage}
isDisabled={page === 0}
aria-label={t('common.prevPage')}
icon={<PiCaretLeftBold />}
/>
{pages.map((p) => (
<Button
w={10}
isDisabled={data.pages === 1}
onClick={p.page === page ? undefined : p.onClick}
variant={p.page === page ? 'solid' : 'ghost'}
key={p.page}
transitionDuration="0s" // the delay in animation looks jank
>
{p.page + 1}
</Button>
))}
<IconButton
variant="ghost"
onClick={handleNextPage}
isDisabled={page === data.pages - 1}
aria-label={t('common.nextPage')}
icon={<PiCaretRightBold />}
/>
</ButtonGroup>
);
};
export default memo(WorkflowLibraryPagination);