mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add ability to archive/unarchive boards, add toggle to gallery settings to show/hide archived boards in list
This commit is contained in:
parent
5120a76ce5
commit
68c0aa898f
@ -17,6 +17,8 @@
|
||||
},
|
||||
"boards": {
|
||||
"addBoard": "Add Board",
|
||||
"archiveBoard": "Archive Board",
|
||||
"archived": "Archived",
|
||||
"autoAddBoard": "Auto-Add Board",
|
||||
"bottomMessage": "Deleting this board and its images will reset any features currently using them.",
|
||||
"cancel": "Cancel",
|
||||
@ -36,6 +38,7 @@
|
||||
"searchBoard": "Search Boards...",
|
||||
"selectBoard": "Select a Board",
|
||||
"topMessage": "This board contains images used in the following features:",
|
||||
"unarchiveBoard": "Unarchive Board",
|
||||
"uncategorized": "Uncategorized",
|
||||
"downloadBoard": "Download Board",
|
||||
"imagesWithCount_one": "{{count}} image",
|
||||
@ -387,6 +390,7 @@
|
||||
"openInViewer": "Open in Viewer",
|
||||
"selectAllOnPage": "Select All On Page",
|
||||
"selectAllOnBoard": "Select All On Board",
|
||||
"showArchivedBoards": "Show Archived Boards",
|
||||
"selectForCompare": "Select for Compare",
|
||||
"selectAnImageToCompare": "Select an Image to Compare",
|
||||
"slider": "Slider",
|
||||
|
@ -11,6 +11,7 @@ import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation } from 'services/api/endpoints/images';
|
||||
import { selectListBoardsQueryArgs } from '../../gallery/store/gallerySelectors';
|
||||
|
||||
const selectImagesToChange = createMemoizedSelector(
|
||||
selectChangeBoardModalSlice,
|
||||
@ -20,7 +21,8 @@ const selectImagesToChange = createMemoizedSelector(
|
||||
const ChangeBoardModal = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [selectedBoard, setSelectedBoard] = useState<string | null>();
|
||||
const { data: boards, isFetching } = useListAllBoardsQuery();
|
||||
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
|
||||
const { data: boards, isFetching } = useListAllBoardsQuery(queryArgs);
|
||||
const isModalOpen = useAppSelector((s) => s.changeBoardModal.isModalOpen);
|
||||
const imagesToChange = useAppSelector(selectImagesToChange);
|
||||
const [addImagesToBoard] = useAddImagesToBoardMutation();
|
||||
|
@ -11,25 +11,28 @@ const BoardAutoAddSelect = () => {
|
||||
const { t } = useTranslation();
|
||||
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const { options, hasBoards } = useListAllBoardsQuery(undefined, {
|
||||
selectFromResult: ({ data }) => {
|
||||
const options: ComboboxOption[] = [
|
||||
{
|
||||
label: t('controlnet.none'),
|
||||
value: 'none',
|
||||
},
|
||||
].concat(
|
||||
(data ?? []).map(({ board_id, board_name }) => ({
|
||||
label: board_name,
|
||||
value: board_id,
|
||||
}))
|
||||
);
|
||||
return {
|
||||
options,
|
||||
hasBoards: options.length > 1,
|
||||
};
|
||||
},
|
||||
});
|
||||
const { options, hasBoards } = useListAllBoardsQuery(
|
||||
{},
|
||||
{
|
||||
selectFromResult: ({ data }) => {
|
||||
const options: ComboboxOption[] = [
|
||||
{
|
||||
label: t('controlnet.none'),
|
||||
value: 'none',
|
||||
},
|
||||
].concat(
|
||||
(data ?? []).map(({ board_id, board_name }) => ({
|
||||
label: board_name,
|
||||
value: board_id,
|
||||
}))
|
||||
);
|
||||
return {
|
||||
options,
|
||||
hasBoards: options.length > 1,
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const onChange = useCallback<ComboboxOnChange>(
|
||||
(v) => {
|
||||
|
@ -7,12 +7,14 @@ import type { BoardId } from 'features/gallery/store/types';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiDownloadBold, PiPlusBold } from 'react-icons/pi';
|
||||
import { PiArchiveBold, PiArchiveFill, PiDownloadBold, PiPlusBold } from 'react-icons/pi';
|
||||
import { useBulkDownloadImagesMutation } from 'services/api/endpoints/images';
|
||||
import { useBoardName } from 'services/api/hooks/useBoardName';
|
||||
import type { BoardDTO } from 'services/api/types';
|
||||
|
||||
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
|
||||
import { useUpdateBoardMutation } from '../../../../services/api/endpoints/boards';
|
||||
import { MdArchive, MdUnarchive } from 'react-icons/md';
|
||||
|
||||
type Props = {
|
||||
board?: BoardDTO;
|
||||
@ -30,6 +32,8 @@ const BoardContextMenu = ({ board, board_id, setBoardToDelete, children }: Props
|
||||
[board]
|
||||
);
|
||||
|
||||
const [updateBoard] = useUpdateBoardMutation();
|
||||
|
||||
const isSelectedForAutoAdd = useAppSelector(selectIsSelectedForAutoAdd);
|
||||
const boardName = useBoardName(board_id);
|
||||
const isBulkDownloadEnabled = useFeatureStatus('bulkDownload');
|
||||
@ -44,13 +48,31 @@ const BoardContextMenu = ({ board, board_id, setBoardToDelete, children }: Props
|
||||
bulkDownload({ image_names: [], board_id: board_id });
|
||||
}, [board_id, bulkDownload]);
|
||||
|
||||
const handleArchive = useCallback(() => {
|
||||
updateBoard({
|
||||
board_id,
|
||||
changes: { archived: true },
|
||||
});
|
||||
}, [board_id, updateBoard]);
|
||||
|
||||
const handleUnarchive = useCallback(() => {
|
||||
updateBoard({
|
||||
board_id,
|
||||
changes: { archived: false },
|
||||
});
|
||||
}, [board_id, updateBoard]);
|
||||
|
||||
const isBoardArchived = useMemo(() => {
|
||||
return !!board?.archived;
|
||||
}, [board]);
|
||||
|
||||
const renderMenuFunc = useCallback(
|
||||
() => (
|
||||
<MenuList visibility="visible">
|
||||
<MenuGroup title={boardName}>
|
||||
<MenuItem
|
||||
icon={<PiPlusBold />}
|
||||
isDisabled={isSelectedForAutoAdd || autoAssignBoardOnClick}
|
||||
isDisabled={isSelectedForAutoAdd || autoAssignBoardOnClick || isBoardArchived}
|
||||
onClick={handleSetAutoAdd}
|
||||
>
|
||||
{t('boards.menuItemAutoAdd')}
|
||||
@ -60,6 +82,17 @@ const BoardContextMenu = ({ board, board_id, setBoardToDelete, children }: Props
|
||||
{t('boards.downloadBoard')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{board &&
|
||||
(isBoardArchived ? (
|
||||
<MenuItem icon={<PiArchiveBold />} onClick={handleUnarchive}>
|
||||
{t('boards.unarchiveBoard')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem icon={<PiArchiveFill />} onClick={handleArchive}>
|
||||
{t('boards.archiveBoard')}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
{board && <GalleryBoardContextMenuItems board={board} setBoardToDelete={setBoardToDelete} />}
|
||||
</MenuGroup>
|
||||
</MenuList>
|
||||
|
@ -3,9 +3,10 @@ import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'servic
|
||||
|
||||
type Props = {
|
||||
board_id: string;
|
||||
isArchived: boolean;
|
||||
};
|
||||
|
||||
export const BoardTotalsTooltip = ({ board_id }: Props) => {
|
||||
export const BoardTotalsTooltip = ({ board_id, isArchived }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { imagesTotal } = useGetBoardImagesTotalQuery(board_id, {
|
||||
selectFromResult: ({ data }) => {
|
||||
@ -17,5 +18,5 @@ export const BoardTotalsTooltip = ({ board_id }: Props) => {
|
||||
return { assetsTotal: data?.total ?? 0 };
|
||||
},
|
||||
});
|
||||
return `${t('boards.imagesWithCount', { count: imagesTotal })}, ${t('boards.assetsWithCount', { count: assetsTotal })}`;
|
||||
return `${t('boards.imagesWithCount', { count: imagesTotal })}, ${t('boards.assetsWithCount', { count: assetsTotal })}${isArchived ? ` (${t('boards.archived')})` : ''}`;
|
||||
};
|
||||
|
@ -12,6 +12,7 @@ import AddBoardButton from './AddBoardButton';
|
||||
import BoardsSearch from './BoardsSearch';
|
||||
import GalleryBoard from './GalleryBoard';
|
||||
import NoBoardBoard from './NoBoardBoard';
|
||||
import { selectListBoardsQueryArgs } from '../../../store/gallerySelectors';
|
||||
|
||||
const overlayScrollbarsStyles: CSSProperties = {
|
||||
height: '100%',
|
||||
@ -26,7 +27,9 @@ const BoardsList = (props: Props) => {
|
||||
const { isOpen } = props;
|
||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||
const { data: boards } = useListAllBoardsQuery();
|
||||
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
|
||||
console.log({ queryArgs });
|
||||
const { data: boards } = useListAllBoardsQuery(queryArgs);
|
||||
const filteredBoards = boardSearchText
|
||||
? boards?.filter((board) => board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()))
|
||||
: boards;
|
||||
|
@ -12,7 +12,7 @@ import { BoardTotalsTooltip } from 'features/gallery/components/Boards/BoardsLis
|
||||
import { autoAddBoardIdChanged, boardIdSelected, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImagesSquare } from 'react-icons/pi';
|
||||
import { PiArchiveBold, PiImagesSquare } from 'react-icons/pi';
|
||||
import { useUpdateBoardMutation } from 'services/api/endpoints/boards';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import type { BoardDTO } from 'services/api/types';
|
||||
@ -33,6 +33,7 @@ interface GalleryBoardProps {
|
||||
|
||||
const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const selectIsSelectedForAutoAdd = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => board.board_id === gallery.autoAddBoardId),
|
||||
@ -103,7 +104,7 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
|
||||
const handleChange = useCallback((newBoardName: string) => {
|
||||
setLocalBoardName(newBoardName);
|
||||
}, []);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box w="full" h="full" userSelect="none">
|
||||
<Flex
|
||||
@ -118,7 +119,10 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
|
||||
>
|
||||
<BoardContextMenu board={board} board_id={board_id} setBoardToDelete={setBoardToDelete}>
|
||||
{(ref) => (
|
||||
<Tooltip label={<BoardTotalsTooltip board_id={board.board_id} />} openDelay={1000}>
|
||||
<Tooltip
|
||||
label={<BoardTotalsTooltip board_id={board.board_id} isArchived={Boolean(board.archived)} />}
|
||||
openDelay={1000}
|
||||
>
|
||||
<Flex
|
||||
ref={ref}
|
||||
onClick={handleSelectBoard}
|
||||
@ -131,6 +135,25 @@ const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps
|
||||
cursor="pointer"
|
||||
bg="base.800"
|
||||
>
|
||||
{board.archived && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 1,
|
||||
insetInlineEnd: 2,
|
||||
p: 0,
|
||||
minW: 0,
|
||||
svg: {
|
||||
transitionProperty: 'common',
|
||||
transitionDuration: 'normal',
|
||||
fill: 'base.300',
|
||||
filter: 'drop-shadow(0px 0px 0.1rem var(--invoke-colors-base-800))',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon as={PiArchiveBold} />
|
||||
</Box>
|
||||
)}
|
||||
{coverImage?.thumbnail_url ? (
|
||||
<Image
|
||||
src={coverImage?.thumbnail_url}
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
autoAssignBoardOnClickChanged,
|
||||
setGalleryImageMinimumWidth,
|
||||
shouldAutoSwitchChanged,
|
||||
shouldShowArchivedBoardsChanged,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
@ -38,6 +39,7 @@ const GallerySettingsPopover = () => {
|
||||
const shouldAutoSwitch = useAppSelector((s) => s.gallery.shouldAutoSwitch);
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge);
|
||||
const shouldShowArchivedBoards = useAppSelector((s) => s.gallery.shouldShowArchivedBoards);
|
||||
|
||||
const handleChangeGalleryImageMinimumWidth = useCallback(
|
||||
(v: number) => {
|
||||
@ -63,6 +65,11 @@ const GallerySettingsPopover = () => {
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleChangeShouldShowArchivedBoardsChanged = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(shouldShowArchivedBoardsChanged(e.target.checked)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
@ -99,6 +106,10 @@ const GallerySettingsPopover = () => {
|
||||
<FormLabel>{t('gallery.alwaysShowImageSizeBadge')}</FormLabel>
|
||||
<Checkbox isChecked={alwaysShowImageSizeBadge} onChange={handleChangeAlwaysShowImageSizeBadgeChanged} />
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel>{t('gallery.showArchivedBoards')}</FormLabel>
|
||||
<Checkbox isChecked={shouldShowArchivedBoards} onChange={handleChangeShouldShowArchivedBoardsChanged} />
|
||||
</FormControl>
|
||||
</FormControlGroup>
|
||||
<BoardAutoAddSelect />
|
||||
</Flex>
|
||||
|
@ -3,7 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import type { ListImagesArgs } from 'services/api/types';
|
||||
import type { ListBoardsArgs, ListImagesArgs } from 'services/api/types';
|
||||
|
||||
export const selectLastSelectedImage = createMemoizedSelector(
|
||||
selectGallerySlice,
|
||||
@ -23,3 +23,10 @@ export const selectListImagesQueryArgs = createMemoizedSelector(
|
||||
}
|
||||
: skipToken
|
||||
);
|
||||
|
||||
export const selectListBoardsQueryArgs = createMemoizedSelector(
|
||||
selectGallerySlice,
|
||||
(gallery): ListBoardsArgs => ({
|
||||
archived: gallery.shouldShowArchivedBoards ? true : undefined
|
||||
})
|
||||
);
|
||||
|
@ -25,6 +25,7 @@ const initialGalleryState: GalleryState = {
|
||||
imageToCompare: null,
|
||||
comparisonMode: 'slider',
|
||||
comparisonFit: 'fill',
|
||||
shouldShowArchivedBoards: false,
|
||||
};
|
||||
|
||||
export const gallerySlice = createSlice({
|
||||
@ -110,6 +111,9 @@ export const gallerySlice = createSlice({
|
||||
limitChanged: (state, action: PayloadAction<number>) => {
|
||||
state.limit = action.payload;
|
||||
},
|
||||
shouldShowArchivedBoardsChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldShowArchivedBoards = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addMatcher(isAnyBoardDeleted, (state, action) => {
|
||||
@ -154,6 +158,7 @@ export const {
|
||||
comparisonModeCycled,
|
||||
offsetChanged,
|
||||
limitChanged,
|
||||
shouldShowArchivedBoardsChanged,
|
||||
} = gallerySlice.actions;
|
||||
|
||||
const isAnyBoardDeleted = isAnyOf(
|
||||
|
@ -25,4 +25,5 @@ export type GalleryState = {
|
||||
comparisonMode: ComparisonMode;
|
||||
comparisonFit: ComparisonFit;
|
||||
isImageViewerOpen: boolean;
|
||||
shouldShowArchivedBoards: boolean;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import { Combobox, FormControl } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { fieldBoardValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { BoardFieldInputInstance, BoardFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@ -8,12 +8,14 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
|
||||
import type { FieldComponentProps } from './types';
|
||||
import { selectListBoardsQueryArgs } from '../../../../../../../gallery/store/gallerySelectors';
|
||||
|
||||
const BoardFieldInputComponent = (props: FieldComponentProps<BoardFieldInputInstance, BoardFieldInputTemplate>) => {
|
||||
const { nodeId, field } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { options, hasBoards } = useListAllBoardsQuery(undefined, {
|
||||
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
|
||||
const { options, hasBoards } = useListAllBoardsQuery(queryArgs, {
|
||||
selectFromResult: ({ data }) => {
|
||||
const options: ComboboxOption[] = [
|
||||
{
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import type { BoardDTO, OffsetPaginatedResults_ImageDTO_, UpdateBoardArg } from 'services/api/types';
|
||||
import type { BoardDTO, ListBoardsArgs, OffsetPaginatedResults_ImageDTO_, UpdateBoardArg } from 'services/api/types';
|
||||
import { getListImagesUrl } from 'services/api/util';
|
||||
|
||||
import type { ApiTagDescription } from '..';
|
||||
@ -18,10 +18,10 @@ export const boardsApi = api.injectEndpoints({
|
||||
/**
|
||||
* Boards Queries
|
||||
*/
|
||||
listAllBoards: build.query<Array<BoardDTO>, void>({
|
||||
query: () => ({
|
||||
listAllBoards: build.query<Array<BoardDTO>, ListBoardsArgs>({
|
||||
query: (args) => ({
|
||||
url: buildBoardsUrl(),
|
||||
params: { all: true },
|
||||
params: { all: true, ...args },
|
||||
}),
|
||||
providesTags: (result) => {
|
||||
// any list of boards
|
||||
|
@ -1,9 +1,10 @@
|
||||
import type { BoardId } from 'features/gallery/store/types';
|
||||
import { t } from 'i18next';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { selectListBoardsQueryArgs } from '../../../features/gallery/store/gallerySelectors';
|
||||
|
||||
export const useBoardName = (board_id: BoardId) => {
|
||||
const { boardName } = useListAllBoardsQuery(undefined, {
|
||||
const { boardName } = useListAllBoardsQuery({}, {
|
||||
selectFromResult: ({ data }) => {
|
||||
const selectedBoard = data?.find((b) => b.board_id === board_id);
|
||||
const boardName = selectedBoard?.board_name || t('boards.uncategorized');
|
||||
|
File diff suppressed because one or more lines are too long
@ -6,6 +6,8 @@ export type S = components['schemas'];
|
||||
export type ListImagesArgs = NonNullable<paths['/api/v1/images/']['get']['parameters']['query']>;
|
||||
export type ListImagesResponse = paths['/api/v1/images/']['get']['responses']['200']['content']['application/json'];
|
||||
|
||||
export type ListBoardsArgs = NonNullable<paths['/api/v1/boards/']['get']['parameters']['query']>;
|
||||
|
||||
export type DeleteBoardResult =
|
||||
paths['/api/v1/boards/{board_id}']['delete']['responses']['200']['content']['application/json'];
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user