Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Boards UI update and add support for private boards #6588

Merged
merged 22 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6437ef3
add view that displays private boards with shared boards
chainchompa Jul 3, 2024
9ca6980
cleanup and bug fixes
chainchompa Jul 8, 2024
3a85ab1
update BoardRecord
chainchompa Jul 8, 2024
79a7b11
remove old boards list
chainchompa Jul 8, 2024
0e092c0
update is_private name
chainchompa Jul 9, 2024
1785825
add current gallery board name
chainchompa Jul 9, 2024
faf65c9
Merge branch 'main' into boards-ui-update
chainchompa Jul 9, 2024
38c5804
remove unused disclosure
chainchompa Jul 9, 2024
40c3b5e
generate types again
chainchompa Jul 9, 2024
e2667f9
prettier
chainchompa Jul 9, 2024
907b257
remove unused file and addressed pr feedback
chainchompa Jul 9, 2024
81cf47d
feat(ui): boards list layout & style tweaking
psychedelicious Jul 9, 2024
2faf1e2
fix(ui): show uncategorized board when private boards disabled
psychedelicious Jul 9, 2024
637802d
fix(ui): restore auto-add indicator
psychedelicious Jul 9, 2024
060d698
feat(ui): restore image count for boards
psychedelicious Jul 9, 2024
6014382
feat(ui): select a board when it is created
psychedelicious Jul 9, 2024
80e1b87
fix(ui): autoadd badge hides when editing name
psychedelicious Jul 9, 2024
d38d513
fix(ui): autoadd badge doesn't flex shrink
psychedelicious Jul 9, 2024
781b800
feat(ui): boards lists start collapsed
psychedelicious Jul 9, 2024
2460689
feat(ui): style board name
psychedelicious Jul 9, 2024
476ebd1
feat(ui): add board button tooltip when private boards enabled
psychedelicious Jul 9, 2024
a79e9ca
Merge branch 'main' into boards-ui-update
chainchompa Jul 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions invokeai/app/api/routers/boards.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class DeleteBoardResult(BaseModel):
)
async def create_board(
board_name: str = Query(description="The name of the board to create"),
is_private: bool = Query(default=False, description="Whether the board is private"),
) -> BoardDTO:
"""Creates a board"""
try:
Expand Down
4 changes: 4 additions & 0 deletions invokeai/app/services/board_records/board_records_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class BoardRecord(BaseModelExcludeNull):
"""The name of the cover image of the board."""
archived: bool = Field(description="Whether or not the board is archived.")
"""Whether or not the board is archived."""
is_private: Optional[bool] = Field(default=None, description="Whether the board is private.")
"""Whether the board is private."""


def deserialize_board_record(board_dict: dict) -> BoardRecord:
Expand All @@ -38,6 +40,7 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord:
updated_at = board_dict.get("updated_at", get_iso_timestamp())
deleted_at = board_dict.get("deleted_at", get_iso_timestamp())
archived = board_dict.get("archived", False)
is_private = board_dict.get("is_private", False)

return BoardRecord(
board_id=board_id,
Expand All @@ -47,6 +50,7 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord:
updated_at=updated_at,
deleted_at=deleted_at,
archived=archived,
is_private=is_private,
)


