From f925f9fbf44fcaca230b1c1bf527e4bae0287cf7 Mon Sep 17 00:00:00 2001 From: kim Date: Wed, 17 Jul 2024 11:15:54 +0200 Subject: [PATCH 1/8] feat: add multi op for home and recycle bin --- package.json | 4 +- src/components/common/DeleteButton.tsx | 16 +- src/components/common/RestoreButton.tsx | 3 + src/components/file/SmallUploadFile.tsx | 18 ++ src/components/item/FolderContent.tsx | 71 ++++--- .../item/FolderSelectionToolbar.tsx | 63 ++++++ src/components/main/DeleteItemDialog.tsx | 19 +- src/components/main/ItemMenuContent.tsx | 87 ++++---- src/components/main/TableHead.tsx | 89 -------- .../itemSelectionModal/RootNavigationTree.tsx | 14 +- .../main/{ => list}/ItemForbiddenScreen.tsx | 8 +- src/components/main/{ => list}/ItemsTable.tsx | 20 +- .../main/{ => list}/ItemsTableCard.tsx | 34 ++-- src/components/main/list/SelectionContext.tsx | 130 ++++++++++++ src/components/main/list/SelectionToolbar.tsx | 34 ++++ .../pages/BookmarkedItemsScreen.tsx | 2 +- src/components/pages/PublishedItemsScreen.tsx | 2 +- src/components/pages/RecycledItemsScreen.tsx | 71 ++++--- src/components/pages/home/HomeScreen.tsx | 63 +++--- .../pages/home/HomeSelectionToolbar.tsx | 63 ++++++ .../pages/item/ItemLoginWrapper.tsx | 2 +- .../recycleBin/RecycleBinSelectionToolbar.tsx | 28 +++ src/components/table/ItemCard.tsx | 51 +++-- src/config/selectors.ts | 1 + yarn.lock | 190 +++++------------- 25 files changed, 668 insertions(+), 415 deletions(-) create mode 100644 src/components/file/SmallUploadFile.tsx create mode 100644 src/components/item/FolderSelectionToolbar.tsx delete mode 100644 src/components/main/TableHead.tsx rename src/components/main/{ => list}/ItemForbiddenScreen.tsx (79%) rename src/components/main/{ => list}/ItemsTable.tsx (89%) rename src/components/main/{ => list}/ItemsTableCard.tsx (68%) create mode 100644 src/components/main/list/SelectionContext.tsx create mode 100644 src/components/main/list/SelectionToolbar.tsx create mode 100644 src/components/pages/home/HomeSelectionToolbar.tsx create mode 100644 src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx diff --git a/package.json b/package.json index 774eb9b1d..1a585edea 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@ag-grid-community/core": "31.3.4", "@ag-grid-community/styles": "31.3.4", + "@air/react-drag-to-select": "5.0.8", "@emotion/cache": "11.13.0", "@emotion/react": "11.13.0", "@emotion/styled": "11.13.0", @@ -24,9 +25,8 @@ "@graasp/query-client": "3.16.0", "@graasp/sdk": "4.19.0", "@graasp/translations": "1.32.0", - "@graasp/ui": "4.21.0", + "@graasp/ui": "github:graasp/graasp-ui#card-selection", "@mui/icons-material": "5.16.4", - "@mui/lab": "5.0.0-alpha.172", "@mui/material": "5.16.4", "@sentry/react": "7.118.0", "axios": "1.7.2", diff --git a/src/components/common/DeleteButton.tsx b/src/components/common/DeleteButton.tsx index 5411ff769..52f2b25d7 100644 --- a/src/components/common/DeleteButton.tsx +++ b/src/components/common/DeleteButton.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; +import { PackedItem } from '@graasp/sdk'; import { ActionButtonVariant, ColorVariants, @@ -12,11 +13,12 @@ import { BUILDER } from '../../langs/constants'; import DeleteItemDialog from '../main/DeleteItemDialog'; type Props = { - itemIds: string[]; + items: PackedItem[]; color?: ColorVariants; id?: string; type?: ActionButtonVariant; - onClick?: () => void; + onConfirm?: () => void; + onClose?: () => void; }; /** @@ -24,11 +26,12 @@ type Props = { * This button opens a dialog to confirm the action */ const DeleteButton = ({ - itemIds, + items, color, id, type, - onClick, + onConfirm, + onClose, }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); @@ -36,11 +39,11 @@ const DeleteButton = ({ const handleClickOpen = () => { setOpen(true); - onClick?.(); }; const handleClose = () => { setOpen(false); + onClose?.(); }; const text = translateBuilder(BUILDER.DELETE_BUTTON); @@ -58,9 +61,10 @@ const DeleteButton = ({ className={ITEM_DELETE_BUTTON_CLASS} /> ); diff --git a/src/components/common/RestoreButton.tsx b/src/components/common/RestoreButton.tsx index 7458bfd8c..9bfd99d67 100644 --- a/src/components/common/RestoreButton.tsx +++ b/src/components/common/RestoreButton.tsx @@ -10,12 +10,14 @@ type Props = { itemIds: string[]; color?: IconButtonProps['color']; id?: string; + onClick?: () => void; }; const RestoreButton = ({ itemIds, color = 'default', id, + onClick: onClickFn, }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { mutate: restoreItems } = mutations.useRestoreItems(); @@ -23,6 +25,7 @@ const RestoreButton = ({ const onClick = () => { // restore items restoreItems(itemIds); + onClickFn?.(); }; const title = translateBuilder(BUILDER.RESTORE_ITEM_BUTTON); diff --git a/src/components/file/SmallUploadFile.tsx b/src/components/file/SmallUploadFile.tsx new file mode 100644 index 000000000..cba6b7e0d --- /dev/null +++ b/src/components/file/SmallUploadFile.tsx @@ -0,0 +1,18 @@ +import { Stack } from '@mui/material'; + +import { Upload } from 'lucide-react'; + +const SmallUploadFile = ({ text }: { text: string }): JSX.Element => ( + + {text} + +); + +export default SmallUploadFile; diff --git a/src/components/item/FolderContent.tsx b/src/components/item/FolderContent.tsx index d0c2d9d91..5ad52b856 100644 --- a/src/components/item/FolderContent.tsx +++ b/src/components/item/FolderContent.tsx @@ -21,14 +21,19 @@ import SelectTypes from '../common/SelectTypes'; import { useFilterItemsContext } from '../context/FilterItemsContext'; import { useLayoutContext } from '../context/LayoutContext'; import FileUploader from '../file/FileUploader'; -import ItemsTable from '../main/ItemsTable'; import NewItemButton from '../main/NewItemButton'; +import ItemsTable from '../main/list/ItemsTable'; +import { + SelectionContextProvider, + useSelectionContext, +} from '../main/list/SelectionContext'; import { DesktopMap } from '../map/DesktopMap'; import NoItemFilters from '../pages/NoItemFilters'; import SortingSelect from '../table/SortingSelect'; import { SortingOptionsForFolder } from '../table/types'; import { useSorting } from '../table/useSorting'; import FolderDescription from './FolderDescription'; +import FolderToolbar from './FolderSelectionToolbar'; import { useItemSearch } from './ItemSearch'; import ModeButton from './header/ModeButton'; @@ -42,6 +47,7 @@ type Props = { const Content = ({ item, searchText, items, sortBy }: Props) => { const { mode } = useLayoutContext(); const { itemTypes } = useFilterItemsContext(); + const { selectedIds, toggleSelection } = useSelectionContext(); const enableEditing = item.permission ? PermissionLevelCompare.lte(PermissionLevel.Write, item.permission) @@ -62,9 +68,11 @@ const Content = ({ item, searchText, items, sortBy }: Props) => { return ( <> {Boolean(enableEditing && !searchText && !itemTypes?.length) && ( @@ -107,6 +115,7 @@ const FolderContent = ({ item }: { item: PackedItem }): JSX.Element => { const { t: translateEnums } = useEnumsTranslation(); const { shouldDisplayItem } = useFilterItemsContext(); const { t: translateBuilder } = useBuilderTranslation(); + const { selectedIds } = useSelectionContext(); const { data: children, @@ -168,29 +177,33 @@ const FolderContent = ({ item }: { item: PackedItem }): JSX.Element => { gap={1} width="100%" > - - - - {sortBy && setSortBy && ( - - translateEnums(t1).localeCompare(translateEnums(t2)), - )} - setOrdering={setOrdering} - /> - )} - + {selectedIds.length && folderChildren?.length ? ( + + ) : ( + + + + {sortBy && setSortBy && ( + + translateEnums(t1).localeCompare(translateEnums(t2)), + )} + setOrdering={setOrdering} + /> + )} + + - + )} { ); }; -export default FolderContent; +export const FolderContentWrapper = ({ + item, +}: { + item: PackedItem; +}): JSX.Element => ( + + + +); + +export default FolderContentWrapper; diff --git a/src/components/item/FolderSelectionToolbar.tsx b/src/components/item/FolderSelectionToolbar.tsx new file mode 100644 index 000000000..a3bae2ae0 --- /dev/null +++ b/src/components/item/FolderSelectionToolbar.tsx @@ -0,0 +1,63 @@ +import { PackedItem } from '@graasp/sdk'; + +import RecycleButton from '@/components/common/RecycleButton'; +import useModalStatus from '@/components/hooks/useModalStatus'; +import CopyButton from '@/components/item/copy/CopyButton'; +import { CopyModal } from '@/components/item/copy/CopyModal'; +import MoveButton from '@/components/item/move/MoveButton'; +import { MoveModal } from '@/components/item/move/MoveModal'; +import { useSelectionContext } from '@/components/main/list/SelectionContext'; +import SelectionToolbar from '@/components/main/list/SelectionToolbar'; + +const FolderSelectionToolbar = ({ + items, +}: { + items: PackedItem[]; +}): JSX.Element => { + const { selectedIds, clearSelection } = useSelectionContext(); + + const { + isOpen: isCopyModalOpen, + openModal: openCopyModal, + closeModal: closeCopyModal, + } = useModalStatus(); + const { + isOpen: isMoveModalOpen, + openModal: openMoveModal, + closeModal: closeMoveModal, + } = useModalStatus(); + + return ( + <> + { + closeCopyModal(); + clearSelection(); + }} + open={isCopyModalOpen} + itemIds={selectedIds} + /> + { + closeMoveModal(); + clearSelection(); + }} + open={isMoveModalOpen} + items={items?.filter(({ id }) => selectedIds.includes(id))} + /> + + <> + + + + + + + ); +}; + +export default FolderSelectionToolbar; diff --git a/src/components/main/DeleteItemDialog.tsx b/src/components/main/DeleteItemDialog.tsx index f7b859806..c9fccfdb2 100644 --- a/src/components/main/DeleteItemDialog.tsx +++ b/src/components/main/DeleteItemDialog.tsx @@ -6,6 +6,7 @@ import { DialogTitle, } from '@mui/material'; +import { PackedItem } from '@graasp/sdk'; import { Button } from '@graasp/ui'; import { useBuilderTranslation } from '../../config/i18n'; @@ -20,20 +21,25 @@ const descriptionId = 'alert-dialog-description'; type Props = { open?: boolean; handleClose: () => void; - itemIds: string[]; + items: PackedItem[]; + onConfirm?: () => void; }; const DeleteItemDialog = ({ - itemIds, + items, open = false, handleClose, + onConfirm, }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { mutate: deleteItems } = mutations.useDeleteItems(); + const itemIds = items.map(({ id }) => id); + const onDelete = () => { deleteItems(itemIds); + onConfirm?.(); handleClose(); }; @@ -52,6 +58,15 @@ const DeleteItemDialog = ({ {translateBuilder(BUILDER.DELETE_ITEM_MODAL_CONTENT, { count: itemIds.length, })} +
    + {items + .sort((a, b) => + a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1, + ) + .map(({ name }) => ( +
  • {name}
  • + ))} +
