can delete and rename boards

This commit is contained in:
Mary Hipp 2023-06-15 13:31:24 -04:00 committed by psychedelicious
parent d306a84447
commit 8aac683319
7 changed files with 185 additions and 87 deletions

View File

@ -1,9 +1,19 @@
import { Flex, Icon, Text } from '@chakra-ui/react'; import { Flex, Icon, Text } from '@chakra-ui/react';
import { useCallback } from 'react';
import { FaPlus } from 'react-icons/fa'; import { FaPlus } from 'react-icons/fa';
import { useAppDispatch } from '../../../../app/store/storeHooks';
import { boardCreated } from '../../../../services/thunks/board';
const AddBoardButton = () => { const AddBoardButton = () => {
const dispatch = useAppDispatch();
const handleCreateBoard = useCallback(() => {
dispatch(boardCreated({ requestBody: 'My Board' }));
}, [dispatch]);
return ( return (
<Flex <Flex
onClick={handleCreateBoard}
sx={{ sx={{
flexDir: 'column', flexDir: 'column',
justifyContent: 'space-between', justifyContent: 'space-between',

View File

@ -0,0 +1,45 @@
import { Flex, Icon, Text } from '@chakra-ui/react';
import { FaImages } from 'react-icons/fa';
import { boardIdSelected } from '../../store/boardSlice';
import { useDispatch } from 'react-redux';
const AllImagesBoard = ({ isSelected }: { isSelected: boolean }) => {
const dispatch = useDispatch();
const handleAllImagesBoardClick = () => {
dispatch(boardIdSelected(null));
};
return (
<Flex
sx={{
flexDir: 'column',
justifyContent: 'space-between',
alignItems: 'center',
cursor: 'pointer',
w: 'full',
h: 'full',
gap: 1,
}}
onClick={handleAllImagesBoardClick}
>
<Flex
sx={{
justifyContent: 'center',
alignItems: 'center',
borderWidth: '1px',
borderRadius: 'base',
borderColor: isSelected ? 'base.500' : 'base.800',
w: 'full',
h: 'full',
aspectRatio: '1/1',
}}
>
<Icon boxSize={8} color="base.700" as={FaImages} />
</Flex>
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>All Images</Text>
</Flex>
);
};
export default AllImagesBoard;

View File

@ -2,22 +2,26 @@ import { Grid } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { selectBoardsAll } from 'features/gallery/store/boardSlice'; import {
import { memo } from 'react'; boardsSelector,
selectBoardsAll,
} from 'features/gallery/store/boardSlice';
import { memo, useState } from 'react';
import HoverableBoard from './HoverableBoard'; import HoverableBoard from './HoverableBoard';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import AddBoardButton from './AddBoardButton'; import AddBoardButton from './AddBoardButton';
import AllImagesBoard from './AllImagesBoard';
const selector = createSelector( const selector = createSelector(
selectBoardsAll, [selectBoardsAll, boardsSelector],
(boards) => { (boards, boardsState) => {
return { boards }; return { boards, selectedBoardId: boardsState.selectedBoardId };
}, },
defaultSelectorOptions defaultSelectorOptions
); );
const BoardsList = () => { const BoardsList = () => {
const { boards } = useAppSelector(selector); const { boards, selectedBoardId } = useAppSelector(selector);
return ( return (
<OverlayScrollbarsComponent <OverlayScrollbarsComponent
@ -42,8 +46,13 @@ const BoardsList = () => {
}} }}
> >
<AddBoardButton /> <AddBoardButton />
<AllImagesBoard isSelected={selectedBoardId === null} />
{boards.map((board) => ( {boards.map((board) => (
<HoverableBoard key={board.board_id} board={board} /> <HoverableBoard
key={board.board_id}
board={board}
isSelected={selectedBoardId === board.board_id}
/>
))} ))}
</Grid> </Grid>
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>

View File

@ -1,49 +1,51 @@
import { import {
Box, Box,
Editable,
EditableInput,
EditablePreview,
Flex, Flex,
Icon, Icon,
Image, Image,
MenuItem, MenuItem,
MenuList, MenuList,
Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch } from 'app/store/storeHooks';
import { PropsWithChildren, memo, useCallback, useState } from 'react'; import { memo, useCallback } from 'react';
import { FaFolder, FaImage } from 'react-icons/fa'; import { FaFolder, FaTrash } from 'react-icons/fa';
import { ContextMenu } from 'chakra-ui-contextmenu'; import { ContextMenu } from 'chakra-ui-contextmenu';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { useAppToaster } from 'app/components/Toaster';
import { BoardDTO } from 'services/api'; import { BoardDTO } from 'services/api';
import { EntityId, createSelector } from '@reduxjs/toolkit';
import {
selectFilteredImagesIds,
selectImagesById,
} from '../../store/imagesSlice';
import { RootState } from '../../../../app/store/store';
import { defaultSelectorOptions } from '../../../../app/store/util/defaultMemoizeOptions';
import { useSelector } from 'react-redux';
import { IAIImageFallback } from 'common/components/IAIImageFallback'; import { IAIImageFallback } from 'common/components/IAIImageFallback';
import { boardIdSelected } from 'features/gallery/store/boardSlice'; import { boardIdSelected } from 'features/gallery/store/boardSlice';
import { boardDeleted, boardUpdated } from '../../../../services/thunks/board';
interface HoverableBoardProps { interface HoverableBoardProps {
board: BoardDTO; board: BoardDTO;
isSelected: boolean;
} }
/** const HoverableBoard = memo(({ board, isSelected }: HoverableBoardProps) => {
* Gallery image component with delete/use all/use seed buttons on hover.
*/
const HoverableBoard = memo(({ board }: HoverableBoardProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { board_name, board_id, cover_image_url } = board; const { board_name, board_id, cover_image_url } = board;
const { t } = useTranslation();
const handleSelectBoard = useCallback(() => { const handleSelectBoard = useCallback(() => {
dispatch(boardIdSelected(board_id)); dispatch(boardIdSelected(board_id));
}, [board_id, dispatch]); }, [board_id, dispatch]);
const handleDeleteBoard = useCallback(() => {
dispatch(boardDeleted(board_id));
}, [board_id, dispatch]);
const handleUpdateBoardName = (newBoardName: string) => {
dispatch(
boardUpdated({
boardId: board_id,
requestBody: { board_name: newBoardName },
})
);
};
return ( return (
<Box sx={{ touchAction: 'none' }}> <Box sx={{ touchAction: 'none' }}>
<ContextMenu<HTMLDivElement> <ContextMenu<HTMLDivElement>
@ -51,10 +53,11 @@ const HoverableBoard = memo(({ board }: HoverableBoardProps) => {
renderMenu={() => ( renderMenu={() => (
<MenuList sx={{ visibility: 'visible !important' }}> <MenuList sx={{ visibility: 'visible !important' }}>
<MenuItem <MenuItem
icon={<ExternalLinkIcon />} sx={{ color: 'error.300' }}
// onClickCapture={handleOpenInNewTab} icon={<FaTrash />}
onClickCapture={handleDeleteBoard}
> >
Sample Menu Item Delete Board
</MenuItem> </MenuItem>
</MenuList> </MenuList>
)} )}
@ -64,7 +67,6 @@ const HoverableBoard = memo(({ board }: HoverableBoardProps) => {
position="relative" position="relative"
key={board_id} key={board_id}
userSelect="none" userSelect="none"
onClick={handleSelectBoard}
ref={ref} ref={ref}
sx={{ sx={{
flexDir: 'column', flexDir: 'column',
@ -77,12 +79,13 @@ const HoverableBoard = memo(({ board }: HoverableBoardProps) => {
}} }}
> >
<Flex <Flex
onClick={handleSelectBoard}
sx={{ sx={{
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderWidth: '1px', borderWidth: '1px',
borderRadius: 'base', borderRadius: 'base',
borderColor: 'base.800', borderColor: isSelected ? 'base.500' : 'base.800',
w: 'full', w: 'full',
h: 'full', h: 'full',
aspectRatio: '1/1', aspectRatio: '1/1',
@ -102,7 +105,26 @@ const HoverableBoard = memo(({ board }: HoverableBoardProps) => {
<Icon boxSize={8} color="base.700" as={FaFolder} /> <Icon boxSize={8} color="base.700" as={FaFolder} />
)} )}
</Flex> </Flex>
<Text sx={{ color: 'base.200', fontSize: 'xs' }}>{board_name}</Text>
<Editable
defaultValue={board_name}
submitOnBlur={false}
onSubmit={(nextValue) => {
handleUpdateBoardName(nextValue);
}}
>
<EditablePreview
sx={{ color: 'base.200', fontSize: 'xs', textAlign: 'left' }}
/>
<EditableInput
sx={{
color: 'base.200',
fontSize: 'xs',
textAlign: 'left',
borderColor: 'base.500',
}}
/>
</Editable>
</Flex> </Flex>
)} )}
</ContextMenu> </ContextMenu>

View File

@ -37,7 +37,7 @@ import {
} from 'react'; } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs'; import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
import { FaFolder, FaImage, FaPlus, FaServer, FaWrench } from 'react-icons/fa'; import { FaImage, FaServer, FaWrench } from 'react-icons/fa';
import { MdPhotoLibrary } from 'react-icons/md'; import { MdPhotoLibrary } from 'react-icons/md';
import HoverableImage from './HoverableImage'; import HoverableImage from './HoverableImage';
@ -55,10 +55,6 @@ import {
} from '../store/imagesSlice'; } from '../store/imagesSlice';
import { receivedPageOfImages } from 'services/thunks/image'; import { receivedPageOfImages } from 'services/thunks/image';
import { boardSelector } from '../store/boardSelectors'; import { boardSelector } from '../store/boardSelectors';
import { BoardDTO, ImageDTO } from '../../../services/api';
import { isBoardDTO, isImageDTO } from '../../../services/types/guards';
import HoverableBoard from './Boards/HoverableBoard';
import IAIInput from '../../../common/components/IAIInput';
import { boardCreated } from '../../../services/thunks/board'; import { boardCreated } from '../../../services/thunks/board';
import BoardsList from './Boards/BoardsList'; import BoardsList from './Boards/BoardsList';
import { selectBoardsById } from '../store/boardSlice'; import { selectBoardsById } from '../store/boardSlice';
@ -66,18 +62,16 @@ import { selectBoardsById } from '../store/boardSlice';
const itemSelector = createSelector( const itemSelector = createSelector(
[(state: RootState) => state], [(state: RootState) => state],
(state) => { (state) => {
const { images, boards, gallery } = state; const { images, boards } = state;
let items: Array<ImageDTO | BoardDTO> = [];
let areMoreAvailable = false;
let isLoading = true;
const { categories } = images; const { categories } = images;
const allImages = selectImagesAll(state); const allImages = selectImagesAll(state);
items = allImages.filter((i) => categories.includes(i.image_category)); const items = allImages.filter((i) =>
areMoreAvailable = items.length < images.total; categories.includes(i.image_category)
isLoading = images.isLoading; );
const areMoreAvailable = items.length < images.total;
const isLoading = images.isLoading;
const selectedBoard = boards.selectedBoardId const selectedBoard = boards.selectedBoardId
? selectBoardsById(state, boards.selectedBoardId) ? selectBoardsById(state, boards.selectedBoardId)
@ -353,27 +347,17 @@ const ImageGalleryContent = () => {
data={items} data={items}
endReached={handleEndReached} endReached={handleEndReached}
scrollerRef={(ref) => setScrollerRef(ref)} scrollerRef={(ref) => setScrollerRef(ref)}
itemContent={(index, item) => { itemContent={(index, item) => (
if (isImageDTO(item)) { <Flex sx={{ pb: 2 }}>
return ( <HoverableImage
<Flex sx={{ pb: 2 }}> key={`${item.image_name}-${item.thumbnail_url}`}
<HoverableImage image={item}
key={`${item.image_name}-${item.thumbnail_url}`} isSelected={
image={item} selectedImage?.image_name === item?.image_name
isSelected={ }
selectedImage?.image_name === item?.image_name />
} </Flex>
/> )}
</Flex>
);
} else if (isBoardDTO(item)) {
return (
<Flex sx={{ pb: 2 }}>
<HoverableBoard key={item.board_id} board={item} />
</Flex>
);
}
}}
/> />
) : ( ) : (
<VirtuosoGrid <VirtuosoGrid
@ -385,23 +369,15 @@ const ImageGalleryContent = () => {
List: ListContainer, List: ListContainer,
}} }}
scrollerRef={setScroller} scrollerRef={setScroller}
itemContent={(index, item) => { itemContent={(index, item) => (
if (isImageDTO(item)) { <HoverableImage
return ( key={`${item.image_name}-${item.thumbnail_url}`}
<HoverableImage image={item}
key={`${item.image_name}-${item.thumbnail_url}`} isSelected={
image={item} selectedImage?.image_name === item?.image_name
isSelected={ }
selectedImage?.image_name === item?.image_name />
} )}
/>
);
} else if (isBoardDTO(item)) {
return (
<HoverableBoard key={item.board_id} board={item} />
);
}
}}
/> />
)} )}
</Box> </Box>

View File

@ -8,7 +8,12 @@ import {
import { RootState } from 'app/store/store'; import { RootState } from 'app/store/store';
import { BoardDTO } from 'services/api'; import { BoardDTO } from 'services/api';
import { dateComparator } from 'common/util/dateComparator'; import { dateComparator } from 'common/util/dateComparator';
import { receivedBoards } from '../../../services/thunks/board'; import {
boardCreated,
boardDeleted,
boardUpdated,
receivedBoards,
} from '../../../services/thunks/board';
export const boardsAdapter = createEntityAdapter<BoardDTO>({ export const boardsAdapter = createEntityAdapter<BoardDTO>({
selectId: (board) => board.board_id, selectId: (board) => board.board_id,
@ -26,7 +31,7 @@ type AdditionalBoardsState = {
export const initialBoardsState = export const initialBoardsState =
boardsAdapter.getInitialState<AdditionalBoardsState>({ boardsAdapter.getInitialState<AdditionalBoardsState>({
offset: 0, offset: 0,
limit: 0, limit: 50,
total: 0, total: 0,
isLoading: false, isLoading: false,
selectedBoardId: null, selectedBoardId: null,
@ -47,7 +52,7 @@ const boardsSlice = createSlice({
boardRemoved: (state, action: PayloadAction<string>) => { boardRemoved: (state, action: PayloadAction<string>) => {
boardsAdapter.removeOne(state, action.payload); boardsAdapter.removeOne(state, action.payload);
}, },
boardIdSelected: (state, action: PayloadAction<string>) => { boardIdSelected: (state, action: PayloadAction<string | null>) => {
state.selectedBoardId = action.payload; state.selectedBoardId = action.payload;
}, },
}, },
@ -66,6 +71,19 @@ const boardsSlice = createSlice({
state.total = total; state.total = total;
boardsAdapter.upsertMany(state, items); boardsAdapter.upsertMany(state, items);
}); });
builder.addCase(boardCreated.fulfilled, (state, action) => {
const board = action.payload;
boardsAdapter.upsertOne(state, board);
});
builder.addCase(boardUpdated.fulfilled, (state, action) => {
const board = action.payload;
boardsAdapter.upsertOne(state, board);
});
builder.addCase(boardDeleted.pending, (state, action) => {
const boardId = action.meta.arg;
console.log({ boardId });
boardsAdapter.removeOne(state, boardId);
});
}, },
}); });

View File

@ -21,3 +21,21 @@ export const boardCreated = createAppAsyncThunk(
return response; return response;
} }
); );
export const boardDeleted = createAppAsyncThunk(
'api/boardDeleted',
async (boardId: string) => {
await BoardsService.deleteBoard({ boardId });
return boardId;
}
);
type BoardUpdatedArg = Parameters<(typeof BoardsService)['updateBoard']>[0];
export const boardUpdated = createAppAsyncThunk(
'api/boardUpdated',
async (arg: BoardUpdatedArg) => {
const response = await BoardsService.updateBoard(arg);
return response;
}
);