Expand Down
5 changes: 5 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
},
"boards": {
"addBoard": "Add Board",
"addPrivateBoard": "Add Private Board",
"addSharedBoard": "Add Shared Board",
"archiveBoard": "Archive Board",
"archived": "Archived",
"autoAddBoard": "Auto-Add Board",
"boards": "Boards",
"selectedForAutoAdd": "Selected for Auto-Add",
"bottomMessage": "Deleting this board and its images will reset any features currently using them.",
"cancel": "Cancel",
Expand All @@ -36,8 +39,10 @@
"movingImagesToBoard_other": "Moving {{count}} images to board:",
"myBoard": "My Board",
"noMatching": "No matching Boards",
"private": "Private",
"searchBoard": "Search Boards...",
"selectBoard": "Select a Board",
"shared": "Shared",
"topMessage": "This board contains images used in the following features:",
"unarchiveBoard": "Unarchive Board",
"uncategorized": "Uncategorized",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ export const addArchivedOrDeletedBoardListener = (startAppListening: AppStartLis
matcher: isAnyOf(
// Updating a board may change its archived status
boardsApi.endpoints.updateBoard.matchFulfilled,
// If the selected/auto-add board was deleted from a different session, we'll only know during the list request,
boardsApi.endpoints.listAllBoards.matchFulfilled,
// If a board is deleted, we'll need to reset the auto-add board
imagesApi.endpoints.deleteBoard.matchFulfilled,
imagesApi.endpoints.deleteBoardAndImages.matchFulfilled,
Expand Down
1 change: 1 addition & 0 deletions invokeai/frontend/web/src/app/types/invokeai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export type AppConfig = {
*/
shouldUpdateImagesOnConnect: boolean;
shouldFetchMetadataFromApi: boolean;
allowPrivateBoards: boolean;
disabledTabs: InvokeTabName[];
disabledFeatures: AppFeature[];
disabledSDFeatures: SDFeature[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ const IAIDropOverlay = (props: Props) => {
bottom={0.5}
opacity={1}
borderWidth={2}
borderColor={isOver ? 'base.50' : 'base.300'}
borderRadius="lg"
borderColor={isOver ? 'base.300' : 'base.500'}
borderRadius="base"
borderStyle="dashed"
transitionProperty="common"
transitionDuration="0.1s"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Badge } from '@invoke-ai/ui-library';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

export const AutoAddBadge = memo(() => {
const { t } = useTranslation();
return (
<Badge color="invokeBlue.400" borderColor="invokeBlue.700" borderWidth={1} bg="transparent" flexShrink={0}>
{t('common.auto')}
</Badge>
);
});

AutoAddBadge.displayName = 'AutoAddBadge';

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,26 +1,48 @@
import { IconButton } from '@invoke-ai/ui-library';
import { memo, useCallback } from 'react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { boardIdSelected } from 'features/gallery/store/gallerySlice';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
import { useCreateBoardMutation } from 'services/api/endpoints/boards';

const AddBoardButton = () => {
type Props = {
isPrivateBoard: boolean;
};

const AddBoardButton = ({ isPrivateBoard }: Props) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
const [createBoard, { isLoading }] = useCreateBoardMutation();
const DEFAULT_BOARD_NAME = t('boards.myBoard');
const handleCreateBoard = useCallback(() => {
createBoard(DEFAULT_BOARD_NAME);
}, [createBoard, DEFAULT_BOARD_NAME]);
const label = useMemo(() => {
if (!allowPrivateBoards) {
return t('boards.addBoard');
}
if (isPrivateBoard) {
return t('boards.addPrivateBoard');
}
return t('boards.addSharedBoard');
}, [allowPrivateBoards, isPrivateBoard, t]);
const handleCreateBoard = useCallback(async () => {
try {
const board = await createBoard({ board_name: t('boards.myBoard'), is_private: isPrivateBoard }).unwrap();
dispatch(boardIdSelected({ boardId: board.board_id }));
} catch {
//no-op
}
}, [t, createBoard, isPrivateBoard, dispatch]);

return (
<IconButton
icon={<PiPlusBold />}
isLoading={isLoading}
tooltip={t('boards.addBoard')}
aria-label={t('boards.addBoard')}
tooltip={label}
aria-label={label}
onClick={handleCreateBoard}
size="sm"
size="md"
data-testid="add-board-button"
variant="ghost"
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Collapse, Flex, Grid, GridItem } from '@invoke-ai/ui-library';
import { Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
import GallerySettingsPopover from 'features/gallery/components/GallerySettingsPopover/GallerySettingsPopover';
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import type { CSSProperties } from 'react';
import { memo, useState } from 'react';
import { memo, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretUpBold } from 'react-icons/pi';
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
import type { BoardDTO } from 'services/api/types';

Expand All @@ -19,56 +23,112 @@ const overlayScrollbarsStyles: CSSProperties = {
width: '100%',
};

type Props = {
isOpen: boolean;
};

const BoardsList = (props: Props) => {
const { isOpen } = props;
const BoardsList = () => {
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
const { data: boards } = useListAllBoardsQuery(queryArgs);
const filteredBoards = boardSearchText
? boards?.filter((board) => board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()))
: boards;
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
const privateBoardsDisclosure = useDisclosure({ defaultIsOpen: false });
const sharedBoardsDisclosure = useDisclosure({ defaultIsOpen: false });
const { t } = useTranslation();

const { filteredPrivateBoards, filteredSharedBoards } = useMemo(() => {
const filteredBoards = boardSearchText
? boards?.filter((board) => board.board_name.toLowerCase().includes(boardSearchText.toLowerCase()))
: boards;
const filteredPrivateBoards = filteredBoards?.filter((board) => board.is_private) ?? EMPTY_ARRAY;
const filteredSharedBoards = filteredBoards?.filter((board) => !board.is_private) ?? EMPTY_ARRAY;
return { filteredPrivateBoards, filteredSharedBoards };
}, [boardSearchText, boards]);

return (
<>
<Collapse in={isOpen} animateOpacity>
<Flex layerStyle="first" flexDir="column" gap={2} p={2} mt={2} borderRadius="base">
<Flex gap={2} alignItems="center">
<BoardsSearch />
<AddBoardButton />
</Flex>
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Grid
className="list-container"
data-testid="boards-list"
gridTemplateColumns="repeat(auto-fill, minmax(90px, 1fr))"
maxH={346}
>
<GridItem p={1.5} data-testid="no-board">
<NoBoardBoard isSelected={selectedBoardId === 'none'} />
</GridItem>
{filteredBoards &&
filteredBoards.map((board, index) => (
<GridItem key={board.board_id} p={1.5} data-testid={`board-${index}`}>
<Flex layerStyle="first" flexDir="column" borderRadius="base">
<Flex gap={2} alignItems="center" pb={2}>
<BoardsSearch />
<GallerySettingsPopover />
</Flex>
{allowPrivateBoards && (
<>
<Flex w="full" gap={2}>
<Flex
flexGrow={1}
onClick={privateBoardsDisclosure.onToggle}
gap={2}
alignItems="center"
cursor="pointer"
>
<Icon
as={PiCaretUpBold}
boxSize={4}
transform={privateBoardsDisclosure.isOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
transitionProperty="common"
transitionDuration="normal"
color="base.400"
/>
<Text fontSize="md" fontWeight="medium" userSelect="none">
{t('boards.private')}
</Text>
</Flex>
<AddBoardButton isPrivateBoard={true} />
</Flex>
<Collapse in={privateBoardsDisclosure.isOpen} animateOpacity>
<OverlayScrollbarsComponent
defer
style={overlayScrollbarsStyles}
options={overlayScrollbarsParams.options}
>
<Flex direction="column" maxH={346} gap={1}>
{allowPrivateBoards && <NoBoardBoard isSelected={selectedBoardId === 'none'} />}
{filteredPrivateBoards.map((board) => (
<GalleryBoard
board={board}
isSelected={selectedBoardId === board.board_id}
setBoardToDelete={setBoardToDelete}
key={board.board_id}
/>
</GridItem>
))}
</Grid>
</OverlayScrollbarsComponent>
))}
</Flex>
</OverlayScrollbarsComponent>
</Collapse>
</>
)}
<Flex w="full" gap={2}>
<Flex onClick={sharedBoardsDisclosure.onToggle} gap={2} alignItems="center" cursor="pointer" flexGrow={1}>
<Icon
as={PiCaretUpBold}
boxSize={4}
transform={sharedBoardsDisclosure.isOpen ? 'rotate(0deg)' : 'rotate(180deg)'}
transitionProperty="common"
transitionDuration="normal"
color="base.400"
/>
<Text fontSize="md" fontWeight="medium" userSelect="none">
{allowPrivateBoards ? t('boards.shared') : t('boards.boards')}
</Text>
</Flex>
<AddBoardButton isPrivateBoard={false} />
</Flex>
</Collapse>
<Collapse in={sharedBoardsDisclosure.isOpen} animateOpacity>
<OverlayScrollbarsComponent defer style={overlayScrollbarsStyles} options={overlayScrollbarsParams.options}>
<Flex direction="column" maxH={346} gap={1}>
{!allowPrivateBoards && <NoBoardBoard isSelected={selectedBoardId === 'none'} />}
{filteredSharedBoards.map((board) => (
<GalleryBoard
board={board}
isSelected={selectedBoardId === board.board_id}
setBoardToDelete={setBoardToDelete}
key={board.board_id}
/>
))}
</Flex>
</OverlayScrollbarsComponent>
</Collapse>
</Flex>
<DeleteBoardModal boardToDelete={boardToDelete} setBoardToDelete={setBoardToDelete} />
</>
);
};

export default memo(BoardsList);
Loading
Loading