diff --git a/src/components/main/ItemMenuContent.tsx b/src/components/main/ItemMenuContent.tsx index 0e25beb81..0a3592151 100644 --- a/src/components/main/ItemMenuContent.tsx +++ b/src/components/main/ItemMenuContent.tsx @@ -117,19 +117,17 @@ const ItemMenuContent = ({ item }: Props): JSX.Element => { type={ActionButton.MENU_ITEM} /> )} - {member?.id && ( - <> - { - openCopyModal(); - closeMenu(); - }} - /> - - - )} + {member?.id && [ + { + openCopyModal(); + closeMenu(); + }} + />, + , + ]} {canAdmin && ( { )} - {canWrite && ( - <> - - - {item.type !== ItemType.FOLDER && ( - - )} - + {canWrite && [ + , + , + ]} + + {canWrite && item.type !== ItemType.FOLDER && ( + )} - {member?.id && ( - <> - { - openCreateShortcutModal(); - closeMenu(); - }} - /> - - - )} + {member?.id && [ + { + openCreateShortcutModal(); + closeMenu(); + }} + />, + , + ]} {canWrite && ( { )} {canAdmin ? ( - <> - + [ + , - + />, + ] ) : ( )} diff --git a/src/components/main/TableHead.tsx b/src/components/main/TableHead.tsx deleted file mode 100644 index 77aece4cc..000000000 --- a/src/components/main/TableHead.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { MouseEvent } from 'react'; - -import { - Checkbox, - TableCell, - TableCellProps, - TableHead, - TableRow, - TableSortLabel, - TableSortLabelProps, -} from '@mui/material'; - -import { useBuilderTranslation } from '../../config/i18n'; -import { Ordering } from '../../enums'; -import { BUILDER } from '../../langs/constants'; - -type Props = { - classes: { - visuallyHidden: string; - }; - numSelected: number; - onRequestSort: (event: MouseEvent, property: string) => void; - onSelectAllClick: () => void; - order: 'asc' | 'desc'; - orderBy: string; - rowCount: number; - headCells: (TableCellProps & { id: string; label: string })[]; -}; - -const CustomTableHead = (props: Props): JSX.Element => { - const { - classes, - onSelectAllClick, - order, - orderBy, - numSelected, - rowCount, - onRequestSort, - headCells, - } = props; - const { t: translateBuilder } = useBuilderTranslation(); - const createSortHandler = - (property: string): TableSortLabelProps['onClick'] => - (event) => { - onRequestSort(event, property); - }; - - return ( - - - - 0 && numSelected < rowCount} - checked={rowCount > 0 && numSelected === rowCount} - onChange={onSelectAllClick} - inputProps={{ - 'aria-label': translateBuilder(BUILDER.TABLE_SELECT_ALL_LABEL), - }} - color="primary" - /> - - {headCells.map((headCell) => ( - - - {headCell.label} - {orderBy === headCell.id ? ( - - {order === Ordering.DESC - ? translateBuilder(BUILDER.TABLE_DESC_SORT_LABEL) - : translateBuilder(BUILDER.TABLE_ASC_SORT_LABEL)} - - ) : null} - - - ))} - - - ); -}; - -export default CustomTableHead; diff --git a/src/components/main/itemSelectionModal/RootNavigationTree.tsx b/src/components/main/itemSelectionModal/RootNavigationTree.tsx index a7db12bad..fc9f11a29 100644 --- a/src/components/main/itemSelectionModal/RootNavigationTree.tsx +++ b/src/components/main/itemSelectionModal/RootNavigationTree.tsx @@ -32,11 +32,15 @@ const RootNavigationTree = ({ onNavigate, rootMenuItems, selectedId, -}: RootNavigationTreeProps): JSX.Element => { +}: RootNavigationTreeProps): JSX.Element | null => { const { t: translateBuilder } = useBuilderTranslation(); - // todo: to change with real recent items (most used) - const { data: recentItems, isLoading } = hooks.useAccessibleItems( + // TODO: to change with real recent items (most used) + const { + data: recentItems, + isLoading, + isSuccess, + } = hooks.useAccessibleItems( // you can move into an item you have at least write permission { permissions: [PermissionLevel.Admin, PermissionLevel.Write], @@ -51,7 +55,7 @@ const RootNavigationTree = ({ enabled: Boolean(items[0]), }); - if (recentItems?.data?.length) { + if (isSuccess) { return ( <> @@ -65,7 +69,7 @@ const RootNavigationTree = ({ buildRowMenuId={buildNavigationModalItemId} buildRowMenuArrowId={buildItemRowArrowId} /> - {recentItems && ( + {Boolean(recentItems.data.length) && ( <> {translateBuilder(BUILDER.ITEM_SELECTION_NAVIGATION_RECENT_ITEMS)} diff --git a/src/components/main/ItemForbiddenScreen.tsx b/src/components/main/list/ItemForbiddenScreen.tsx similarity index 79% rename from src/components/main/ItemForbiddenScreen.tsx rename to src/components/main/list/ItemForbiddenScreen.tsx index d40170ec0..725289403 100644 --- a/src/components/main/ItemForbiddenScreen.tsx +++ b/src/components/main/list/ItemForbiddenScreen.tsx @@ -5,10 +5,10 @@ import { Button, ForbiddenContent } from '@graasp/ui'; import { hooks } from '@/config/queryClient'; -import { useBuilderTranslation } from '../../config/i18n'; -import { ITEM_LOGIN_SCREEN_FORBIDDEN_ID } from '../../config/selectors'; -import { BUILDER } from '../../langs/constants'; -import UserSwitchWrapper from '../common/UserSwitchWrapper'; +import { useBuilderTranslation } from '../../../config/i18n'; +import { ITEM_LOGIN_SCREEN_FORBIDDEN_ID } from '../../../config/selectors'; +import { BUILDER } from '../../../langs/constants'; +import UserSwitchWrapper from '../../common/UserSwitchWrapper'; const ItemForbiddenScreen = (): JSX.Element => { const { data: member } = hooks.useCurrentMember(); diff --git a/src/components/main/ItemsTable.tsx b/src/components/main/list/ItemsTable.tsx similarity index 89% rename from src/components/main/ItemsTable.tsx rename to src/components/main/list/ItemsTable.tsx index 5eb2e270e..ed3e479d2 100644 --- a/src/components/main/ItemsTable.tsx +++ b/src/components/main/list/ItemsTable.tsx @@ -7,7 +7,7 @@ import { DialogActions, DialogContent, Skeleton } from '@mui/material'; import Dialog from '@mui/material/Dialog'; import DialogTitle from '@mui/material/DialogTitle'; -import { ItemType, PackedItem } from '@graasp/sdk'; +import { DiscriminatedItem, ItemType, PackedItem } from '@graasp/sdk'; import { COMMON } from '@graasp/translations'; import { Button, DraggingWrapper } from '@graasp/ui'; @@ -18,9 +18,9 @@ import { } from '@/config/i18n'; import { BUILDER } from '@/langs/constants'; -import { hooks, mutations } from '../../config/queryClient'; -import { useUploadWithProgress } from '../hooks/uploadWithProgress'; -import { useItemsStatuses } from '../table/Badges'; +import { hooks, mutations } from '../../../config/queryClient'; +import { useUploadWithProgress } from '../../hooks/uploadWithProgress'; +import { useItemsStatuses } from '../../table/Badges'; import ItemsTableCard from './ItemsTableCard'; const { useItem } = hooks; @@ -31,6 +31,8 @@ export type ItemsTableProps = { showThumbnails?: boolean; canMove?: boolean; enableMoveInBetween?: boolean; + onCardClick?: (id: DiscriminatedItem['id']) => void; + selectedIds?: string[]; }; const ItemsTable = ({ @@ -39,6 +41,8 @@ const ItemsTable = ({ showThumbnails = true, canMove = true, enableMoveInBetween = true, + selectedIds, + onCardClick, }: ItemsTableProps): JSX.Element => { const [open, setOpen] = useState(false); const { t: translateCommon } = useCommonTranslation(); @@ -154,6 +158,14 @@ const ItemsTable = ({ enableMoveInBetween={enableMoveInBetween} itemsStatuses={itemsStatuses} showThumbnails={showThumbnails} + isSelected={ + 'id' in droppedEl ? selectedIds?.includes(droppedEl.id) : false + } + onThumbnailClick={() => { + if ('id' in droppedEl) { + onCardClick?.(droppedEl.id); + } + }} /> )} rows={rows} diff --git a/src/components/main/ItemsTableCard.tsx b/src/components/main/list/ItemsTableCard.tsx similarity index 68% rename from src/components/main/ItemsTableCard.tsx rename to src/components/main/list/ItemsTableCard.tsx index 3dd5c830c..daf0a74a6 100644 --- a/src/components/main/ItemsTableCard.tsx +++ b/src/components/main/list/ItemsTableCard.tsx @@ -3,17 +3,16 @@ import { Box, Stack } from '@mui/material'; import { PackedItem } from '@graasp/sdk'; import type { DroppedFile } from '@graasp/ui'; -import { Upload } from 'lucide-react'; - +import SmallUploadFile from '@/components/file/SmallUploadFile'; import { useBuilderTranslation } from '@/config/i18n'; import { ItemLayoutMode } from '@/enums'; import { BUILDER } from '@/langs/constants'; -import { useLayoutContext } from '../context/LayoutContext'; -import Badges, { ItemsStatuses } from '../table/Badges'; -import ItemActions from '../table/ItemActions'; -import ItemCard from '../table/ItemCard'; -import ItemMenuContent from './ItemMenuContent'; +import { useLayoutContext } from '../../context/LayoutContext'; +import Badges, { ItemsStatuses } from '../../table/Badges'; +import ItemActions from '../../table/ItemActions'; +import ItemCard from '../../table/ItemCard'; +import ItemMenuContent from '../ItemMenuContent'; type Props = { item: PackedItem | DroppedFile; @@ -23,6 +22,9 @@ type Props = { showThumbnails: boolean; itemsStatuses: ItemsStatuses; enableMoveInBetween: boolean; + onClick?: (id: string) => void; + isSelected?: boolean; + onThumbnailClick?: () => void; }; const ItemsTableCard = ({ @@ -33,6 +35,9 @@ const ItemsTableCard = ({ showThumbnails, itemsStatuses, enableMoveInBetween, + onClick, + isSelected, + onThumbnailClick, }: Props): JSX.Element => { const { mode } = useLayoutContext(); @@ -42,27 +47,20 @@ const ItemsTableCard = ({ if ('files' in item) { return ( - - {translateBuilder(BUILDER.UPLOAD_BETWEEN_FILES)} - + ); } return ( - + onClick?.(item.id)}> } footer={ diff --git a/src/components/main/list/SelectionContext.tsx b/src/components/main/list/SelectionContext.tsx new file mode 100644 index 000000000..aec71d8c0 --- /dev/null +++ b/src/components/main/list/SelectionContext.tsx @@ -0,0 +1,130 @@ +import { + createContext, + useCallback, + useContext, + useMemo, + useRef, + useState, +} from 'react'; + +import { PRIMARY_COLOR } from '@graasp/ui'; + +import { + Box, + boxesIntersect, + useSelectionContainer, +} from '@air/react-drag-to-select'; + +import { ITEM_CARD_CLASS } from '@/config/selectors'; + +type SelectionContextValue = { + selectedIds: string[]; + toggleSelection: (id: string) => void; + clearSelection: () => void; +}; + +export const SelectionContext = createContext({ + selectedIds: [], + toggleSelection: () => {}, + clearSelection: () => {}, +}); + +export const SelectionContextProvider = ({ + children, + elementClass = ITEM_CARD_CLASS, +}: { + children: JSX.Element; + elementClass?: string; +}): JSX.Element => { + const [selection, setSelection] = useState(new Set()); + const elementsContainerRef = useRef(null); + + const clearSelection = useCallback(() => { + setSelection(new Set()); + }, []); + + const toggleSelection = useCallback( + (id: string) => { + if (selection.has(id)) { + selection.delete(id); + } else { + selection.add(id); + } + setSelection(new Set(selection)); + }, + [selection], + ); + + const { DragSelection } = useSelectionContainer({ + eventsElement: document.getElementById('root'), + onSelectionChange: (box) => { + /** + * Here we make sure to adjust the box's left and top with the scroll position of the window + * @see https://github.com/AirLabsTeam/react-drag-to-select/#scrolling + */ + const scrollAwareBox: Box = { + ...box, + top: box.top + window.scrollY, + left: box.left + window.scrollX, + }; + + Array.from(document.getElementsByClassName(elementClass)).forEach( + (item) => { + const bb = item.getBoundingClientRect(); + if ( + boxesIntersect(scrollAwareBox, bb) && + item.parentNode instanceof HTMLElement + ) { + const itemId = item.parentNode.dataset.id; + if (itemId) { + selection.add(itemId); + } + } + }, + ); + + setSelection(new Set(selection)); + }, + onSelectionStart: () => { + // does not trigger selection + clearSelection(); + }, + shouldStartSelecting: (e) => { + // does not trigger drag selection if mousedown on card + if (e instanceof HTMLElement) { + return !e?.closest(`.${ITEM_CARD_CLASS}`); + } + return true; + }, + onSelectionEnd: () => {}, + selectionProps: { + style: { + border: `2px dashed ${PRIMARY_COLOR}`, + borderRadius: 4, + backgroundColor: 'lightblue', + opacity: 0.5, + }, + }, + isEnabled: true, + }); + + const value: SelectionContextValue = useMemo( + () => ({ + selectedIds: [...selection.values()], + toggleSelection, + clearSelection, + elementsContainerRef, + }), + [selection, toggleSelection, clearSelection, elementsContainerRef], + ); + + return ( + + +
{children}
+
+ ); +}; + +export const useSelectionContext = (): SelectionContextValue => + useContext(SelectionContext); diff --git a/src/components/main/list/SelectionToolbar.tsx b/src/components/main/list/SelectionToolbar.tsx new file mode 100644 index 000000000..ec98f50bc --- /dev/null +++ b/src/components/main/list/SelectionToolbar.tsx @@ -0,0 +1,34 @@ +import { Close } from '@mui/icons-material'; +import { IconButton, Stack } from '@mui/material'; + +import { useSelectionContext } from './SelectionContext'; + +const SelectionToolbar = ({ + children, +}: { + children: JSX.Element; +}): JSX.Element => { + const { selectedIds, clearSelection } = useSelectionContext(); + // const { t: translateEnums } = useEnumsTranslation(); + + return ( + + + + + + {selectedIds.length} selected + + {children} + + ); +}; + +export default SelectionToolbar; diff --git a/src/components/pages/BookmarkedItemsScreen.tsx b/src/components/pages/BookmarkedItemsScreen.tsx index f167b1aa9..ca86d0dfe 100644 --- a/src/components/pages/BookmarkedItemsScreen.tsx +++ b/src/components/pages/BookmarkedItemsScreen.tsx @@ -15,7 +15,7 @@ import { useFilterItemsContext } from '../context/FilterItemsContext'; import { useItemSearch } from '../item/ItemSearch'; import ModeButton from '../item/header/ModeButton'; import LoadingScreen from '../layout/LoadingScreen'; -import ItemsTable from '../main/ItemsTable'; +import ItemsTable from '../main/list/ItemsTable'; import SortingSelect from '../table/SortingSelect'; import { SortingOptions } from '../table/types'; import { useSorting, useTranslatedSortingOptions } from '../table/useSorting'; diff --git a/src/components/pages/PublishedItemsScreen.tsx b/src/components/pages/PublishedItemsScreen.tsx index 66db29200..8d992e428 100644 --- a/src/components/pages/PublishedItemsScreen.tsx +++ b/src/components/pages/PublishedItemsScreen.tsx @@ -15,7 +15,7 @@ import { useFilterItemsContext } from '../context/FilterItemsContext'; import { useItemSearch } from '../item/ItemSearch'; import ModeButton from '../item/header/ModeButton'; import LoadingScreen from '../layout/LoadingScreen'; -import ItemsTable from '../main/ItemsTable'; +import ItemsTable from '../main/list/ItemsTable'; import SortingSelect from '../table/SortingSelect'; import { SortingOptions } from '../table/types'; import { useSorting, useTranslatedSortingOptions } from '../table/useSorting'; diff --git a/src/components/pages/RecycledItemsScreen.tsx b/src/components/pages/RecycledItemsScreen.tsx index 0245e96cf..d32b42b77 100644 --- a/src/components/pages/RecycledItemsScreen.tsx +++ b/src/components/pages/RecycledItemsScreen.tsx @@ -17,11 +17,16 @@ import { useFilterItemsContext } from '../context/FilterItemsContext'; import { useItemSearch } from '../item/ItemSearch'; import ModeButton from '../item/header/ModeButton'; import LoadingScreen from '../layout/LoadingScreen'; +import { + SelectionContextProvider, + useSelectionContext, +} from '../main/list/SelectionContext'; import ItemCard from '../table/ItemCard'; import SortingSelect from '../table/SortingSelect'; import { SortingOptions } from '../table/types'; import { useSorting, useTranslatedSortingOptions } from '../table/useSorting'; import PageWrapper from './PageWrapper'; +import RecycleBinToolbar from './recycleBin/RecycleBinSelectionToolbar'; const RecycledItemsScreenContent = ({ searchText, @@ -32,20 +37,23 @@ const RecycledItemsScreenContent = ({ const { data: recycledItems, isLoading, isError } = hooks.useRecycledItems(); const options = useTranslatedSortingOptions(); const { shouldDisplayItem } = useFilterItemsContext(); - const filteredData = recycledItems?.filter( - (d) => - shouldDisplayItem(d.type) && - d.name.toLowerCase().includes(searchText.toLocaleLowerCase()), - ); const { sortBy, setSortBy, ordering, setOrdering, sortFn } = useSorting({ sortBy: SortingOptions.ItemUpdatedAt, ordering: Ordering.DESC, }); - filteredData?.sort(sortFn); + const filteredData = recycledItems + ?.filter( + (d) => + shouldDisplayItem(d.type) && + d.name.toLowerCase().includes(searchText.toLocaleLowerCase()), + ) + ?.sort(sortFn); + const { selectedIds, toggleSelection } = useSelectionContext(); // render this when there is data from the query if (recycledItems?.length) { + const hasSelection = selectedIds.length && filteredData?.length; return ( - - - - {sortBy && setSortBy && ( - - )} - + {hasSelection ? ( + + ) : ( + + + + {sortBy && setSortBy && ( + + )} + + - + )} { // render the filtered data and when it is empty display that nothing matches the search @@ -82,11 +94,14 @@ const RecycledItemsScreenContent = ({ toggleSelection(item.id)} + isSelected={selectedIds.includes(item.id)} showThumbnail={false} + allowNavigation={false} footer={ - + } /> @@ -135,7 +150,9 @@ const RecycledItemsScreen = (): JSX.Element | null => { } > - + + + ); }; diff --git a/src/components/pages/home/HomeScreen.tsx b/src/components/pages/home/HomeScreen.tsx index d7c86f2dd..d594571b2 100644 --- a/src/components/pages/home/HomeScreen.tsx +++ b/src/components/pages/home/HomeScreen.tsx @@ -5,6 +5,10 @@ import { Alert, Box, LinearProgress, Stack } from '@mui/material'; import { Button } from '@graasp/ui'; import LoadingScreen from '@/components/layout/LoadingScreen'; +import { + SelectionContextProvider, + useSelectionContext, +} from '@/components/main/list/SelectionContext'; import { ITEM_PAGE_SIZE } from '@/config/constants'; import { ShowOnlyMeChangeType } from '@/config/types'; import { ItemLayoutMode, Ordering } from '@/enums'; @@ -22,8 +26,8 @@ import { useLayoutContext } from '../../context/LayoutContext'; import FileUploader from '../../file/FileUploader'; import { useItemSearch } from '../../item/ItemSearch'; import ModeButton from '../../item/header/ModeButton'; -import ItemsTable from '../../main/ItemsTable'; import NewItemButton from '../../main/NewItemButton'; +import ItemsTable from '../../main/list/ItemsTable'; import { DesktopMap } from '../../map/DesktopMap'; import ShowOnlyMeButton from '../../table/ShowOnlyMeButton'; import SortingSelect from '../../table/SortingSelect'; @@ -31,6 +35,7 @@ import { SortingOptions } from '../../table/types'; import { useSorting } from '../../table/useSorting'; import NoItemFilters from '../NoItemFilters'; import PageWrapper from '../PageWrapper'; +import HomeSelectionToolbar from './HomeSelectionToolbar'; const HomeScreenContent = ({ searchText }: { searchText: string }) => { const { t: translateBuilder } = useBuilderTranslation(); @@ -39,6 +44,7 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => { const { itemTypes } = useFilterItemsContext(); const [showOnlyMe, setShowOnlyMe] = useState(false); + const { selectedIds, toggleSelection } = useSelectionContext(); const { mode } = useLayoutContext(); const { sortBy, setSortBy, ordering, setOrdering } = useSorting({ @@ -92,6 +98,8 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => { id={ACCESSIBLE_ITEMS_TABLE_ID} items={data.pages.flatMap(({ data: i }) => i)} enableMoveInBetween={false} + onCardClick={toggleSelection} + selectedIds={selectedIds} /> {!isFetching && data.pages[0].totalCount > totalFetchedItems && ( @@ -127,28 +135,35 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => { enabled={showOnlyMe} /> - - - - {sortBy && setSortBy && ( - - sortBy={sortBy} - setSortBy={setSortBy} - ordering={ordering} - setOrdering={setOrdering} - options={Object.values(SortingOptions).sort((t1, t2) => - translateEnums(t1).localeCompare(translateEnums(t2)), - )} - /> - )} - + + {selectedIds.length ? ( + i)} + /> + ) : ( + + + + {sortBy && setSortBy && ( + + sortBy={sortBy} + setSortBy={setSortBy} + ordering={ordering} + setOrdering={setOrdering} + options={Object.values(SortingOptions).sort((t1, t2) => + translateEnums(t1).localeCompare(translateEnums(t2)), + )} + /> + )} + + - + )} {content} @@ -191,7 +206,9 @@ const HomeScreen = (): JSX.Element => { } > - + + + ); }; diff --git a/src/components/pages/home/HomeSelectionToolbar.tsx b/src/components/pages/home/HomeSelectionToolbar.tsx new file mode 100644 index 000000000..16eb1b1b3 --- /dev/null +++ b/src/components/pages/home/HomeSelectionToolbar.tsx @@ -0,0 +1,63 @@ +import { PackedItem } from '@graasp/sdk'; + +import RecycleButton from '@/components/common/RecycleButton'; +import useModalStatus from '@/components/hooks/useModalStatus'; +import CopyButton from '@/components/item/copy/CopyButton'; +import { CopyModal } from '@/components/item/copy/CopyModal'; +import MoveButton from '@/components/item/move/MoveButton'; +import { MoveModal } from '@/components/item/move/MoveModal'; +import { useSelectionContext } from '@/components/main/list/SelectionContext'; +import SelectionToolbar from '@/components/main/list/SelectionToolbar'; + +const HomeSelectionToolbar = ({ + items, +}: { + items: PackedItem[]; +}): JSX.Element => { + const { selectedIds, clearSelection } = useSelectionContext(); + + const { + isOpen: isCopyModalOpen, + openModal: openCopyModal, + closeModal: closeCopyModal, + } = useModalStatus(); + const { + isOpen: isMoveModalOpen, + openModal: openMoveModal, + closeModal: closeMoveModal, + } = useModalStatus(); + + return ( + <> + { + closeCopyModal(); + clearSelection(); + }} + open={isCopyModalOpen} + itemIds={selectedIds} + /> + { + closeMoveModal(); + clearSelection(); + }} + open={isMoveModalOpen} + items={items?.filter(({ id }) => selectedIds.includes(id))} + /> + + <> + + + + + + + ); +}; + +export default HomeSelectionToolbar; diff --git a/src/components/pages/item/ItemLoginWrapper.tsx b/src/components/pages/item/ItemLoginWrapper.tsx index e0ec69502..f7a2f7cde 100644 --- a/src/components/pages/item/ItemLoginWrapper.tsx +++ b/src/components/pages/item/ItemLoginWrapper.tsx @@ -10,7 +10,7 @@ import { ITEM_LOGIN_SIGN_IN_USERNAME_ID, } from '@/config/selectors'; -import ItemForbiddenScreen from '../../main/ItemForbiddenScreen'; +import ItemForbiddenScreen from '../../main/list/ItemForbiddenScreen'; const { useItem, useCurrentMember, useItemLoginSchemaType } = hooks; diff --git a/src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx b/src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx new file mode 100644 index 000000000..34db25cde --- /dev/null +++ b/src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx @@ -0,0 +1,28 @@ +import { PackedItem } from '@graasp/sdk'; + +import DeleteButton from '@/components/common/DeleteButton'; +import RestoreButton from '@/components/common/RestoreButton'; +import { useSelectionContext } from '@/components/main/list/SelectionContext'; +import SelectionToolbar from '@/components/main/list/SelectionToolbar'; + +const RecycleBinSelectionToolbar = ({ + items, +}: { + items: PackedItem[]; +}): JSX.Element => { + const { selectedIds, clearSelection } = useSelectionContext(); + + return ( + + <> + + selectedIds.includes(id))} + onConfirm={clearSelection} + /> + + + ); +}; + +export default RecycleBinSelectionToolbar; diff --git a/src/components/table/ItemCard.tsx b/src/components/table/ItemCard.tsx index a76d95840..b7d0ddef0 100644 --- a/src/components/table/ItemCard.tsx +++ b/src/components/table/ItemCard.tsx @@ -1,4 +1,4 @@ -import { Typography } from '@mui/material'; +import { Box, Typography } from '@mui/material'; import Grid2 from '@mui/material/Unstable_Grid2/Grid2'; import { @@ -14,7 +14,7 @@ import { Card, TextDisplay } from '@graasp/ui'; import i18n, { useCommonTranslation } from '@/config/i18n'; import { buildItemPath } from '@/config/paths'; import { hooks } from '@/config/queryClient'; -import { buildItemCard } from '@/config/selectors'; +import { ITEM_CARD_CLASS, buildItemCard } from '@/config/selectors'; type Props = { item: PackedItem; @@ -25,6 +25,9 @@ type Props = { isDragging?: boolean; disabled?: boolean; menu?: JSX.Element; + isSelected?: boolean; + allowNavigation?: boolean; + onThumbnailClick?: () => void; }; const ItemCard = ({ @@ -33,9 +36,12 @@ const ItemCard = ({ dense = true, isDragging = false, isOver = false, + isSelected = false, showThumbnail = true, disabled, menu, + allowNavigation = true, + onThumbnailClick, }: Props): JSX.Element => { const { t: translateCommon } = useCommonTranslation(); const { data: thumbnailUrl } = hooks.useItemThumbnailUrl({ @@ -90,24 +96,29 @@ const ItemCard = ({ } return ( - + + + ); }; export default ItemCard; diff --git a/src/config/selectors.ts b/src/config/selectors.ts index f9d6fc26a..d481cfe17 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -424,3 +424,4 @@ export const BOOKMARK_ICON_SELECTOR = export const MEMBER_VALIDATION_BANNER_ID = 'memberValidationBanner'; export const MEMBER_VALIDATION_BANNER_CLOSE_BUTTON_ID = 'memberValidationBannerCloseButton'; +export const ITEM_CARD_CLASS = 'item-card'; diff --git a/yarn.lock b/yarn.lock index f361c2305..8258d3dad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -81,6 +81,18 @@ __metadata: languageName: node linkType: hard +"@air/react-drag-to-select@npm:5.0.8": + version: 5.0.8 + resolution: "@air/react-drag-to-select@npm:5.0.8" + dependencies: + react-style-object-to-css: "npm:^1.1.2" + peerDependencies: + react: 16 - 18 + react-dom: 16 - 18 + checksum: 10/37ca191e981793e1357833238a48749426ca17782e99c7e5a64eed0a1a1473d2255cead716d6af8e9f605f633a2801a9e1bfe36583a3fc10e961c9a89f340772 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -1925,25 +1937,24 @@ __metadata: languageName: node linkType: hard -"@graasp/ui@npm:4.20.0": - version: 4.20.0 - resolution: "@graasp/ui@npm:4.20.0" +"@graasp/ui@github:graasp/graasp-ui#card-selection": + version: 4.21.0 + resolution: "@graasp/ui@https://github.com/graasp/graasp-ui.git#commit=b61d3b5ea6492d3802bceae5e92ade7a229a79de" dependencies: "@ag-grid-community/client-side-row-model": "npm:31.3.2" - "@ag-grid-community/react": "npm:^31.3.1" - "@ag-grid-community/styles": "npm:^31.3.1" - "@storybook/react-vite": "npm:8.1.10" + "@ag-grid-community/react": "npm:^31.3.2" + "@ag-grid-community/styles": "npm:^31.3.2" + "@storybook/react-vite": "npm:8.1.11" http-status-codes: "npm:2.3.0" interweave: "npm:13.1.0" - katex: "npm:0.16.10" + katex: "npm:0.16.11" lodash.truncate: "npm:4.4.2" - lucide-react: "npm:0.395.0" + lucide-react: "npm:0.408.0" react-cookie-consent: "npm:9.0.0" react-dnd: "npm:16.0.1" react-dnd-html5-backend: "npm:16.0.1" react-quill: "npm:2.0.0" react-rnd: "npm:10.4.11" - react-text-mask: "npm:5.5.0" uuid: "npm:10.0.0" vitest: "npm:1.6.0" peerDependencies: @@ -1952,9 +1963,9 @@ __metadata: "@emotion/styled": ~11.10.6 || ~11.11.0 "@graasp/sdk": ^4.14.0 "@graasp/translations": ^1.23.0 - "@mui/icons-material": ~5.14.0 || ~5.15.0 + "@mui/icons-material": ~5.14.0 || ~5.15.0 || ~5.16.0 "@mui/lab": ~5.0.0-alpha.150 - "@mui/material": ~5.14.0 || ~5.15.0 + "@mui/material": ~5.14.0 || ~5.15.0 || ~5.16.0 i18next: ^22.4.15 || ^23.0.0 react: ^18.0.0 react-dom: ^18.0.0 @@ -1962,28 +1973,29 @@ __metadata: react-router-dom: ^6.11.0 stylis: ^4.1.3 stylis-plugin-rtl: ^2.1.1 - checksum: 10/2259b603ddffb00187f5684215a9e16e91a192612a84f7807759f385b7121fe198bdb20ae17fc978b1996a0b921dd2381283f22a3a84ad40c3b8a0031efdaf05 + checksum: 10/3cc25f93e2b81e3e9703c0af6910d3495effb177942707329e4264dc940f3b340e36f6078db8a7b73afbddfdfa0e3506adb6a67715c688f7cbaf95561c509adf languageName: node linkType: hard -"@graasp/ui@npm:4.21.0": - version: 4.21.0 - resolution: "@graasp/ui@npm:4.21.0" +"@graasp/ui@npm:4.20.0": + version: 4.20.0 + resolution: "@graasp/ui@npm:4.20.0" dependencies: "@ag-grid-community/client-side-row-model": "npm:31.3.2" - "@ag-grid-community/react": "npm:^31.3.2" - "@ag-grid-community/styles": "npm:^31.3.2" - "@storybook/react-vite": "npm:8.1.11" + "@ag-grid-community/react": "npm:^31.3.1" + "@ag-grid-community/styles": "npm:^31.3.1" + "@storybook/react-vite": "npm:8.1.10" http-status-codes: "npm:2.3.0" interweave: "npm:13.1.0" - katex: "npm:0.16.11" + katex: "npm:0.16.10" lodash.truncate: "npm:4.4.2" - lucide-react: "npm:0.402.0" + lucide-react: "npm:0.395.0" react-cookie-consent: "npm:9.0.0" react-dnd: "npm:16.0.1" react-dnd-html5-backend: "npm:16.0.1" react-quill: "npm:2.0.0" react-rnd: "npm:10.4.11" + react-text-mask: "npm:5.5.0" uuid: "npm:10.0.0" vitest: "npm:1.6.0" peerDependencies: @@ -1992,9 +2004,9 @@ __metadata: "@emotion/styled": ~11.10.6 || ~11.11.0 "@graasp/sdk": ^4.14.0 "@graasp/translations": ^1.23.0 - "@mui/icons-material": ~5.14.0 || ~5.15.0 || ~5.16.0 + "@mui/icons-material": ~5.14.0 || ~5.15.0 "@mui/lab": ~5.0.0-alpha.150 - "@mui/material": ~5.14.0 || ~5.15.0 || ~5.16.0 + "@mui/material": ~5.14.0 || ~5.15.0 i18next: ^22.4.15 || ^23.0.0 react: ^18.0.0 react-dom: ^18.0.0 @@ -2002,7 +2014,7 @@ __metadata: react-router-dom: ^6.11.0 stylis: ^4.1.3 stylis-plugin-rtl: ^2.1.1 - checksum: 10/6a8778ae27ebb534204a44e1f97b08fdba6d7524fe3417fd7eaab8f4cf66e19800a38812f0b10c8796c885d6cabc5823bed137aebfbf36f29308ac7f04525314 + checksum: 10/2259b603ddffb00187f5684215a9e16e91a192612a84f7807759f385b7121fe198bdb20ae17fc978b1996a0b921dd2381283f22a3a84ad40c3b8a0031efdaf05 languageName: node linkType: hard @@ -2254,35 +2266,6 @@ __metadata: languageName: node linkType: hard -"@mui/lab@npm:5.0.0-alpha.172": - version: 5.0.0-alpha.172 - resolution: "@mui/lab@npm:5.0.0-alpha.172" - dependencies: - "@babel/runtime": "npm:^7.23.9" - "@mui/base": "npm:5.0.0-beta.40" - "@mui/system": "npm:^5.16.1" - "@mui/types": "npm:^7.2.15" - "@mui/utils": "npm:^5.16.1" - clsx: "npm:^2.1.0" - prop-types: "npm:^15.8.1" - peerDependencies: - "@emotion/react": ^11.5.0 - "@emotion/styled": ^11.3.0 - "@mui/material": ">=5.15.0" - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@emotion/react": - optional: true - "@emotion/styled": - optional: true - "@types/react": - optional: true - checksum: 10/8d05664c115eda2d7594fe52ad2cb8cb6ee45fe01a44d88fd30abe2848c51911c8310f08122f7a98e5cc9658e3cedc9e7ccbf31c503a838cb4abd00c0c176b7a - languageName: node - linkType: hard - "@mui/material@npm:5.15.20": version: 5.15.20 resolution: "@mui/material@npm:5.15.20" @@ -2383,23 +2366,6 @@ __metadata: languageName: node linkType: hard -"@mui/private-theming@npm:^5.16.1": - version: 5.16.1 - resolution: "@mui/private-theming@npm:5.16.1" - dependencies: - "@babel/runtime": "npm:^7.23.9" - "@mui/utils": "npm:^5.16.1" - prop-types: "npm:^15.8.1" - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/fdfbc5e55bc1c980a00faab952e37280f437826788d8b1908d3eed75d053a4848b6927ea650a976b23419997113509e7ae0f21f325df6ba3a394ccdb32aa4f84 - languageName: node - linkType: hard - "@mui/private-theming@npm:^5.16.4": version: 5.16.4 resolution: "@mui/private-theming@npm:5.16.4" @@ -2438,27 +2404,6 @@ __metadata: languageName: node linkType: hard -"@mui/styled-engine@npm:^5.16.1": - version: 5.16.1 - resolution: "@mui/styled-engine@npm:5.16.1" - dependencies: - "@babel/runtime": "npm:^7.23.9" - "@emotion/cache": "npm:^11.11.0" - csstype: "npm:^3.1.3" - prop-types: "npm:^15.8.1" - peerDependencies: - "@emotion/react": ^11.4.1 - "@emotion/styled": ^11.3.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@emotion/react": - optional: true - "@emotion/styled": - optional: true - checksum: 10/3f1d39b48a437179e96ffe82b51f19e45aeffa51dae2644ac218e4e1700945680caa680ff009658b41cfe2a05ae4e756c9b9f59a4df0ce6ada82e727597949f6 - languageName: node - linkType: hard - "@mui/styled-engine@npm:^5.16.4": version: 5.16.4 resolution: "@mui/styled-engine@npm:5.16.4" @@ -2536,34 +2481,6 @@ __metadata: languageName: node linkType: hard -"@mui/system@npm:^5.16.1": - version: 5.16.1 - resolution: "@mui/system@npm:5.16.1" - dependencies: - "@babel/runtime": "npm:^7.23.9" - "@mui/private-theming": "npm:^5.16.1" - "@mui/styled-engine": "npm:^5.16.1" - "@mui/types": "npm:^7.2.15" - "@mui/utils": "npm:^5.16.1" - clsx: "npm:^2.1.0" - csstype: "npm:^3.1.3" - prop-types: "npm:^15.8.1" - peerDependencies: - "@emotion/react": ^11.5.0 - "@emotion/styled": ^11.3.0 - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@emotion/react": - optional: true - "@emotion/styled": - optional: true - "@types/react": - optional: true - checksum: 10/e49852b203b61bf4166ba4f1c1e56c846ea204219a6c9e725cbadcad264bff52cec5a10a0cd6516e1772958022b0d204bd8c7674e29d63ea212f5aef7e2814c5 - languageName: node - linkType: hard - "@mui/system@npm:^5.16.4": version: 5.16.4 resolution: "@mui/system@npm:5.16.4" @@ -2634,24 +2551,6 @@ __metadata: languageName: node linkType: hard -"@mui/utils@npm:^5.16.1": - version: 5.16.1 - resolution: "@mui/utils@npm:5.16.1" - dependencies: - "@babel/runtime": "npm:^7.23.9" - "@types/prop-types": "npm:^15.7.12" - prop-types: "npm:^15.8.1" - react-is: "npm:^18.3.1" - peerDependencies: - "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10/d3294dfc9953b8f1697c4837bf57a81a97b26fdfc6dd4d28747b3126a4ae8d9f4160e03326d42fbb2e1885ea4d9c56301516e13c8b50d81ea4db2455d9f18f3b - languageName: node - linkType: hard - "@mui/utils@npm:^5.16.4": version: 5.16.4 resolution: "@mui/utils@npm:5.16.4" @@ -8134,6 +8033,7 @@ __metadata: dependencies: "@ag-grid-community/core": "npm:31.3.4" "@ag-grid-community/styles": "npm:31.3.4" + "@air/react-drag-to-select": "npm:5.0.8" "@commitlint/cli": "npm:19.3.0" "@commitlint/config-conventional": "npm:19.2.2" "@cypress/code-coverage": "npm:3.12.44" @@ -8145,9 +8045,8 @@ __metadata: "@graasp/query-client": "npm:3.16.0" "@graasp/sdk": "npm:4.19.0" "@graasp/translations": "npm:1.32.0" - "@graasp/ui": "npm:4.21.0" + "@graasp/ui": "github:graasp/graasp-ui#card-selection" "@mui/icons-material": "npm:5.16.4" - "@mui/lab": "npm:5.0.0-alpha.172" "@mui/material": "npm:5.16.4" "@sentry/react": "npm:7.118.0" "@testing-library/jest-dom": "npm:^6.4.2" @@ -9904,12 +9803,12 @@ __metadata: languageName: node linkType: hard -"lucide-react@npm:0.402.0": - version: 0.402.0 - resolution: "lucide-react@npm:0.402.0" +"lucide-react@npm:0.408.0": + version: 0.408.0 + resolution: "lucide-react@npm:0.408.0" peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/fe8a7d5b65e469ee9f376e65942a1cb66aa39fc650265be4153e0f0235a0a5b09cdb03e8f51ed1f2319bdf91e1ae0859c61fe3a25c68e08ecb289a692bf52a17 + checksum: 10/3849f76a3aec5a60008606d661ba8b540f2769b117a797c4c5300f303f18971a725b9de0410f278b89004bff99d1596e3669acb46c6452aa9bd7b029acd1c758 languageName: node linkType: hard @@ -12254,6 +12153,13 @@ __metadata: languageName: node linkType: hard +"react-style-object-to-css@npm:^1.1.2": + version: 1.1.2 + resolution: "react-style-object-to-css@npm:1.1.2" + checksum: 10/1f854bf5c7fabcc0be3db3e35e4b346dd0d3cb2b09079f100380ab299d2db94ba75ab225254446958d820933c7be67b609219746610a14cda2c42e708711cc78 + languageName: node + linkType: hard + "react-text-mask@npm:5.5.0": version: 5.5.0 resolution: "react-text-mask@npm:5.5.0" From e1d336f8ca35efabf33f31739a9d840be2cbeece Mon Sep 17 00:00:00 2001 From: kim Date: Thu, 18 Jul 2024 11:26:37 +0200 Subject: [PATCH 2/8] refactor: remove useless code --- src/components/main/list/SelectionContext.tsx | 8 ++++---- src/components/main/list/SelectionToolbar.tsx | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/main/list/SelectionContext.tsx b/src/components/main/list/SelectionContext.tsx index aec71d8c0..61051cf61 100644 --- a/src/components/main/list/SelectionContext.tsx +++ b/src/components/main/list/SelectionContext.tsx @@ -85,10 +85,6 @@ export const SelectionContextProvider = ({ setSelection(new Set(selection)); }, - onSelectionStart: () => { - // does not trigger selection - clearSelection(); - }, shouldStartSelecting: (e) => { // does not trigger drag selection if mousedown on card if (e instanceof HTMLElement) { @@ -96,6 +92,10 @@ export const SelectionContextProvider = ({ } return true; }, + onSelectionStart: () => { + // clear selection on new dragging action + clearSelection(); + }, onSelectionEnd: () => {}, selectionProps: { style: { diff --git a/src/components/main/list/SelectionToolbar.tsx b/src/components/main/list/SelectionToolbar.tsx index ec98f50bc..5cf6eb331 100644 --- a/src/components/main/list/SelectionToolbar.tsx +++ b/src/components/main/list/SelectionToolbar.tsx @@ -9,7 +9,6 @@ const SelectionToolbar = ({ children: JSX.Element; }): JSX.Element => { const { selectedIds, clearSelection } = useSelectionContext(); - // const { t: translateEnums } = useEnumsTranslation(); return ( Date: Mon, 22 Jul 2024 12:20:56 +0200 Subject: [PATCH 3/8] refactor: add tests --- cypress/e2e/item/bookmarks/bookmarks.cy.ts | 6 +- cypress/e2e/item/copy/copy.cy.ts | 97 +++++++++++++++++ .../item/move/{moveItem.cy.ts => move.cy.ts} | 99 ++++++++++++++++- .../trash/{deleteItem.cy.ts => delete.cy.ts} | 35 +++++- cypress/e2e/item/trash/recycle.cy.ts | 103 ++++++++++++++++++ cypress/e2e/item/trash/recycleItem.cy.ts | 46 -------- .../{restoreItem.cy.ts => restore.cy.ts} | 31 +++++- cypress/e2e/item/view/viewLink.cy.ts | 2 +- cypress/support/commands/item.ts | 12 +- cypress/support/index.ts | 4 + package.json | 1 + .../recycleBin/RecycleBinSelectionToolbar.tsx | 11 +- src/config/selectors.ts | 7 ++ yarn.lock | 38 ++++++- 14 files changed, 424 insertions(+), 68 deletions(-) rename cypress/e2e/item/move/{moveItem.cy.ts => move.cy.ts} (56%) rename cypress/e2e/item/trash/{deleteItem.cy.ts => delete.cy.ts} (54%) create mode 100644 cypress/e2e/item/trash/recycle.cy.ts delete mode 100644 cypress/e2e/item/trash/recycleItem.cy.ts rename cypress/e2e/item/trash/{restoreItem.cy.ts => restore.cy.ts} (57%) diff --git a/cypress/e2e/item/bookmarks/bookmarks.cy.ts b/cypress/e2e/item/bookmarks/bookmarks.cy.ts index f0a90c430..3b4f7a9cd 100644 --- a/cypress/e2e/item/bookmarks/bookmarks.cy.ts +++ b/cypress/e2e/item/bookmarks/bookmarks.cy.ts @@ -47,7 +47,7 @@ describe('Bookmarked Item', () => { }); it('Show empty table', () => { - i18n.changeLanguage(CURRENT_USER.extra.lang as string); + i18n.changeLanguage(CURRENT_USER.extra.lang); const text = i18n.t(BUILDER.BOOKMARKS_NO_ITEM, { ns: BUILDER_NAMESPACE }); cy.get(`#${BOOKMARKED_ITEMS_ID}`).should('contain', text); }); @@ -59,7 +59,7 @@ describe('Bookmarked Item', () => { items: [...ITEMS, NON_BOOKMARKED_ITEM], bookmarkedItems: BOOKMARKED_ITEMS, }); - i18n.changeLanguage(CURRENT_USER.extra.lang as string); + i18n.changeLanguage(CURRENT_USER.extra.lang); cy.visit(BOOKMARKED_ITEMS_PATH); }); @@ -83,7 +83,7 @@ describe('Bookmarked Item', () => { } }); - it('Add item to bookmarks', () => { + it.only('Add item to bookmarks', () => { cy.visit(HOME_PATH); const item = NON_BOOKMARKED_ITEM; diff --git a/cypress/e2e/item/copy/copy.cy.ts b/cypress/e2e/item/copy/copy.cy.ts index 634dddb55..bd0fab642 100644 --- a/cypress/e2e/item/copy/copy.cy.ts +++ b/cypress/e2e/item/copy/copy.cy.ts @@ -11,6 +11,17 @@ import { buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; +const copyItems = ({ + toItemPath, + rootId, +}: { + toItemPath: string; + rootId?: string; +}) => { + cy.get(`[data-testid="FilterNoneIcon"]`).click(); + cy.handleTreeMenu(toItemPath, rootId); +}; + const copyItem = ({ id, toItemPath, @@ -82,4 +93,90 @@ describe('Copy Item', () => { expect(url).to.contain(id); }); }); + + it('copy many items on Home', () => { + const folders = [ + PackedFolderItemFactory(), + PackedFolderItemFactory(), + PackedFolderItemFactory(), + ]; + cy.setUpApi({ + items: folders, + }); + + // go to children item + cy.visit('/'); + + folders.forEach((item) => { + cy.selectItem(item.id); + }); + + // copy on home + copyItems({ toItemPath: '' }); + + cy.wait('@copyItems').then(({ request: { url } }) => { + folders.forEach((item) => { + expect(url).to.contain(item.id); + }); + }); + }); + + it('copy many items from Home to folder', () => { + const folders = [ + PackedFolderItemFactory(), + PackedFolderItemFactory(), + PackedFolderItemFactory(), + ]; + const toItem = PackedFolderItemFactory(); + cy.setUpApi({ + items: [...folders, toItem], + }); + + // go to children item + cy.visit('/'); + + folders.forEach((item) => { + cy.selectItem(item.id); + }); + + // copy on home + copyItems({ toItemPath: toItem.path }); + + cy.wait('@copyItems').then(({ request: { url, body } }) => { + expect(body.parentId).to.eq(toItem.id); + folders.forEach((item) => { + expect(url).to.contain(item.id); + }); + }); + }); + + it('copy many items from folder to folder', () => { + const parentItem = PackedFolderItemFactory(); + const folders = [ + PackedFolderItemFactory({ parentItem }), + PackedFolderItemFactory({ parentItem }), + PackedFolderItemFactory({ parentItem }), + ]; + const toItem = PackedFolderItemFactory(); + cy.setUpApi({ + items: [...folders, parentItem, toItem], + }); + + // go to children item + cy.visit(buildItemPath(parentItem.id)); + + folders.forEach((item) => { + cy.selectItem(item.id); + }); + + // copy on home + copyItems({ toItemPath: toItem.path }); + + cy.wait('@copyItems').then(({ request: { url, body } }) => { + expect(body.parentId).to.eq(toItem.id); + folders.forEach((item) => { + expect(url).to.contain(item.id); + }); + }); + }); }); diff --git a/cypress/e2e/item/move/moveItem.cy.ts b/cypress/e2e/item/move/move.cy.ts similarity index 56% rename from cypress/e2e/item/move/moveItem.cy.ts rename to cypress/e2e/item/move/move.cy.ts index b6a379d1f..8f8c5abc1 100644 --- a/cypress/e2e/item/move/moveItem.cy.ts +++ b/cypress/e2e/item/move/move.cy.ts @@ -24,6 +24,17 @@ const openMoveModal = ({ id: movedItemId }: { id: string }) => { cy.get(`.${ITEM_MENU_MOVE_BUTTON_CLASS}`).click(); }; +const moveItems = ({ + toItemPath, + rootId, +}: { + toItemPath: string; + rootId?: string; +}) => { + cy.get(`[data-testid="OpenWithIcon"]`).click(); + cy.handleTreeMenu(toItemPath, rootId); +}; + const moveItem = ({ id: movedItemId, toItemPath, @@ -37,7 +48,7 @@ const moveItem = ({ cy.handleTreeMenu(toItemPath, rootId); }; -describe('Move Item', () => { +describe('Move Items', () => { it('move item on Home', () => { cy.setUpApi({ items }); cy.visit(HOME_PATH); @@ -118,4 +129,90 @@ describe('Move Item', () => { expect(url).to.contain(movedItem); }); }); + + it('move many items from Home to folder', () => { + const folders = [ + PackedFolderItemFactory(), + PackedFolderItemFactory(), + PackedFolderItemFactory(), + ]; + const toItem = PackedFolderItemFactory(); + cy.setUpApi({ + items: [...folders, toItem], + }); + + // go to children item + cy.visit('/'); + + folders.forEach((item) => { + cy.selectItem(item.id); + }); + + moveItems({ toItemPath: toItem.path }); + + cy.wait('@moveItems').then(({ request: { url, body } }) => { + expect(body.parentId).to.eq(toItem.id); + folders.forEach((item) => { + expect(url).to.contain(item.id); + }); + }); + }); + + it('move many items from folder to folder', () => { + const parentItem = PackedFolderItemFactory(); + const folders = [ + PackedFolderItemFactory({ parentItem }), + PackedFolderItemFactory({ parentItem }), + PackedFolderItemFactory({ parentItem }), + ]; + const toItem = PackedFolderItemFactory(); + cy.setUpApi({ + items: [...folders, parentItem, toItem], + }); + + // go to children item + cy.visit(buildItemPath(parentItem.id)); + + folders.forEach((item) => { + cy.selectItem(item.id); + }); + + moveItems({ toItemPath: toItem.path }); + + cy.wait('@moveItems').then(({ request: { url, body } }) => { + expect(body.parentId).to.eq(toItem.id); + folders.forEach((item) => { + expect(url).to.contain(item.id); + }); + }); + }); + + it('move many items from folder to Home', () => { + const parentItem = PackedFolderItemFactory(); + const folders = [ + PackedFolderItemFactory({ parentItem }), + PackedFolderItemFactory({ parentItem }), + PackedFolderItemFactory({ parentItem }), + ]; + cy.setUpApi({ + items: [...folders, parentItem], + }); + + // go to children item + cy.visit(buildItemPath(parentItem.id)); + + folders.forEach((item) => { + cy.selectItem(item.id); + }); + + moveItems({ toItemPath: '' }); + + cy.wait('@moveItems').then(({ request: { url, body } }) => { + // eslint-disable-next-line no-unused-expressions + expect(body.parentId).to.be.undefined; + folders.forEach((item) => { + expect(url).to.contain(item.id); + }); + }); + }); }); diff --git a/cypress/e2e/item/trash/deleteItem.cy.ts b/cypress/e2e/item/trash/delete.cy.ts similarity index 54% rename from cypress/e2e/item/trash/deleteItem.cy.ts rename to cypress/e2e/item/trash/delete.cy.ts index d0bdc875f..5176f7a99 100644 --- a/cypress/e2e/item/trash/deleteItem.cy.ts +++ b/cypress/e2e/item/trash/delete.cy.ts @@ -3,6 +3,7 @@ import { PackedRecycledItemDataFactory } from '@graasp/sdk'; import { RECYCLE_BIN_PATH } from '../../../../src/config/paths'; import { CONFIRM_DELETE_BUTTON_ID, + RECYCLE_BIN_DELETE_MANY_ITEMS_BUTTON_ID, buildItemCard, } from '../../../../src/config/selectors'; @@ -11,18 +12,26 @@ const deleteItem = (id: string) => { cy.get(`#${CONFIRM_DELETE_BUTTON_ID}`).click(); }; -describe('Delete Item', () => { - it('delete item', () => { - const recycledItemData = [ - PackedRecycledItemDataFactory(), - PackedRecycledItemDataFactory(), - ]; +const deleteItems = () => { + cy.get(`#${RECYCLE_BIN_DELETE_MANY_ITEMS_BUTTON_ID}`).click(); + cy.get(`#${CONFIRM_DELETE_BUTTON_ID}`).click(); +}; + +const recycledItemData = [ + PackedRecycledItemDataFactory(), + PackedRecycledItemDataFactory(), +]; + +describe('Delete Items', () => { + beforeEach(() => { cy.setUpApi({ items: recycledItemData.map(({ item }) => item), recycledItemData, }); cy.visit(RECYCLE_BIN_PATH); + }); + it('delete item', () => { const { id } = recycledItemData[0].item; // delete @@ -32,4 +41,18 @@ describe('Delete Item', () => { }); cy.wait('@getRecycledItems'); }); + + it('delete many items', () => { + recycledItemData.forEach(({ item }) => { + cy.selectItem(item.id); + }); + + deleteItems(); + + cy.wait('@deleteItems').then(({ request: { url } }) => { + recycledItemData.forEach(({ item }) => { + expect(url).to.contain(item.id); + }); + }); + }); }); diff --git a/cypress/e2e/item/trash/recycle.cy.ts b/cypress/e2e/item/trash/recycle.cy.ts new file mode 100644 index 000000000..f780f6852 --- /dev/null +++ b/cypress/e2e/item/trash/recycle.cy.ts @@ -0,0 +1,103 @@ +import { PackedFolderItemFactory } from '@graasp/sdk'; + +import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; +import { + ITEM_MENU_RECYCLE_BUTTON_CLASS, + ITEM_RECYCLE_BUTTON_CLASS, + buildItemsGridMoreButtonSelector, +} from '../../../../src/config/selectors'; + +const recycleItem = (id: string) => { + cy.get(buildItemsGridMoreButtonSelector(id)).click(); + cy.get(`.${ITEM_MENU_RECYCLE_BUTTON_CLASS}`).click(); +}; + +const FOLDER = PackedFolderItemFactory(); +const CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); +const items = [FOLDER, CHILD, PackedFolderItemFactory()]; + +const recycleItems = () => { + cy.get(`.${ITEM_RECYCLE_BUTTON_CLASS}`).click(); +}; + +describe('Recycle Items', () => { + it('recycle item on Home', () => { + cy.setUpApi({ items }); + cy.visit(HOME_PATH); + + const { id } = items[0]; + + recycleItem(id); + cy.wait('@recycleItems').then(({ request: { url } }) => { + expect(url).to.contain(id); + }); + cy.wait('@getAccessibleItems'); + }); + + it('recycle item inside parent', () => { + cy.setUpApi({ items }); + const { id } = FOLDER; + const { id: idToDelete } = CHILD; + + // go to children item + cy.visit(buildItemPath(id)); + + // delete + recycleItem(idToDelete); + cy.wait('@recycleItems').then(({ request: { url } }) => { + expect(url).to.contain(idToDelete); + }); + }); + + it('recycle many items from Home', () => { + const folders = [ + PackedFolderItemFactory(), + PackedFolderItemFactory(), + PackedFolderItemFactory(), + ]; + cy.setUpApi({ + items: folders, + }); + + cy.visit('/'); + + folders.forEach((item) => { + cy.selectItem(item.id); + }); + + recycleItems(); + + cy.wait('@recycleItems').then(({ request: { url } }) => { + folders.forEach((item) => { + expect(url).to.contain(item.id); + }); + }); + }); + + it('recycle many items from folder', () => { + const parentItem = PackedFolderItemFactory(); + const folders = [ + PackedFolderItemFactory({ parentItem }), + PackedFolderItemFactory({ parentItem }), + PackedFolderItemFactory({ parentItem }), + ]; + cy.setUpApi({ + items: [...folders, parentItem], + }); + + // go to children item + cy.visit(buildItemPath(parentItem.id)); + + folders.forEach((item) => { + cy.selectItem(item.id); + }); + + recycleItems(); + + cy.wait('@recycleItems').then(({ request: { url } }) => { + folders.forEach((item) => { + expect(url).to.contain(item.id); + }); + }); + }); +}); diff --git a/cypress/e2e/item/trash/recycleItem.cy.ts b/cypress/e2e/item/trash/recycleItem.cy.ts deleted file mode 100644 index 4c2086fb7..000000000 --- a/cypress/e2e/item/trash/recycleItem.cy.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PackedFolderItemFactory } from '@graasp/sdk'; - -import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; -import { - ITEM_MENU_RECYCLE_BUTTON_CLASS, - buildItemsGridMoreButtonSelector, -} from '../../../../src/config/selectors'; - -const recycleItem = (id: string) => { - cy.get(buildItemsGridMoreButtonSelector(id)).click(); - cy.get(`.${ITEM_MENU_RECYCLE_BUTTON_CLASS}`).click(); -}; - -const FOLDER = PackedFolderItemFactory(); -const CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); -const items = [FOLDER, CHILD, PackedFolderItemFactory()]; - -describe('Recycle Item', () => { - it('recycle item on Home', () => { - cy.setUpApi({ items }); - cy.visit(HOME_PATH); - - const { id } = items[0]; - - recycleItem(id); - cy.wait('@recycleItems').then(({ request: { url } }) => { - expect(url).to.contain(id); - }); - cy.wait('@getAccessibleItems'); - }); - - it('recycle item inside parent', () => { - cy.setUpApi({ items }); - const { id } = FOLDER; - const { id: idToDelete } = CHILD; - - // go to children item - cy.visit(buildItemPath(id)); - - // delete - recycleItem(idToDelete); - cy.wait('@recycleItems').then(({ request: { url } }) => { - expect(url).to.contain(idToDelete); - }); - }); -}); diff --git a/cypress/e2e/item/trash/restoreItem.cy.ts b/cypress/e2e/item/trash/restore.cy.ts similarity index 57% rename from cypress/e2e/item/trash/restoreItem.cy.ts rename to cypress/e2e/item/trash/restore.cy.ts index 5f5b062de..55e00a214 100644 --- a/cypress/e2e/item/trash/restoreItem.cy.ts +++ b/cypress/e2e/item/trash/restore.cy.ts @@ -2,6 +2,7 @@ import { PackedRecycledItemDataFactory } from '@graasp/sdk'; import { RECYCLE_BIN_PATH } from '../../../../src/config/paths'; import { + RECYCLE_BIN_RESTORE_MANY_ITEMS_BUTTON_ID, RESTORE_ITEMS_BUTTON_CLASS, buildItemCard, } from '../../../../src/config/selectors'; @@ -9,19 +10,25 @@ import { const restoreItem = (id: string) => { cy.get(`#${buildItemCard(id)} .${RESTORE_ITEMS_BUTTON_CLASS}`).click(); }; +const restoreItems = () => { + cy.get(`#${RECYCLE_BIN_RESTORE_MANY_ITEMS_BUTTON_ID}`).click(); +}; + +const recycledItemData = [ + PackedRecycledItemDataFactory(), + PackedRecycledItemDataFactory(), +]; describe('Restore Items', () => { - it('restore one item', () => { - const recycledItemData = [ - PackedRecycledItemDataFactory(), - PackedRecycledItemDataFactory(), - ]; + beforeEach(() => { cy.setUpApi({ items: recycledItemData.map(({ item }) => item), recycledItemData, }); cy.visit(RECYCLE_BIN_PATH); + }); + it('restore one item', () => { const { id } = recycledItemData[0].item; // restore @@ -31,4 +38,18 @@ describe('Restore Items', () => { }); cy.wait('@getRecycledItems'); }); + + it('restore many items', () => { + recycledItemData.forEach(({ item }) => { + cy.selectItem(item.id); + }); + + restoreItems(); + + cy.wait('@restoreItems').then(({ request: { url } }) => { + recycledItemData.forEach(({ item }) => { + expect(url).to.contain(item.id); + }); + }); + }); }); diff --git a/cypress/e2e/item/view/viewLink.cy.ts b/cypress/e2e/item/view/viewLink.cy.ts index 23bad2ed2..c97b9e723 100644 --- a/cypress/e2e/item/view/viewLink.cy.ts +++ b/cypress/e2e/item/view/viewLink.cy.ts @@ -71,7 +71,7 @@ describe('Links', () => { cy.get(`[src="${extra.embeddedLink.thumbnails[0]}"]`); }); - it.only('view youtube', () => { + it('view youtube', () => { const { id, extra } = YOUTUBE_LINK_ITEM; cy.visit(buildItemPath(id)); diff --git a/cypress/support/commands/item.ts b/cypress/support/commands/item.ts index c31a9cce1..6329c10a8 100644 --- a/cypress/support/commands/item.ts +++ b/cypress/support/commands/item.ts @@ -1,4 +1,9 @@ -import { ItemType, getAppExtra, getDocumentExtra } from '@graasp/sdk'; +import { + DiscriminatedItem, + ItemType, + getAppExtra, + getDocumentExtra, +} from '@graasp/sdk'; import { CUSTOM_APP_CYPRESS_ID, @@ -15,6 +20,7 @@ import { SHARE_ITEM_EMAIL_INPUT_ID, SHARE_ITEM_SHARE_BUTTON_ID, TREE_MODAL_CONFIRM_BUTTON_ID, + buildFolderItemCardThumbnail, buildItemFormAppOptionId, buildItemRowArrowId, buildNavigationModalItemId, @@ -203,3 +209,7 @@ Cypress.Commands.add('dragAndDrop', (subject, x, y) => { .trigger('mouseup'); }); }); + +Cypress.Commands.add('selectItem', (id: DiscriminatedItem['id']) => { + cy.get(buildFolderItemCardThumbnail(id)).click(); +}); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 7eb6bda45..85ce30a14 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -1,5 +1,6 @@ import { AppItemExtra, + DiscriminatedItem, DocumentItemExtra, ItemType, PermissionLevel, @@ -77,6 +78,9 @@ declare global { ): void; dragAndDrop(subject: string, x: number, y: number): void; + + selectItem(id: DiscriminatedItem['id']): void; + // TODO setUpApi(args?: any): any; diff --git a/package.json b/package.json index 1a585edea..a89ba1a28 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@graasp/translations": "1.32.0", "@graasp/ui": "github:graasp/graasp-ui#card-selection", "@mui/icons-material": "5.16.4", + "@mui/lab": "5.0.0-alpha.172", "@mui/material": "5.16.4", "@sentry/react": "7.118.0", "axios": "1.7.2", diff --git a/src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx b/src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx index 34db25cde..275887de9 100644 --- a/src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx +++ b/src/components/pages/recycleBin/RecycleBinSelectionToolbar.tsx @@ -4,6 +4,10 @@ import DeleteButton from '@/components/common/DeleteButton'; import RestoreButton from '@/components/common/RestoreButton'; import { useSelectionContext } from '@/components/main/list/SelectionContext'; import SelectionToolbar from '@/components/main/list/SelectionToolbar'; +import { + RECYCLE_BIN_DELETE_MANY_ITEMS_BUTTON_ID, + RECYCLE_BIN_RESTORE_MANY_ITEMS_BUTTON_ID, +} from '@/config/selectors'; const RecycleBinSelectionToolbar = ({ items, @@ -15,8 +19,13 @@ const RecycleBinSelectionToolbar = ({ return ( <> - + selectedIds.includes(id))} onConfirm={clearSelection} /> diff --git a/src/config/selectors.ts b/src/config/selectors.ts index d481cfe17..e06fd9740 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -425,3 +425,10 @@ export const MEMBER_VALIDATION_BANNER_ID = 'memberValidationBanner'; export const MEMBER_VALIDATION_BANNER_CLOSE_BUTTON_ID = 'memberValidationBannerCloseButton'; export const ITEM_CARD_CLASS = 'item-card'; +export const buildFolderItemCardThumbnail = (id: string): string => + `#${buildItemCard(id)} .lucide-folder`; +export const RECYCLE_BIN_DELETE_MANY_ITEMS_BUTTON_ID = + 'recycleBinDeleteManyButton'; + +export const RECYCLE_BIN_RESTORE_MANY_ITEMS_BUTTON_ID = + 'recycleBinRestoreManyButton'; diff --git a/yarn.lock b/yarn.lock index 8258d3dad..722dbcb93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1939,7 +1939,7 @@ __metadata: "@graasp/ui@github:graasp/graasp-ui#card-selection": version: 4.21.0 - resolution: "@graasp/ui@https://github.com/graasp/graasp-ui.git#commit=b61d3b5ea6492d3802bceae5e92ade7a229a79de" + resolution: "@graasp/ui@https://github.com/graasp/graasp-ui.git#commit=686ce8b7aec07a5a6149a51c69280dab99741876" dependencies: "@ag-grid-community/client-side-row-model": "npm:31.3.2" "@ag-grid-community/react": "npm:^31.3.2" @@ -1973,7 +1973,7 @@ __metadata: react-router-dom: ^6.11.0 stylis: ^4.1.3 stylis-plugin-rtl: ^2.1.1 - checksum: 10/3cc25f93e2b81e3e9703c0af6910d3495effb177942707329e4264dc940f3b340e36f6078db8a7b73afbddfdfa0e3506adb6a67715c688f7cbaf95561c509adf + checksum: 10/7702e6e02528bb2979b7ec42d7341ff868c5644b75b419552e52d2c66356c71acce3883d06025d9812f2ad06c4d167bcfaea7d6851a7f032ee6eddf18c2745c3 languageName: node linkType: hard @@ -2266,6 +2266,35 @@ __metadata: languageName: node linkType: hard +"@mui/lab@npm:5.0.0-alpha.172": + version: 5.0.0-alpha.172 + resolution: "@mui/lab@npm:5.0.0-alpha.172" + dependencies: + "@babel/runtime": "npm:^7.23.9" + "@mui/base": "npm:5.0.0-beta.40" + "@mui/system": "npm:^5.16.1" + "@mui/types": "npm:^7.2.15" + "@mui/utils": "npm:^5.16.1" + clsx: "npm:^2.1.0" + prop-types: "npm:^15.8.1" + peerDependencies: + "@emotion/react": ^11.5.0 + "@emotion/styled": ^11.3.0 + "@mui/material": ">=5.15.0" + "@types/react": ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@emotion/react": + optional: true + "@emotion/styled": + optional: true + "@types/react": + optional: true + checksum: 10/8d05664c115eda2d7594fe52ad2cb8cb6ee45fe01a44d88fd30abe2848c51911c8310f08122f7a98e5cc9658e3cedc9e7ccbf31c503a838cb4abd00c0c176b7a + languageName: node + linkType: hard + "@mui/material@npm:5.15.20": version: 5.15.20 resolution: "@mui/material@npm:5.15.20" @@ -2481,7 +2510,7 @@ __metadata: languageName: node linkType: hard -"@mui/system@npm:^5.16.4": +"@mui/system@npm:^5.16.1, @mui/system@npm:^5.16.4": version: 5.16.4 resolution: "@mui/system@npm:5.16.4" dependencies: @@ -2551,7 +2580,7 @@ __metadata: languageName: node linkType: hard -"@mui/utils@npm:^5.16.4": +"@mui/utils@npm:^5.16.1, @mui/utils@npm:^5.16.4": version: 5.16.4 resolution: "@mui/utils@npm:5.16.4" dependencies: @@ -8047,6 +8076,7 @@ __metadata: "@graasp/translations": "npm:1.32.0" "@graasp/ui": "github:graasp/graasp-ui#card-selection" "@mui/icons-material": "npm:5.16.4" + "@mui/lab": "npm:5.0.0-alpha.172" "@mui/material": "npm:5.16.4" "@sentry/react": "npm:7.118.0" "@testing-library/jest-dom": "npm:^6.4.2" From 12d68c930e6f77f957a01e0ac43f5e842385569d Mon Sep 17 00:00:00 2001 From: kim Date: Mon, 22 Jul 2024 13:18:39 +0200 Subject: [PATCH 4/8] feat: allow drag move many items --- package.json | 2 +- src/components/item/FolderContent.tsx | 4 +- src/components/main/list/ItemsTable.tsx | 60 ++++++++++++++++++------ src/components/pages/home/HomeScreen.tsx | 6 ++- src/langs/en.json | 5 +- yarn.lock | 10 ++-- 6 files changed, 61 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index a89ba1a28..fd4419718 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@graasp/chatbox": "3.1.0", "@graasp/map": "1.16.0", "@graasp/query-client": "3.16.0", - "@graasp/sdk": "4.19.0", + "@graasp/sdk": "4.20.0", "@graasp/translations": "1.32.0", "@graasp/ui": "github:graasp/graasp-ui#card-selection", "@mui/icons-material": "5.16.4", diff --git a/src/components/item/FolderContent.tsx b/src/components/item/FolderContent.tsx index 5ad52b856..6f1681f0c 100644 --- a/src/components/item/FolderContent.tsx +++ b/src/components/item/FolderContent.tsx @@ -47,7 +47,8 @@ type Props = { const Content = ({ item, searchText, items, sortBy }: Props) => { const { mode } = useLayoutContext(); const { itemTypes } = useFilterItemsContext(); - const { selectedIds, toggleSelection } = useSelectionContext(); + const { selectedIds, clearSelection, toggleSelection } = + useSelectionContext(); const enableEditing = item.permission ? PermissionLevelCompare.lte(PermissionLevel.Write, item.permission) @@ -73,6 +74,7 @@ const Content = ({ item, searchText, items, sortBy }: Props) => { id={buildItemsTableId(item.id)} items={items ?? []} onCardClick={toggleSelection} + onMove={clearSelection} /> {Boolean(enableEditing && !searchText && !itemTypes?.length) && ( diff --git a/src/components/main/list/ItemsTable.tsx b/src/components/main/list/ItemsTable.tsx index ed3e479d2..d2dcea21e 100644 --- a/src/components/main/list/ItemsTable.tsx +++ b/src/components/main/list/ItemsTable.tsx @@ -8,13 +8,14 @@ import Dialog from '@mui/material/Dialog'; import DialogTitle from '@mui/material/DialogTitle'; import { DiscriminatedItem, ItemType, PackedItem } from '@graasp/sdk'; -import { COMMON } from '@graasp/translations'; +import { COMMON, FAILURE_MESSAGES } from '@graasp/translations'; import { Button, DraggingWrapper } from '@graasp/ui'; import { useBuilderTranslation, useCommonTranslation, useEnumsTranslation, + useMessagesTranslation, } from '@/config/i18n'; import { BUILDER } from '@/langs/constants'; @@ -33,6 +34,7 @@ export type ItemsTableProps = { enableMoveInBetween?: boolean; onCardClick?: (id: DiscriminatedItem['id']) => void; selectedIds?: string[]; + onMove?: () => void; }; const ItemsTable = ({ @@ -43,10 +45,12 @@ const ItemsTable = ({ enableMoveInBetween = true, selectedIds, onCardClick, + onMove, }: ItemsTableProps): JSX.Element => { const [open, setOpen] = useState(false); const { t: translateCommon } = useCommonTranslation(); const { t: translateBuilder } = useBuilderTranslation(); + const { t: translateMessage } = useMessagesTranslation(); const { t: translateEnums } = useEnumsTranslation(); const { itemId } = useParams(); @@ -59,7 +63,7 @@ const ItemsTable = ({ const { mutate: moveItems } = mutations.useMoveItems(); const { mutateAsync: uploadItems } = mutations.useUploadFiles(); const [moveData, setMoveData] = useState<{ - movedItem: PackedItem; + movedItems: PackedItem[]; to: PackedItem; }>(); @@ -71,7 +75,10 @@ const ItemsTable = ({ items: rows, }); - const onDropInRow = (movedItem: PackedItem | any, targetItem: PackedItem) => { + const onDropInRow = ( + movedItem: PackedItem | { files: File[] }, + targetItem: PackedItem, + ) => { // prevent drop in non-folder item if (targetItem.type !== ItemType.FOLDER) { toast.error( @@ -83,7 +90,7 @@ const ItemsTable = ({ } // upload files in item - if (movedItem.files) { + if ('files' in movedItem) { uploadItems({ files: movedItem.files, id: targetItem.id, @@ -95,21 +102,39 @@ const ItemsTable = ({ .catch((e) => { close(e); }); - } else if (movedItem.id !== targetItem.id) { - setOpen(true); - setMoveData({ movedItem, to: targetItem }); + return; + } + + // cannot move item into itself, or target cannot be part of selection if moving selection + if ( + movedItem.id === targetItem.id || + (selectedIds?.includes(movedItem?.id) && + selectedIds?.includes(targetItem.id)) + ) { + toast.error(translateMessage(FAILURE_MESSAGES.INVALID_MOVE_TARGET)); + return; + } + + let movedItems: PackedItem[] = []; + // use selected ids on drag move if moved item is part of selected ids + if (selectedIds?.includes(movedItem?.id)) { + movedItems = rows.filter(({ id }) => selectedIds.includes(id)); + } else if (movedItem) { + movedItems = [movedItem]; } + setMoveData({ movedItems, to: targetItem }); + setOpen(true); }; // warning: this won't work anymore with pagination! const onDropBetweenRow = ( - { files, id }: PackedItem | any, + el: PackedItem | { files: File[] }, previousItem?: PackedItem, ) => { // upload files at row - if (files) { + if ('files' in el) { uploadItems({ - files, + files: el.files, id: parentItem?.id, previousItemId: previousItem?.id, onUploadProgress: update, @@ -124,6 +149,7 @@ const ItemsTable = ({ console.error('cannot move in root'); toast.error(BUILDER.ERROR_MESSAGE); } else { + const { id } = el; setMovingId(id); reorder({ id, @@ -137,9 +163,10 @@ const ItemsTable = ({ const handleMoveItems = () => { if (moveData) { - moveItems({ items: [moveData.movedItem], to: moveData.to.id }); + moveItems({ items: moveData.movedItems, to: moveData.to.id }); setMoveData(undefined); handleClose(); + onMove?.(); } }; @@ -181,7 +208,7 @@ const ItemsTable = ({ t={translateBuilder} i18nKey={BUILDER.MOVE_CONFIRM_TITLE} values={{ - name: moveData.movedItem.name, + count: moveData.movedItems.length, targetName: moveData.to.name, }} components={{ 1: }} @@ -189,9 +216,12 @@ const ItemsTable = ({ - {translateBuilder(BUILDER.MOVE_WARNING, { - name: moveData.movedItem.name, - })} + {translateBuilder(BUILDER.MOVE_WARNING)} +
    + {moveData.movedItems.map(({ name, id }) => ( +
  • {name}
  • + ))} +
) : ( diff --git a/src/components/pages/home/HomeScreen.tsx b/src/components/pages/home/HomeScreen.tsx index d594571b2..31201a23d 100644 --- a/src/components/pages/home/HomeScreen.tsx +++ b/src/components/pages/home/HomeScreen.tsx @@ -44,7 +44,8 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => { const { itemTypes } = useFilterItemsContext(); const [showOnlyMe, setShowOnlyMe] = useState(false); - const { selectedIds, toggleSelection } = useSelectionContext(); + const { selectedIds, toggleSelection, clearSelection } = + useSelectionContext(); const { mode } = useLayoutContext(); const { sortBy, setSortBy, ordering, setOrdering } = useSorting({ @@ -80,7 +81,7 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => { ); } - if (data && data.pages.length) { + if (data?.pages?.length) { // default show upload zone let content = ( @@ -100,6 +101,7 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => { enableMoveInBetween={false} onCardClick={toggleSelection} selectedIds={selectedIds} + onMove={clearSelection} /> {!isFetching && data.pages[0].totalCount > totalFetchedItems && ( diff --git a/src/langs/en.json b/src/langs/en.json index 40081c78b..a991c3f60 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -459,9 +459,10 @@ "BOOKMARKS_NO_ITEM": "No bookmarked item", "BOOKMARKS_NO_ITEM_SEARCH": "No bookmarked item for {{search}}", "UPLOAD_BETWEEN_FILES": "Upload your file(s) here", - "MOVE_WARNING": "This operation might give access to {{name}} to more persons than previously. Do you want to proceed?", + "MOVE_WARNING": "This operation might give access to more persons than previously to the items below. Do you want to proceed?", "MOVE_IN_NON_FOLDER_ERROR_MESSAGE": "Cannot add items in {{type}}", - "MOVE_CONFIRM_TITLE": "Confirm moving <1>{{name}} inside <1>{{targetName}}", + "MOVE_CONFIRM_TITLE_one": "Confirm moving one item inside <1>{{targetName}}?", + "MOVE_CONFIRM_TITLE": "Confirm moving <1>{{count}} items inside <1>{{targetName}}?", "ITEM_SEARCH_NOTHING_FOUND": "No item found with these parameters", "ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE": "search", "ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE": "types", diff --git a/yarn.lock b/yarn.lock index 722dbcb93..cf1533cc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1889,9 +1889,9 @@ __metadata: languageName: node linkType: hard -"@graasp/sdk@npm:4.19.0": - version: 4.19.0 - resolution: "@graasp/sdk@npm:4.19.0" +"@graasp/sdk@npm:4.20.0": + version: 4.20.0 + resolution: "@graasp/sdk@npm:4.20.0" dependencies: "@faker-js/faker": "npm:8.4.1" filesize: "npm:10.1.4" @@ -1900,7 +1900,7 @@ __metadata: peerDependencies: date-fns: ^3 uuid: ^9 || ^10.0.0 - checksum: 10/d2fb382f5669ab1b8e265612208d6766672be8b6fa833fe1af8e4f17d2c3f7f384e4bf3f5fd004ec76c8c1ab1ad382c37730477e90ec1ae85b3079ca6a8e68aa + checksum: 10/81e336d68094bcd234b4983217ce9f05d41501032fac00810bf927b8289ef933e18cdfb0e1b0275df6af5a7683b54a8ebf509134aa95b4c7ecfce4a5f313752d languageName: node linkType: hard @@ -8072,7 +8072,7 @@ __metadata: "@graasp/chatbox": "npm:3.1.0" "@graasp/map": "npm:1.16.0" "@graasp/query-client": "npm:3.16.0" - "@graasp/sdk": "npm:4.19.0" + "@graasp/sdk": "npm:4.20.0" "@graasp/translations": "npm:1.32.0" "@graasp/ui": "github:graasp/graasp-ui#card-selection" "@mui/icons-material": "npm:5.16.4" From 2e9d1aec8004836ce9a56090ffc281074889afda Mon Sep 17 00:00:00 2001 From: kim Date: Mon, 22 Jul 2024 14:23:21 +0200 Subject: [PATCH 5/8] revert: set back thumbnail --- src/components/table/ItemCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/table/ItemCard.tsx b/src/components/table/ItemCard.tsx index b7d0ddef0..d6d7a62e6 100644 --- a/src/components/table/ItemCard.tsx +++ b/src/components/table/ItemCard.tsx @@ -104,7 +104,7 @@ const ItemCard = ({ sx={{ background: disabled ? 'lightgrey' : undefined }} dense={dense} elevation={false} - thumbnail={thumbnailUrl} + thumbnail={thumbnail} to={allowNavigation ? to : undefined} name={item.name} alt={item.name} From 59ecff07f4c81ed46a6d1dfa5da8dde2f1f77a20 Mon Sep 17 00:00:00 2001 From: kim Date: Wed, 24 Jul 2024 10:41:12 +0200 Subject: [PATCH 6/8] refactor: apply PR requested changes --- cypress/e2e/item/bookmarks/bookmarks.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/item/bookmarks/bookmarks.cy.ts b/cypress/e2e/item/bookmarks/bookmarks.cy.ts index 3b4f7a9cd..95bd4a796 100644 --- a/cypress/e2e/item/bookmarks/bookmarks.cy.ts +++ b/cypress/e2e/item/bookmarks/bookmarks.cy.ts @@ -83,7 +83,7 @@ describe('Bookmarked Item', () => { } }); - it.only('Add item to bookmarks', () => { + it('Add item to bookmarks', () => { cy.visit(HOME_PATH); const item = NON_BOOKMARKED_ITEM; From e7fdcbe36e25fd9ec8afd7ee3141b32800144f4f Mon Sep 17 00:00:00 2001 From: kim Date: Wed, 24 Jul 2024 11:42:48 +0200 Subject: [PATCH 7/8] refactor: apply PR requested changes --- cypress/e2e/item/copy/copy.cy.ts | 3 +- cypress/e2e/item/move/move.cy.ts | 3 +- package.json | 2 +- src/components/item/FolderContent.tsx | 9 +- src/components/main/DeleteItemDialog.tsx | 14 +- src/components/main/list/ItemsTable.tsx | 15 +- src/components/pages/home/HomeScreen.tsx | 9 +- src/config/selectors.ts | 2 + yarn.lock | 179 +++++++++++++++++++---- 9 files changed, 184 insertions(+), 52 deletions(-) diff --git a/cypress/e2e/item/copy/copy.cy.ts b/cypress/e2e/item/copy/copy.cy.ts index bd0fab642..2a7afac86 100644 --- a/cypress/e2e/item/copy/copy.cy.ts +++ b/cypress/e2e/item/copy/copy.cy.ts @@ -5,6 +5,7 @@ import { import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { + COPY_MANY_ITEMS_BUTTON_SELECTOR, ITEM_MENU_COPY_BUTTON_CLASS, MY_GRAASP_ITEM_PATH, buildItemCard, @@ -18,7 +19,7 @@ const copyItems = ({ toItemPath: string; rootId?: string; }) => { - cy.get(`[data-testid="FilterNoneIcon"]`).click(); + cy.get(COPY_MANY_ITEMS_BUTTON_SELECTOR).click(); cy.handleTreeMenu(toItemPath, rootId); }; diff --git a/cypress/e2e/item/move/move.cy.ts b/cypress/e2e/item/move/move.cy.ts index 8f8c5abc1..8d3dd7a1a 100644 --- a/cypress/e2e/item/move/move.cy.ts +++ b/cypress/e2e/item/move/move.cy.ts @@ -6,6 +6,7 @@ import { import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { ITEM_MENU_MOVE_BUTTON_CLASS, + MOVE_MANY_ITEMS_BUTTON_SELECTOR, MY_GRAASP_ITEM_PATH, buildItemsGridMoreButtonSelector, buildNavigationModalItemId, @@ -31,7 +32,7 @@ const moveItems = ({ toItemPath: string; rootId?: string; }) => { - cy.get(`[data-testid="OpenWithIcon"]`).click(); + cy.get(MOVE_MANY_ITEMS_BUTTON_SELECTOR).click(); cy.handleTreeMenu(toItemPath, rootId); }; diff --git a/package.json b/package.json index fd4419718..8be63fd8a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@graasp/query-client": "3.16.0", "@graasp/sdk": "4.20.0", "@graasp/translations": "1.32.0", - "@graasp/ui": "github:graasp/graasp-ui#card-selection", + "@graasp/ui": "4.22.0", "@mui/icons-material": "5.16.4", "@mui/lab": "5.0.0-alpha.172", "@mui/material": "5.16.4", diff --git a/src/components/item/FolderContent.tsx b/src/components/item/FolderContent.tsx index 6f1681f0c..cbe7503e2 100644 --- a/src/components/item/FolderContent.tsx +++ b/src/components/item/FolderContent.tsx @@ -144,6 +144,10 @@ const FolderContent = ({ item }: { item: PackedItem }): JSX.Element => { ) .sort(sortFn); + const sortingOptions = Object.values(SortingOptionsForFolder).sort((t1, t2) => + translateEnums(t1).localeCompare(translateEnums(t2)), + ); + if (children) { return ( <> @@ -195,10 +199,7 @@ const FolderContent = ({ item }: { item: PackedItem }): JSX.Element => { ordering={ordering} sortBy={sortBy} setSortBy={setSortBy} - options={Object.values(SortingOptionsForFolder).sort( - (t1, t2) => - translateEnums(t1).localeCompare(translateEnums(t2)), - )} + options={sortingOptions} setOrdering={setOrdering} /> )} diff --git a/src/components/main/DeleteItemDialog.tsx b/src/components/main/DeleteItemDialog.tsx index c9fccfdb2..c4b46dff4 100644 --- a/src/components/main/DeleteItemDialog.tsx +++ b/src/components/main/DeleteItemDialog.tsx @@ -43,6 +43,10 @@ const DeleteItemDialog = ({ handleClose(); }; + const names = items + .sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1)) + .map(({ name }) =>
  • {name}
  • ); + return ( - {items - .sort((a, b) => - a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1, - ) - .map(({ name }) => ( -
  • {name}
  • - ))} - +
      {names}
    diff --git a/src/components/main/list/ItemsTable.tsx b/src/components/main/list/ItemsTable.tsx index d2dcea21e..a00ea3749 100644 --- a/src/components/main/list/ItemsTable.tsx +++ b/src/components/main/list/ItemsTable.tsx @@ -43,7 +43,7 @@ const ItemsTable = ({ showThumbnails = true, canMove = true, enableMoveInBetween = true, - selectedIds, + selectedIds = [], onCardClick, onMove, }: ItemsTableProps): JSX.Element => { @@ -108,8 +108,8 @@ const ItemsTable = ({ // cannot move item into itself, or target cannot be part of selection if moving selection if ( movedItem.id === targetItem.id || - (selectedIds?.includes(movedItem?.id) && - selectedIds?.includes(targetItem.id)) + (selectedIds.includes(movedItem?.id) && + selectedIds.includes(targetItem.id)) ) { toast.error(translateMessage(FAILURE_MESSAGES.INVALID_MOVE_TARGET)); return; @@ -117,7 +117,7 @@ const ItemsTable = ({ let movedItems: PackedItem[] = []; // use selected ids on drag move if moved item is part of selected ids - if (selectedIds?.includes(movedItem?.id)) { + if (selectedIds.includes(movedItem?.id)) { movedItems = rows.filter(({ id }) => selectedIds.includes(id)); } else if (movedItem) { movedItems = [movedItem]; @@ -170,6 +170,9 @@ const ItemsTable = ({ } }; + const isSelected = (droppedEl: PackedItem | { files: File[] }): boolean => + 'id' in droppedEl ? selectedIds.includes(droppedEl.id) : false; + return ( <> { if ('id' in droppedEl) { onCardClick?.(droppedEl.id); diff --git a/src/components/pages/home/HomeScreen.tsx b/src/components/pages/home/HomeScreen.tsx index 31201a23d..f7381d1a4 100644 --- a/src/components/pages/home/HomeScreen.tsx +++ b/src/components/pages/home/HomeScreen.tsx @@ -88,6 +88,7 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => { } />
    ); + if (data.pages[0].data.length) { const totalFetchedItems = data ? data.pages.map(({ data: d }) => d.length).reduce((a, b) => a + b, 0) @@ -122,6 +123,10 @@ const HomeScreenContent = ({ searchText }: { searchText: string }) => { content = ; } + const sortingOptions = Object.values(SortingOptions).sort((t1, t2) => + translateEnums(t1).localeCompare(translateEnums(t2)), + ); + return ( <> { setSortBy={setSortBy} ordering={ordering} setOrdering={setOrdering} - options={Object.values(SortingOptions).sort((t1, t2) => - translateEnums(t1).localeCompare(translateEnums(t2)), - )} + options={sortingOptions} /> )} diff --git a/src/config/selectors.ts b/src/config/selectors.ts index e06fd9740..4cd767e1b 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -432,3 +432,5 @@ export const RECYCLE_BIN_DELETE_MANY_ITEMS_BUTTON_ID = export const RECYCLE_BIN_RESTORE_MANY_ITEMS_BUTTON_ID = 'recycleBinRestoreManyButton'; +export const COPY_MANY_ITEMS_BUTTON_SELECTOR = `[data-testid="FilterNoneIcon"]`; +export const MOVE_MANY_ITEMS_BUTTON_SELECTOR = `[data-testid="OpenWithIcon"]`; diff --git a/yarn.lock b/yarn.lock index cf1533cc8..b9dfd5303 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1937,24 +1937,25 @@ __metadata: languageName: node linkType: hard -"@graasp/ui@github:graasp/graasp-ui#card-selection": - version: 4.21.0 - resolution: "@graasp/ui@https://github.com/graasp/graasp-ui.git#commit=686ce8b7aec07a5a6149a51c69280dab99741876" +"@graasp/ui@npm:4.20.0": + version: 4.20.0 + resolution: "@graasp/ui@npm:4.20.0" dependencies: "@ag-grid-community/client-side-row-model": "npm:31.3.2" - "@ag-grid-community/react": "npm:^31.3.2" - "@ag-grid-community/styles": "npm:^31.3.2" - "@storybook/react-vite": "npm:8.1.11" + "@ag-grid-community/react": "npm:^31.3.1" + "@ag-grid-community/styles": "npm:^31.3.1" + "@storybook/react-vite": "npm:8.1.10" http-status-codes: "npm:2.3.0" interweave: "npm:13.1.0" - katex: "npm:0.16.11" + katex: "npm:0.16.10" lodash.truncate: "npm:4.4.2" - lucide-react: "npm:0.408.0" + lucide-react: "npm:0.395.0" react-cookie-consent: "npm:9.0.0" react-dnd: "npm:16.0.1" react-dnd-html5-backend: "npm:16.0.1" react-quill: "npm:2.0.0" react-rnd: "npm:10.4.11" + react-text-mask: "npm:5.5.0" uuid: "npm:10.0.0" vitest: "npm:1.6.0" peerDependencies: @@ -1963,9 +1964,9 @@ __metadata: "@emotion/styled": ~11.10.6 || ~11.11.0 "@graasp/sdk": ^4.14.0 "@graasp/translations": ^1.23.0 - "@mui/icons-material": ~5.14.0 || ~5.15.0 || ~5.16.0 + "@mui/icons-material": ~5.14.0 || ~5.15.0 "@mui/lab": ~5.0.0-alpha.150 - "@mui/material": ~5.14.0 || ~5.15.0 || ~5.16.0 + "@mui/material": ~5.14.0 || ~5.15.0 i18next: ^22.4.15 || ^23.0.0 react: ^18.0.0 react-dom: ^18.0.0 @@ -1973,40 +1974,39 @@ __metadata: react-router-dom: ^6.11.0 stylis: ^4.1.3 stylis-plugin-rtl: ^2.1.1 - checksum: 10/7702e6e02528bb2979b7ec42d7341ff868c5644b75b419552e52d2c66356c71acce3883d06025d9812f2ad06c4d167bcfaea7d6851a7f032ee6eddf18c2745c3 + checksum: 10/2259b603ddffb00187f5684215a9e16e91a192612a84f7807759f385b7121fe198bdb20ae17fc978b1996a0b921dd2381283f22a3a84ad40c3b8a0031efdaf05 languageName: node linkType: hard -"@graasp/ui@npm:4.20.0": - version: 4.20.0 - resolution: "@graasp/ui@npm:4.20.0" +"@graasp/ui@npm:4.22.0": + version: 4.22.0 + resolution: "@graasp/ui@npm:4.22.0" dependencies: "@ag-grid-community/client-side-row-model": "npm:31.3.2" - "@ag-grid-community/react": "npm:^31.3.1" - "@ag-grid-community/styles": "npm:^31.3.1" - "@storybook/react-vite": "npm:8.1.10" + "@ag-grid-community/react": "npm:^31.3.2" + "@ag-grid-community/styles": "npm:^31.3.2" + "@storybook/react-vite": "npm:8.1.11" http-status-codes: "npm:2.3.0" interweave: "npm:13.1.0" - katex: "npm:0.16.10" + katex: "npm:0.16.11" lodash.truncate: "npm:4.4.2" - lucide-react: "npm:0.395.0" + lucide-react: "npm:0.408.0" react-cookie-consent: "npm:9.0.0" react-dnd: "npm:16.0.1" react-dnd-html5-backend: "npm:16.0.1" react-quill: "npm:2.0.0" react-rnd: "npm:10.4.11" - react-text-mask: "npm:5.5.0" uuid: "npm:10.0.0" - vitest: "npm:1.6.0" + vitest: "npm:2.0.2" peerDependencies: "@emotion/cache": ~11.10.7 || ~11.11.0 "@emotion/react": ~11.10.6 || ~11.11.0 "@emotion/styled": ~11.10.6 || ~11.11.0 "@graasp/sdk": ^4.14.0 "@graasp/translations": ^1.23.0 - "@mui/icons-material": ~5.14.0 || ~5.15.0 + "@mui/icons-material": ~5.14.0 || ~5.15.0 || ~5.16.0 "@mui/lab": ~5.0.0-alpha.150 - "@mui/material": ~5.14.0 || ~5.15.0 + "@mui/material": ~5.14.0 || ~5.15.0 || ~5.16.0 i18next: ^22.4.15 || ^23.0.0 react: ^18.0.0 react-dom: ^18.0.0 @@ -2014,7 +2014,7 @@ __metadata: react-router-dom: ^6.11.0 stylis: ^4.1.3 stylis-plugin-rtl: ^2.1.1 - checksum: 10/2259b603ddffb00187f5684215a9e16e91a192612a84f7807759f385b7121fe198bdb20ae17fc978b1996a0b921dd2381283f22a3a84ad40c3b8a0031efdaf05 + checksum: 10/af8bcd06fd743d84739bf1c20aaceca67321c12456bcd905e7233d50c51c3fed25da41c835f8e9d1543a26a46f1da851c45222763dcbc7ad884cf0abb61b63d3 languageName: node linkType: hard @@ -4341,6 +4341,18 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/expect@npm:2.0.2" + dependencies: + "@vitest/spy": "npm:2.0.2" + "@vitest/utils": "npm:2.0.2" + chai: "npm:^5.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10/67ebe5dcc083cbaf152fa1845da5ab4cd5a37fcc8657caaec214878c145516cf270998ad300ab9c3e7d8b4fc9ab41cbc4606af3341ae06d08c5cf44354ba5a56 + languageName: node + linkType: hard + "@vitest/expect@npm:2.0.4": version: 2.0.4 resolution: "@vitest/expect@npm:2.0.4" @@ -4353,7 +4365,16 @@ __metadata: languageName: node linkType: hard -"@vitest/pretty-format@npm:2.0.4, @vitest/pretty-format@npm:^2.0.4": +"@vitest/pretty-format@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/pretty-format@npm:2.0.2" + dependencies: + tinyrainbow: "npm:^1.2.0" + checksum: 10/30ae021ea3b36271e00aac5a49084de9403900ae574b1ce1c26385ee792a7fed700f2deb2cd841b64724a4e428e908a5d3ffc1b4e6ca83daa351d76de925e9a6 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:2.0.4, @vitest/pretty-format@npm:^2.0.2, @vitest/pretty-format@npm:^2.0.4": version: 2.0.4 resolution: "@vitest/pretty-format@npm:2.0.4" dependencies: @@ -4373,6 +4394,16 @@ __metadata: languageName: node linkType: hard +"@vitest/runner@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/runner@npm:2.0.2" + dependencies: + "@vitest/utils": "npm:2.0.2" + pathe: "npm:^1.1.2" + checksum: 10/f3f9f15b5a3d0b5fe5815ed0ad04bd3fceab0768c441baf20931d78f2599261c172724955e9de35020ff79950e1fd5398d0d5aad2c5ee8a91e4cc2b85943ac81 + languageName: node + linkType: hard + "@vitest/runner@npm:2.0.4": version: 2.0.4 resolution: "@vitest/runner@npm:2.0.4" @@ -4394,6 +4425,17 @@ __metadata: languageName: node linkType: hard +"@vitest/snapshot@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/snapshot@npm:2.0.2" + dependencies: + "@vitest/pretty-format": "npm:2.0.2" + magic-string: "npm:^0.30.10" + pathe: "npm:^1.1.2" + checksum: 10/c0d41c3ff71ada909b34a8cbfe4ae9d59126fdae243b89e4eba5110db8eeb41234897159de20050a18aac2cbb7694e3fddd94bf7c79c1e9b169f1f4cf642bf07 + languageName: node + linkType: hard + "@vitest/snapshot@npm:2.0.4": version: 2.0.4 resolution: "@vitest/snapshot@npm:2.0.4" @@ -4414,6 +4456,15 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/spy@npm:2.0.2" + dependencies: + tinyspy: "npm:^3.0.0" + checksum: 10/feca3d26b824350d2f4f11a1e5881f1c7eeba5b903399ee8fbc2aceb4bf4201da61088783cf56bd5a2850b3e2380905f69128106655d7d849c62c52861b5af1a + languageName: node + linkType: hard + "@vitest/spy@npm:2.0.4": version: 2.0.4 resolution: "@vitest/spy@npm:2.0.4" @@ -4435,6 +4486,18 @@ __metadata: languageName: node linkType: hard +"@vitest/utils@npm:2.0.2": + version: 2.0.2 + resolution: "@vitest/utils@npm:2.0.2" + dependencies: + "@vitest/pretty-format": "npm:2.0.2" + estree-walker: "npm:^3.0.3" + loupe: "npm:^3.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10/771a1579c9d11bf02ed5d641619bdb9ee06f4096a2965183298c8610476316f899561dabf48e589eecccd76c75155131dc7a90d98d7519e07483b7ed09e0a5b9 + languageName: node + linkType: hard + "@vitest/utils@npm:2.0.4": version: 2.0.4 resolution: "@vitest/utils@npm:2.0.4" @@ -8074,7 +8137,7 @@ __metadata: "@graasp/query-client": "npm:3.16.0" "@graasp/sdk": "npm:4.20.0" "@graasp/translations": "npm:1.32.0" - "@graasp/ui": "github:graasp/graasp-ui#card-selection" + "@graasp/ui": "npm:4.22.0" "@mui/icons-material": "npm:5.16.4" "@mui/lab": "npm:5.0.0-alpha.172" "@mui/material": "npm:5.16.4" @@ -14144,6 +14207,21 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:2.0.2": + version: 2.0.2 + resolution: "vite-node@npm:2.0.2" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.3.5" + pathe: "npm:^1.1.2" + tinyrainbow: "npm:^1.2.0" + vite: "npm:^5.0.0" + bin: + vite-node: vite-node.mjs + checksum: 10/9335168dc5a20c1d0c6b53cf20f098875c7556b0eb1e1ae871aedcc796edd5906f06ab259d9b57ec12719041838cac8186e54e597c0012ee77b03a4e2be84722 + languageName: node + linkType: hard + "vite-node@npm:2.0.4": version: 2.0.4 resolution: "vite-node@npm:2.0.4" @@ -14357,6 +14435,55 @@ __metadata: languageName: node linkType: hard +"vitest@npm:2.0.2": + version: 2.0.2 + resolution: "vitest@npm:2.0.2" + dependencies: + "@ampproject/remapping": "npm:^2.3.0" + "@vitest/expect": "npm:2.0.2" + "@vitest/pretty-format": "npm:^2.0.2" + "@vitest/runner": "npm:2.0.2" + "@vitest/snapshot": "npm:2.0.2" + "@vitest/spy": "npm:2.0.2" + "@vitest/utils": "npm:2.0.2" + chai: "npm:^5.1.1" + debug: "npm:^4.3.5" + execa: "npm:^8.0.1" + magic-string: "npm:^0.30.10" + pathe: "npm:^1.1.2" + std-env: "npm:^3.7.0" + tinybench: "npm:^2.8.0" + tinypool: "npm:^1.0.0" + tinyrainbow: "npm:^1.2.0" + vite: "npm:^5.0.0" + vite-node: "npm:2.0.2" + why-is-node-running: "npm:^2.2.2" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 2.0.2 + "@vitest/ui": 2.0.2 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10/d92053b0d6e3e800d56cbe5eb860625fb9d50e66857da189ac19a68e511bbb0c59baf6a6b3a8ecb0b46c011567723e16e550136655e93767f228fb91caf4e16f + languageName: node + linkType: hard + "vitest@npm:2.0.4": version: 2.0.4 resolution: "vitest@npm:2.0.4" From eca1a5311954d5c4e50152d7fb3c92fe3006e4a8 Mon Sep 17 00:00:00 2001 From: kim Date: Wed, 24 Jul 2024 13:06:34 +0200 Subject: [PATCH 8/8] refactor: fix translations --- src/langs/de.json | 1 - src/langs/es.json | 1 - src/langs/fr.json | 3 ++- src/langs/it.json | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/langs/de.json b/src/langs/de.json index 2ff7d45bd..185ffe51b 100644 --- a/src/langs/de.json +++ b/src/langs/de.json @@ -461,7 +461,6 @@ "UPLOAD_BETWEEN_FILES": "Laden Sie Ihre Datei(en) hier hoch", "MOVE_WARNING": "Dieser Vorgang könnte mehr Personen als zuvor Zugriff auf {{name}} gewähren. Möchten Sie fortfahren?", "MOVE_IN_NON_FOLDER_ERROR_MESSAGE": "Elemente in {{type}} können nicht hinzugefügt werden", - "MOVE_CONFIRM_TITLE": "Bestätigen Sie das Verschieben von <1>{{name}} innerhalb von <1>{{targetName}}", "ITEM_SEARCH_NOTHING_FOUND": "Kein Artikel mit diesen Parametern gefunden", "ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE": "suchen", "ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE": "Typen", diff --git a/src/langs/es.json b/src/langs/es.json index f9211bf76..8b8ffbd4e 100644 --- a/src/langs/es.json +++ b/src/langs/es.json @@ -457,7 +457,6 @@ "UPLOAD_BETWEEN_FILES": "Sube tu(s) archivo(s) aquí", "MOVE_WARNING": "Esta operación podría dar acceso a {{name}} a más personas que antes. Quieres proceder?", "MOVE_IN_NON_FOLDER_ERROR_MESSAGE": "No se pueden agregar elementos en {{type}}", - "MOVE_CONFIRM_TITLE": "Confirma mover <1>{{name}} dentro de <1>{{targetName}}", "ITEM_SEARCH_NOTHING_FOUND": "No se encontró ningún artículo con estos parámetros.", "ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE": "buscar", "ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE": "tipos", diff --git a/src/langs/fr.json b/src/langs/fr.json index aa28a7067..7fcaa5125 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -461,7 +461,8 @@ "UPLOAD_BETWEEN_FILES": "Téléchargez votre (vos) fichier(s) ici", "MOVE_WARNING": "Cette opération pourrait donner accès à {{name}} à plus de personnes que précédemment. Voulez-vous poursuivre?", "MOVE_IN_NON_FOLDER_ERROR_MESSAGE": "Impossible d'ajouter des éléments dans {{type}}", - "MOVE_CONFIRM_TITLE": "Confirmez le déplacement de <1>{{name}} à l'intérieur de <1>{{targetName}}", + "MOVE_CONFIRM_TITLE_one": "Confirmez le déplacement d'un élément à l'intérieur de <1>{{targetName}}", + "MOVE_CONFIRM_TITLE": "Confirmez le déplacement de <1>{{count}} éléments à l'intérieur de <1>{{targetName}}", "ITEM_SEARCH_NOTHING_FOUND": "Aucun élément trouvé avec ces paramètres", "ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE": "recherche", "ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE": "les types", diff --git a/src/langs/it.json b/src/langs/it.json index 31cbbf882..433cda578 100644 --- a/src/langs/it.json +++ b/src/langs/it.json @@ -457,7 +457,6 @@ "UPLOAD_BETWEEN_FILES": "Carica i tuoi file qui", "MOVE_WARNING": "Questa operazione potrebbe consentire l'accesso a {{name}} a più persone rispetto a prima. Vuoi procedere?", "MOVE_IN_NON_FOLDER_ERROR_MESSAGE": "Impossibile aggiungere elementi in {{type}}", - "MOVE_CONFIRM_TITLE": "Conferma lo spostamento di <1>{{name}} all'interno di <1>{{targetName}}", "ITEM_SEARCH_NOTHING_FOUND": "Nessun elemento trovato con questi parametri", "ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE": "ricerca", "ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE": "tipi",