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 { useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
import { useAppDispatch } from '../../../../app/store/storeHooks';
import { boardCreated } from '../../../../services/thunks/board';
const AddBoardButton = () => {
const dispatch = useAppDispatch();
const handleCreateBoard = useCallback(() => {
dispatch(boardCreated({ requestBody: 'My Board' }));
}, [dispatch]);
return (
<Flex
onClick={handleCreateBoard}
sx={{
flexDir: 'column',
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 { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { selectBoardsAll } from 'features/gallery/store/boardSlice';
import { memo } from 'react';
import {
boardsSelector,
selectBoardsAll,
} from 'features/gallery/store/boardSlice';
import { memo, useState } from 'react';
import HoverableBoard from './HoverableBoard';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import AddBoardButton from './AddBoardButton';
import AllImagesBoard from './AllImagesBoard';
const selector = createSelector(
selectBoardsAll,
(boards) => {
return { boards };
[selectBoardsAll, boardsSelector],
(boards, boardsState) => {
return { boards, selectedBoardId: boardsState.selectedBoardId };
},
defaultSelectorOptions
);
const BoardsList = () => {
const { boards } = useAppSelector(selector);
const { boards, selectedBoardId } = useAppSelector(selector);
return (
<OverlayScrollbarsComponent
@ -42,8 +46,13 @@ const BoardsList = () => {
}}
>
<AddBoardButton />
<AllImagesBoard isSelected={selectedBoardId === null} />
{boards.map((board) => (
<HoverableBoard key={board.board_id} board={board} />
<HoverableBoard
key={board.board_id}
board={board}
isSelected={selectedBoardId === board.board_id}
/>
))}
</Grid>
</OverlayScrollbarsComponent>

View File

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

View File

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

View File

@ -8,7 +8,12 @@ import {
import { RootState } from 'app/store/store';
import { BoardDTO } from 'services/api';
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>({
selectId: (board) => board.board_id,
@ -26,7 +31,7 @@ type AdditionalBoardsState = {
export const initialBoardsState =
boardsAdapter.getInitialState<AdditionalBoardsState>({
offset: 0,
limit: 0,
limit: 50,
total: 0,
isLoading: false,
selectedBoardId: null,
@ -47,7 +52,7 @@ const boardsSlice = createSlice({
boardRemoved: (state, action: PayloadAction<string>) => {
boardsAdapter.removeOne(state, action.payload);
},
boardIdSelected: (state, action: PayloadAction<string>) => {
boardIdSelected: (state, action: PayloadAction<string | null>) => {
state.selectedBoardId = action.payload;
},
},
@ -66,6 +71,19 @@ const boardsSlice = createSlice({
state.total = total;
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;
}
);
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;
}
);