From 57da3f3986d05149e839ca3906c7f15afc047e30 Mon Sep 17 00:00:00 2001 From: Kim Lan Phan Hoang Date: Fri, 12 Jul 2024 17:01:36 +0200 Subject: [PATCH] feat: improve table and views (#1215) * feat: allow drag and drop for list item * refactor: change new button layout * refactor: fix tests * refactor: fix small layout issues * refactor: fix small layout issues * refactor: fix small layout issues * refactor: set back packages --- cypress/e2e/item/bookmarks/bookmarks.cy.ts | 97 ++- .../copy/{gridCopyItem.cy.ts => copy.cy.ts} | 16 +- cypress/e2e/item/copy/listCopyItem.cy.ts | 94 --- cypress/e2e/item/copy/listCopyMultiple.cy.ts | 108 --- cypress/e2e/item/create/createApp.cy.ts | 18 +- cypress/e2e/item/create/createDocument.cy.ts | 14 +- cypress/e2e/item/create/createFolder.cy.ts | 138 ++-- cypress/e2e/item/create/createLink.cy.ts | 19 +- cypress/e2e/item/create/createShortcut.cy.ts | 117 +--- cypress/e2e/item/create/importH5p.cy.ts | 18 +- cypress/e2e/item/create/importZip.cy.ts | 7 - cypress/e2e/item/delete/gridRecycleItem.cy.ts | 50 -- cypress/e2e/item/delete/listDeleteItems.cy.ts | 43 -- .../e2e/item/delete/listRecycleItems.cy.ts | 50 -- cypress/e2e/item/delete/listRestoreItem.cy.ts | 73 -- cypress/e2e/item/download/downloadItem.cy.ts | 2 - .../e2e/item/duplicate/duplicateItem.cy.ts | 67 +- cypress/e2e/item/edit/editApp.cy.ts | 195 ++---- cypress/e2e/item/edit/editDocument.cy.ts | 178 ++--- cypress/e2e/item/edit/editEtherpad.cy.ts | 81 +-- cypress/e2e/item/edit/editFile.cy.ts | 80 +-- cypress/e2e/item/edit/editFolder.cy.ts | 224 ++----- cypress/e2e/item/edit/editH5p.cy.ts | 81 +-- cypress/e2e/item/edit/editLink.cy.ts | 87 +-- cypress/e2e/item/edit/editShortcut.cy.ts | 81 +-- cypress/e2e/item/flag/flagItem.cy.ts | 15 +- cypress/e2e/item/hide/hideItem.cy.ts | 129 +--- cypress/e2e/item/home/home.cy.ts | 389 ++++------- cypress/e2e/item/home/layoutMode.cy.ts | 53 ++ cypress/e2e/item/move/gridMoveItem.cy.ts | 89 --- cypress/e2e/item/move/listMoveMultiple.cy.ts | 110 --- .../{listMoveItem.cy.ts => moveItem.cy.ts} | 22 +- cypress/e2e/item/order/reorderItems.cy.ts | 105 +-- cypress/e2e/item/pin/pinItem.cy.ts | 117 ++-- cypress/e2e/item/publish/viewPublished.cy.ts | 116 ++++ cypress/e2e/item/settings/itemSettings.cy.ts | 22 +- cypress/e2e/item/share/itemLogin.cy.ts | 39 +- .../deleteItem.cy.ts} | 11 +- .../recycleItem.cy.ts} | 18 +- cypress/e2e/item/trash/restoreItem.cy.ts | 34 + cypress/e2e/item/trash/viewTrash.cy.ts | 112 ++++ cypress/e2e/item/upload/dropzoneUpload.cy.ts | 8 +- cypress/e2e/item/view/viewFile.cy.ts | 37 +- cypress/e2e/item/view/viewFolder.cy.ts | 208 +++--- cypress/e2e/item/view/viewThumbnails.cy.ts | 15 - cypress/fixtures/chatbox.ts | 2 +- cypress/fixtures/items.ts | 4 +- cypress/support/actionsUtils.ts | 10 +- cypress/support/commands.ts | 42 +- cypress/support/commands/item.ts | 3 - cypress/support/commands/navigation.ts | 14 +- cypress/support/constants.ts | 3 - cypress/support/editUtils.ts | 23 +- cypress/support/index.ts | 3 +- cypress/support/server.ts | 48 +- package.json | 10 +- src/components/App.tsx | 2 +- src/components/Root.tsx | 4 +- src/components/common/BookmarkButton.tsx | 2 - src/components/common/DuplicateButton.tsx | 49 ++ src/components/common/EditButton.tsx | 82 --- src/components/common/FlagButton.tsx | 32 + src/components/common/MoveButton.tsx | 124 ---- src/components/common/SelectTypes.tsx | 5 +- src/components/file/FileUploader.tsx | 8 +- src/components/file/FileUploaderOverlay.tsx | 117 ---- src/components/hooks/uploadWithProgress.ts | 50 ++ src/components/hooks/uploadWithProgress.tsx | 63 -- src/components/item/FolderContent.tsx | 220 ++++++ src/components/item/ItemContent.tsx | 81 +-- src/components/item/ItemMain.tsx | 15 +- src/components/item/ItemPanel.tsx | 4 +- src/components/item/MapView.tsx | 77 +-- src/components/item/copy/CopyButton.tsx | 37 ++ src/components/item/copy/CopyModal.tsx | 49 ++ src/components/item/edit/EditButton.tsx | 35 + .../EditModal.tsx} | 54 +- src/components/item/form/BaseItemForm.tsx | 2 +- src/components/item/form/DocumentForm.tsx | 2 +- src/components/item/form/FileForm.tsx | 2 +- src/components/item/form/NameForm.tsx | 2 +- src/components/item/header/Actions.tsx | 126 ++++ src/components/item/header/ItemHeader.tsx | 43 +- .../item/header/ItemHeaderActions.tsx | 106 +-- src/components/item/header/ModeButton.tsx | 13 +- src/components/item/move/MoveButton.tsx | 43 ++ src/components/item/move/MoveModal.tsx | 79 +++ .../item/shortcut/CreateShortcutButton.tsx | 25 + .../item/shortcut/CreateShortcutModal.tsx | 76 +++ src/components/main/CopyButton.tsx | 95 --- src/components/main/CreateShortcutButton.tsx | 103 --- src/components/main/EmptyItem.tsx | 22 - src/components/main/ImportH5P.tsx | 17 +- src/components/main/ImportZip.tsx | 8 +- src/components/main/ItemActions.tsx | 34 - src/components/main/ItemCard.tsx | 144 ---- src/components/main/ItemMenu.tsx | 182 ----- src/components/main/ItemMenuContent.tsx | 205 ++++++ src/components/main/Items.tsx | 158 ----- src/components/main/ItemsGrid.tsx | 123 ---- src/components/main/ItemsTable.tsx | 442 +++++------- src/components/main/ItemsTableCard.tsx | 83 +++ src/components/main/ItemsToolbar.tsx | 61 -- src/components/main/NewItemButton.tsx | 47 +- src/components/main/NewItemModal.tsx | 15 +- src/components/map/DesktopMap.tsx | 34 + src/components/map/useCurrentLocation.tsx | 76 +++ .../pages/BookmarkedItemsScreen.tsx | 159 +++-- src/components/pages/HomeScreen.tsx | 148 ----- src/components/pages/NoItemFilters.tsx | 36 + src/components/pages/PageWrapper.tsx | 34 + src/components/pages/PublishedItemsScreen.tsx | 135 +++- src/components/pages/RecycledItemsScreen.tsx | 182 +++-- src/components/pages/home/HomeScreen.tsx | 200 ++++++ .../pages/home/HomeScreenLoading.tsx | 19 + .../pages/item/ItemLoginWrapper.tsx | 2 - src/components/table/ActionsCellRenderer.tsx | 60 -- .../{BadgesCellRenderer.tsx => Badges.tsx} | 68 +- src/components/table/ItemActions.tsx | 20 + src/components/table/ItemCard.tsx | 101 +++ src/components/table/ItemNameCellRenderer.tsx | 65 -- src/components/table/ItemThumbnail.tsx | 47 ++ src/components/table/ShowOnlyMeButton.tsx | 30 + src/components/table/SortingSelect.tsx | 106 +++ src/components/table/types.ts | 19 + src/components/table/useSorting.tsx | 74 +++ .../thumbnails/ThumbnailUploader.hook.tsx | 8 +- src/config/selectors.ts | 54 +- src/langs/ar.json | 1 - src/langs/constants.ts | 18 + src/langs/de.json | 1 - src/langs/en.json | 27 +- src/langs/es.json | 1 - src/langs/fr.json | 9 +- src/langs/it.json | 1 - yarn.lock | 628 ++++++++++++++---- 136 files changed, 4510 insertions(+), 4970 deletions(-) rename cypress/e2e/item/copy/{gridCopyItem.cy.ts => copy.cy.ts} (80%) delete mode 100644 cypress/e2e/item/copy/listCopyItem.cy.ts delete mode 100644 cypress/e2e/item/copy/listCopyMultiple.cy.ts delete mode 100644 cypress/e2e/item/delete/gridRecycleItem.cy.ts delete mode 100644 cypress/e2e/item/delete/listDeleteItems.cy.ts delete mode 100644 cypress/e2e/item/delete/listRecycleItems.cy.ts delete mode 100644 cypress/e2e/item/delete/listRestoreItem.cy.ts create mode 100644 cypress/e2e/item/home/layoutMode.cy.ts delete mode 100644 cypress/e2e/item/move/gridMoveItem.cy.ts delete mode 100644 cypress/e2e/item/move/listMoveMultiple.cy.ts rename cypress/e2e/item/move/{listMoveItem.cy.ts => moveItem.cy.ts} (85%) create mode 100644 cypress/e2e/item/publish/viewPublished.cy.ts rename cypress/e2e/item/{delete/listDeleteItem.cy.ts => trash/deleteItem.cy.ts} (73%) rename cypress/e2e/item/{delete/listRecycleItem.cy.ts => trash/recycleItem.cy.ts} (68%) create mode 100644 cypress/e2e/item/trash/restoreItem.cy.ts create mode 100644 cypress/e2e/item/trash/viewTrash.cy.ts create mode 100644 src/components/common/DuplicateButton.tsx delete mode 100644 src/components/common/EditButton.tsx create mode 100644 src/components/common/FlagButton.tsx delete mode 100644 src/components/common/MoveButton.tsx delete mode 100644 src/components/file/FileUploaderOverlay.tsx create mode 100644 src/components/hooks/uploadWithProgress.ts delete mode 100644 src/components/hooks/uploadWithProgress.tsx create mode 100644 src/components/item/FolderContent.tsx create mode 100644 src/components/item/copy/CopyButton.tsx create mode 100644 src/components/item/copy/CopyModal.tsx create mode 100644 src/components/item/edit/EditButton.tsx rename src/components/item/{form/EditModalWrapper.tsx => edit/EditModal.tsx} (77%) create mode 100644 src/components/item/header/Actions.tsx create mode 100644 src/components/item/move/MoveButton.tsx create mode 100644 src/components/item/move/MoveModal.tsx create mode 100644 src/components/item/shortcut/CreateShortcutButton.tsx create mode 100644 src/components/item/shortcut/CreateShortcutModal.tsx delete mode 100644 src/components/main/CopyButton.tsx delete mode 100644 src/components/main/CreateShortcutButton.tsx delete mode 100644 src/components/main/EmptyItem.tsx delete mode 100644 src/components/main/ItemActions.tsx delete mode 100644 src/components/main/ItemCard.tsx delete mode 100644 src/components/main/ItemMenu.tsx create mode 100644 src/components/main/ItemMenuContent.tsx delete mode 100644 src/components/main/Items.tsx delete mode 100644 src/components/main/ItemsGrid.tsx create mode 100644 src/components/main/ItemsTableCard.tsx delete mode 100644 src/components/main/ItemsToolbar.tsx create mode 100644 src/components/map/DesktopMap.tsx create mode 100644 src/components/map/useCurrentLocation.tsx delete mode 100644 src/components/pages/HomeScreen.tsx create mode 100644 src/components/pages/NoItemFilters.tsx create mode 100644 src/components/pages/PageWrapper.tsx create mode 100644 src/components/pages/home/HomeScreen.tsx create mode 100644 src/components/pages/home/HomeScreenLoading.tsx delete mode 100644 src/components/table/ActionsCellRenderer.tsx rename src/components/table/{BadgesCellRenderer.tsx => Badges.tsx} (55%) create mode 100644 src/components/table/ItemActions.tsx create mode 100644 src/components/table/ItemCard.tsx delete mode 100644 src/components/table/ItemNameCellRenderer.tsx create mode 100644 src/components/table/ItemThumbnail.tsx create mode 100644 src/components/table/ShowOnlyMeButton.tsx create mode 100644 src/components/table/SortingSelect.tsx create mode 100644 src/components/table/types.ts create mode 100644 src/components/table/useSorting.tsx diff --git a/cypress/e2e/item/bookmarks/bookmarks.cy.ts b/cypress/e2e/item/bookmarks/bookmarks.cy.ts index acf19ce3b..f0a90c430 100644 --- a/cypress/e2e/item/bookmarks/bookmarks.cy.ts +++ b/cypress/e2e/item/bookmarks/bookmarks.cy.ts @@ -3,15 +3,22 @@ import { PackedItemBookmarkFactory, } from '@graasp/sdk'; -import i18n from '../../../../src/config/i18n'; +import { SortingOptions } from '@/components/table/types'; +import { BUILDER } from '@/langs/constants'; + +import i18n, { BUILDER_NAMESPACE } from '../../../../src/config/i18n'; import { BOOKMARKED_ITEMS_PATH, HOME_PATH } from '../../../../src/config/paths'; import { + BOOKMARKED_ITEMS_ERROR_ALERT_ID, BOOKMARKED_ITEMS_ID, - BOOKMARKED_ITEM_BUTTON_CLASS, + BOOKMARK_ICON_SELECTOR, CREATE_ITEM_BUTTON_ID, - buildItemMenu, - buildItemMenuButtonId, - buildItemsTableRowIdAttribute, + ITEM_SEARCH_INPUT_ID, + SORTING_ORDERING_SELECTOR_ASC, + SORTING_ORDERING_SELECTOR_DESC, + SORTING_SELECT_SELECTOR, + UNBOOKMARK_ICON_SELECTOR, + buildItemCard, } from '../../../../src/config/selectors'; import { CURRENT_USER } from '../../../fixtures/members'; @@ -22,11 +29,12 @@ const BOOKMARKED_ITEMS = [ const ITEMS = BOOKMARKED_ITEMS.map(({ item }) => item); const NON_BOOKMARKED_ITEM = PackedFolderItemFactory(); -const toggleBookmarkButton = (itemId: string) => { - // todo: remove when refactoring the table - cy.wait(500); - cy.get(`#${buildItemMenuButtonId(itemId)}`).click(); - cy.get(`#${buildItemMenu(itemId)} .${BOOKMARKED_ITEM_BUTTON_CLASS}`).click(); +const removefromBookmark = (itemId: string) => { + cy.get(`#${buildItemCard(itemId)} ${UNBOOKMARK_ICON_SELECTOR}`).click(); +}; + +const addToBookmark = (itemId: string) => { + cy.get(`#${buildItemCard(itemId)} ${BOOKMARK_ICON_SELECTOR}`).click(); }; describe('Bookmarked Item', () => { @@ -34,35 +42,53 @@ describe('Bookmarked Item', () => { beforeEach(() => { cy.setUpApi({ items: ITEMS, - bookmarkedItems: BOOKMARKED_ITEMS, }); cy.visit(BOOKMARKED_ITEMS_PATH); }); it('Show empty table', () => { - cy.get(`#${BOOKMARKED_ITEMS_ID}`).should('exist'); + i18n.changeLanguage(CURRENT_USER.extra.lang as string); + const text = i18n.t(BUILDER.BOOKMARKS_NO_ITEM, { ns: BUILDER_NAMESPACE }); + cy.get(`#${BOOKMARKED_ITEMS_ID}`).should('contain', text); }); }); - describe('Member has several valid bookmarked items', () => { + describe('Member has bookmarked items', () => { beforeEach(() => { cy.setUpApi({ items: [...ITEMS, NON_BOOKMARKED_ITEM], bookmarkedItems: BOOKMARKED_ITEMS, }); i18n.changeLanguage(CURRENT_USER.extra.lang as string); - cy.visit(HOME_PATH); + cy.visit(BOOKMARKED_ITEMS_PATH); + }); + + it('Empty search', () => { + const searchText = 'mysearch'; + cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(searchText); + const text = i18n.t(BUILDER.BOOKMARKS_NO_ITEM_SEARCH, { + search: searchText, + ns: BUILDER_NAMESPACE, + }); + cy.get(`#${BOOKMARKED_ITEMS_ID}`).should('contain', text); }); it("New button doesn't exist", () => { - cy.visit(BOOKMARKED_ITEMS_PATH); cy.get(`#${CREATE_ITEM_BUTTON_ID}`).should('not.exist'); }); - it('add item to bookmarks', () => { + it('Check bookmarked items view', () => { + for (const { item } of BOOKMARKED_ITEMS) { + cy.get(`#${buildItemCard(item.id)}`).should('be.visible'); + } + }); + + it('Add item to bookmarks', () => { + cy.visit(HOME_PATH); + const item = NON_BOOKMARKED_ITEM; - toggleBookmarkButton(item.id); + addToBookmark(item.id); cy.wait('@bookmarkItem').then(({ request }) => { expect(request.url).to.contain(item.id); @@ -72,19 +98,40 @@ describe('Bookmarked Item', () => { it('remove item from bookmarks', () => { const itemId = ITEMS[1].id; - toggleBookmarkButton(itemId); + removefromBookmark(itemId); cy.wait('@unbookmarkItem').then(({ request }) => { expect(request.url).to.contain(itemId); }); }); - it('check bookmarked items view', () => { - cy.visit(BOOKMARKED_ITEMS_PATH); - - const itemId = ITEMS[1].id; + it('Sorting & Ordering', () => { + cy.get(`${SORTING_SELECT_SELECTOR} input`).should( + 'have.value', + SortingOptions.ItemUpdatedAt, + ); + cy.get(SORTING_ORDERING_SELECTOR_DESC).should('be.visible'); + + cy.get(SORTING_SELECT_SELECTOR).click(); + cy.get('li[data-value="item.name"]').click(); + + // check items are ordered by name + cy.get(`#${BOOKMARKED_ITEMS_ID} h5`).then(($e) => { + BOOKMARKED_ITEMS.sort((a, b) => (a.item.name < b.item.name ? 1 : -1)); + for (let idx = 0; idx < BOOKMARKED_ITEMS.length; idx += 1) { + expect($e[idx].innerText).to.eq(BOOKMARKED_ITEMS[idx].item.name); + } + }); - cy.get(buildItemsTableRowIdAttribute(itemId)).should('exist'); + // change ordering + cy.get(SORTING_ORDERING_SELECTOR_DESC).click(); + cy.get(SORTING_ORDERING_SELECTOR_ASC).should('be.visible'); + cy.get(`#${BOOKMARKED_ITEMS_ID} h5`).then(($e) => { + BOOKMARKED_ITEMS.reverse(); + for (let idx = 0; idx < BOOKMARKED_ITEMS.length; idx += 1) { + expect($e[idx].innerText).to.eq(BOOKMARKED_ITEMS[idx].item.name); + } + }); }); }); @@ -96,9 +143,7 @@ describe('Bookmarked Item', () => { }); cy.visit(BOOKMARKED_ITEMS_PATH); - it('Show empty table', () => { - cy.get(`#${BOOKMARKED_ITEMS_ID}`).should('exist'); - }); + cy.get(`#${BOOKMARKED_ITEMS_ERROR_ALERT_ID}`).should('exist'); }); }); }); diff --git a/cypress/e2e/item/copy/gridCopyItem.cy.ts b/cypress/e2e/item/copy/copy.cy.ts similarity index 80% rename from cypress/e2e/item/copy/gridCopyItem.cy.ts rename to cypress/e2e/item/copy/copy.cy.ts index 42801e38c..634dddb55 100644 --- a/cypress/e2e/item/copy/gridCopyItem.cy.ts +++ b/cypress/e2e/item/copy/copy.cy.ts @@ -8,10 +8,8 @@ import { ITEM_MENU_COPY_BUTTON_CLASS, MY_GRAASP_ITEM_PATH, buildItemCard, - buildItemMenu, - buildItemMenuButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; const copyItem = ({ id, @@ -22,9 +20,8 @@ const copyItem = ({ toItemPath: string; rootId?: string; }) => { - const menuSelector = `#${buildItemMenuButtonId(id)}`; - cy.get(menuSelector).click(); - cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_COPY_BUTTON_CLASS}`).click(); + cy.get(buildItemsGridMoreButtonSelector(id)).click(); + cy.get(`.${ITEM_MENU_COPY_BUTTON_CLASS}`).click(); cy.handleTreeMenu(toItemPath, rootId); }; @@ -35,18 +32,17 @@ const FOLDER2 = PackedFolderItemFactory(); const items = [IMAGE_ITEM, FOLDER, FOLDER2, IMAGE_ITEM_CHILD]; -describe('Copy Item in Grid', () => { +describe('Copy Item', () => { it('copy item on Home', () => { cy.setUpApi({ items }); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); // copy const { id: copyItemId } = FOLDER; copyItem({ id: copyItemId, toItemPath: MY_GRAASP_ITEM_PATH }); cy.wait('@copyItems').then(({ request: { url } }) => { - cy.get(`#${buildItemCard(copyItemId)}`).should('exist'); + cy.get(`#${buildItemCard(copyItemId)}`).should('be.visible'); expect(url).to.contain(copyItemId); }); }); @@ -57,7 +53,6 @@ describe('Copy Item in Grid', () => { // go to children item cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.Grid); // copy const { id: copyItemId } = IMAGE_ITEM_CHILD; @@ -76,7 +71,6 @@ describe('Copy Item in Grid', () => { // go to children item cy.visit(buildItemPath(FOLDER.id)); - cy.switchMode(ItemLayoutMode.Grid); // copy const { id } = IMAGE_ITEM_CHILD; diff --git a/cypress/e2e/item/copy/listCopyItem.cy.ts b/cypress/e2e/item/copy/listCopyItem.cy.ts deleted file mode 100644 index 3dec3c6c4..000000000 --- a/cypress/e2e/item/copy/listCopyItem.cy.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - PackedFolderItemFactory, - PackedLocalFileItemFactory, -} from '@graasp/sdk'; - -import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; -import { - ITEM_MENU_COPY_BUTTON_CLASS, - MY_GRAASP_ITEM_PATH, - buildItemMenu, - buildItemMenuButtonId, - buildItemsTableRowIdAttribute, -} from '../../../../src/config/selectors'; -import ItemLayoutMode from '../../../../src/enums/itemLayoutMode'; - -const copyItem = ({ - id, - toItemPath, - rootId, -}: { - id: string; - toItemPath: string; - rootId?: string; -}) => { - // I need this wait because the table reloads and I lose the menu - // todo: remove on table refactor - cy.wait(500); - cy.get(`#${buildItemMenuButtonId(id)}`).click(); - cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_COPY_BUTTON_CLASS}`).click(); - cy.handleTreeMenu(toItemPath, rootId); -}; - -const IMAGE_ITEM = PackedLocalFileItemFactory(); -const FOLDER = PackedFolderItemFactory(); -const IMAGE_ITEM_CHILD = PackedLocalFileItemFactory({ parentItem: FOLDER }); -const FOLDER2 = PackedFolderItemFactory(); - -const items = [IMAGE_ITEM, FOLDER, FOLDER2, IMAGE_ITEM_CHILD]; - -describe('Copy Item in List', () => { - it('copy item on Home', () => { - cy.setUpApi({ items }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - - // copy - const { id: copyItemId } = IMAGE_ITEM; - copyItem({ id: copyItemId, toItemPath: MY_GRAASP_ITEM_PATH }); - - cy.wait('@copyItems').then(({ request: { url } }) => { - expect(url).to.contain(copyItemId); - cy.get(buildItemsTableRowIdAttribute(copyItemId)).should('exist'); - }); - }); - - it('copy item in item', () => { - cy.setUpApi({ items }); - const { id } = FOLDER; - - // go to children item - cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - - // copy - const { id: copyItemId } = IMAGE_ITEM_CHILD; - const { id: toItem, path: toItemPath } = FOLDER2; - copyItem({ id: copyItemId, toItemPath }); - - cy.wait('@copyItems').then(({ request: { url, body } }) => { - expect(url).to.contain(copyItemId); - expect(body.parentId).to.contain(toItem); - cy.get(buildItemsTableRowIdAttribute(copyItemId)).should('exist'); - }); - }); - - it('copy item to Home', () => { - cy.setUpApi({ items }); - const { id } = FOLDER; - - // go to children item - cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - - // copy - const { id: copyItemId } = IMAGE_ITEM_CHILD; - copyItem({ id: copyItemId, toItemPath: MY_GRAASP_ITEM_PATH }); - - cy.wait('@copyItems').then(({ request: { url } }) => { - expect(url).to.contain(copyItemId); - - cy.get(buildItemsTableRowIdAttribute(copyItemId)).should('exist'); - }); - }); -}); diff --git a/cypress/e2e/item/copy/listCopyMultiple.cy.ts b/cypress/e2e/item/copy/listCopyMultiple.cy.ts deleted file mode 100644 index 23081e09a..000000000 --- a/cypress/e2e/item/copy/listCopyMultiple.cy.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - PackedFolderItemFactory, - PackedLocalFileItemFactory, -} from '@graasp/sdk'; - -import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; -import { - ITEMS_TABLE_COPY_SELECTED_ITEMS_ID, - MY_GRAASP_ITEM_PATH, - buildItemsTableRowIdAttribute, -} from '../../../../src/config/selectors'; -import ItemLayoutMode from '../../../../src/enums/itemLayoutMode'; - -const IMAGE_ITEM = PackedLocalFileItemFactory(); -const FOLDER = PackedFolderItemFactory(); -const IMAGE_ITEM_CHILD = PackedLocalFileItemFactory({ parentItem: FOLDER }); -const IMAGE_ITEM_CHILD2 = PackedLocalFileItemFactory({ parentItem: FOLDER }); -const FOLDER2 = PackedFolderItemFactory(); -const FOLDER3 = PackedFolderItemFactory(); - -const items = [ - IMAGE_ITEM, - FOLDER, - FOLDER2, - FOLDER3, - IMAGE_ITEM_CHILD, - IMAGE_ITEM_CHILD2, -]; - -const copyItems = ({ - itemIds, - toItemPath, - rootId, -}: { - itemIds: string[]; - toItemPath: string; - rootId?: string; -}) => { - // check selected ids - itemIds.forEach((id) => { - cy.get(`${buildItemsTableRowIdAttribute(id)} input`).click(); - }); - - cy.get(`#${ITEMS_TABLE_COPY_SELECTED_ITEMS_ID}`).click(); - cy.handleTreeMenu(toItemPath, rootId); -}; - -describe('Copy items in List', () => { - it('Copy items on Home', () => { - cy.setUpApi({ items }); - cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); - - const itemIds = [FOLDER2.id, FOLDER3.id]; - const { path: toItemPath } = FOLDER; - copyItems({ itemIds, toItemPath }); - - cy.wait('@copyItems').then(({ request: { url } }) => { - itemIds.forEach((id) => { - expect(url).to.contain(id); - cy.get(`${buildItemsTableRowIdAttribute(id)}`).should('exist'); - }); - }); - }); - - it('Copy items in item', () => { - cy.setUpApi({ items }); - const { id: start } = FOLDER; - - // go to children item - cy.visit(buildItemPath(start)); - cy.switchMode(ItemLayoutMode.List); - - // copy - const itemIds = [IMAGE_ITEM_CHILD.id, IMAGE_ITEM_CHILD2.id]; - const { id: toItem, path: toItemPath } = FOLDER2; - copyItems({ itemIds, toItemPath }); - - cy.wait('@copyItems').then(({ request: { url, body } }) => { - expect(body.parentId).to.equal(toItem); - itemIds.forEach((id) => { - expect(url).to.contain(id); - cy.get(`${buildItemsTableRowIdAttribute(id)}`).should('exist'); - }); - }); - }); - - it('Copy items to Home', () => { - cy.setUpApi({ items }); - const { id: start } = FOLDER; - - // go to children item - cy.visit(buildItemPath(start)); - cy.switchMode(ItemLayoutMode.List); - - // copy - const itemIds = [IMAGE_ITEM_CHILD.id, IMAGE_ITEM_CHILD2.id]; - copyItems({ itemIds, toItemPath: MY_GRAASP_ITEM_PATH }); - - cy.wait('@copyItems').then(({ request: { url } }) => { - itemIds.forEach((id) => { - expect(url).to.contain(id); - cy.get(`${buildItemsTableRowIdAttribute(id)}`).should('exist'); - }); - }); - }); -}); diff --git a/cypress/e2e/item/create/createApp.cy.ts b/cypress/e2e/item/create/createApp.cy.ts index 4ab567816..8b1442d84 100644 --- a/cypress/e2e/item/create/createApp.cy.ts +++ b/cypress/e2e/item/create/createApp.cy.ts @@ -1,7 +1,6 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; -import ItemLayoutMode from '../../../../src/enums/itemLayoutMode'; import { GRAASP_APP_ITEM, GRAASP_CUSTOM_APP_ITEM, @@ -10,6 +9,7 @@ import { APPS_LIST } from '../../../fixtures/apps/apps'; import { createApp } from '../../../support/createUtils'; const FOLDER = PackedFolderItemFactory(); +const CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); describe('Create App', () => { describe('create app on Home', () => { @@ -17,8 +17,6 @@ describe('Create App', () => { cy.setUpApi(); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - // create createApp(GRAASP_APP_ITEM, { id: APPS_LIST[0].id }); @@ -32,8 +30,6 @@ describe('Create App', () => { cy.setUpApi(); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - // create createApp(GRAASP_APP_ITEM, { custom: true }); @@ -46,18 +42,18 @@ describe('Create App', () => { describe('create app in item', () => { it('Create app with dropdown', () => { - cy.setUpApi({ items: [FOLDER] }); + cy.setUpApi({ items: [FOLDER, CHILD] }); const { id } = FOLDER; // go to children item cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // create createApp(GRAASP_APP_ITEM, { id: APPS_LIST[0].id }); - - cy.wait('@postItem').then(() => { + cy.wait('@postItem').then(({ request: { url } }) => { + expect(url).to.contain(FOLDER.id); + // add after child + expect(url).to.contain(CHILD.id); // expect update cy.wait('@getItem').its('response.url').should('contain', id); }); @@ -70,8 +66,6 @@ describe('Create App', () => { // go to children item cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // create createApp(GRAASP_CUSTOM_APP_ITEM, { custom: true }); diff --git a/cypress/e2e/item/create/createDocument.cy.ts b/cypress/e2e/item/create/createDocument.cy.ts index 0203c77bf..bb51ed6fb 100644 --- a/cypress/e2e/item/create/createDocument.cy.ts +++ b/cypress/e2e/item/create/createDocument.cy.ts @@ -7,7 +7,6 @@ import { import { ITEM_FORM_CONFIRM_BUTTON_ID } from '@/config/selectors'; import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; -import ItemLayoutMode from '../../../../src/enums/itemLayoutMode'; import { createDocument } from '../../../support/createUtils'; describe('Create Document', () => { @@ -15,8 +14,6 @@ describe('Create Document', () => { cy.setUpApi(); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - // create createDocument(DocumentItemFactory()); @@ -28,18 +25,20 @@ describe('Create Document', () => { it('create document in item', () => { const FOLDER = PackedFolderItemFactory(); - cy.setUpApi({ items: [FOLDER] }); + const CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); + cy.setUpApi({ items: [FOLDER, CHILD] }); const { id } = FOLDER; // go to children item cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // create createDocument(DocumentItemFactory()); - cy.wait('@postItem').then(() => { + cy.wait('@postItem').then(({ request: { url } }) => { + expect(url).to.contain(FOLDER.id); + // add after child + expect(url).to.contain(CHILD.id); // expect update cy.wait('@getItem').its('response.url').should('contain', id); }); @@ -49,7 +48,6 @@ describe('Create Document', () => { cy.setUpApi(); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); createDocument( DocumentItemFactory({ name: '', diff --git a/cypress/e2e/item/create/createFolder.cy.ts b/cypress/e2e/item/create/createFolder.cy.ts index 3ce85c7f9..a32e60c14 100644 --- a/cypress/e2e/item/create/createFolder.cy.ts +++ b/cypress/e2e/item/create/createFolder.cy.ts @@ -2,122 +2,62 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { - CREATE_ITEM_BUTTON_ID, ITEM_FORM_CONFIRM_BUTTON_ID, - ITEM_FORM_NAME_INPUT_ID, ITEM_SETTING_DESCRIPTION_PLACEMENT_SELECT_ID, - buildItemsTableRowIdAttribute, } from '../../../../src/config/selectors'; -import ItemLayoutMode from '../../../../src/enums/itemLayoutMode'; import { createFolder } from '../../../support/createUtils'; describe('Create Folder', () => { - describe('List', () => { - it('create folder on Home', () => { - cy.setUpApi(); - cy.visit(HOME_PATH); + it('create folder on Home', () => { + cy.setUpApi(); + cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); + // create + createFolder({ name: 'created item' }); - // create - createFolder({ name: 'created item' }); - - cy.wait(['@postItem', '@getAccessibleItems']); - }); - - it('create folder in item', () => { - const FOLDER = PackedFolderItemFactory(); - cy.setUpApi({ items: [FOLDER] }); - const { id } = FOLDER; - - // go to children item - cy.visit(buildItemPath(id)); - - cy.switchMode(ItemLayoutMode.List); - - // create - createFolder({ name: 'created item' }); - }); - - it('cannot create folder with blank name in item', () => { - // create - cy.setUpApi(); - cy.visit(HOME_PATH); - createFolder({ name: ' ' }, { confirm: false }); - - cy.get(`#${ITEM_FORM_CONFIRM_BUTTON_ID}`).should( - 'have.prop', - 'disabled', - true, - ); - }); - - it('description placement should not exist for folder', () => { - // create - cy.setUpApi(); - cy.visit(HOME_PATH); - createFolder({ name: ' ' }, { confirm: false }); - - cy.get(`#${ITEM_SETTING_DESCRIPTION_PLACEMENT_SELECT_ID}`).should( - 'not.exist', - ); - }); + cy.wait(['@postItem', '@getAccessibleItems']); }); - describe('Grid', () => { - it('create folder on Home', () => { - cy.setUpApi(); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - // create - createFolder({ name: 'created item' }); - - cy.wait('@postItem'); - // small necessary pause required in order for the form to be able to reset - cy.wait(300); - // form is cleared - cy.get(`#${CREATE_ITEM_BUTTON_ID}`).click({ force: true }); - cy.get(`#${ITEM_FORM_NAME_INPUT_ID}`).should('have.value', ''); - }); - - it('create folder in item', () => { - const FOLDER = PackedFolderItemFactory(); - cy.setUpApi({ items: [FOLDER] }); - const { id } = FOLDER; + it('create folder in item', () => { + const FOLDER = PackedFolderItemFactory(); + const CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); + cy.setUpApi({ items: [FOLDER, CHILD] }); + const { id } = FOLDER; - // go to children item - cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.Grid); + // go to children item + cy.visit(buildItemPath(id)); - // create - createFolder({ name: 'created item' }); + // create + createFolder({ name: 'created item' }); - cy.wait('@postItem').then(() => { - // expect update - cy.wait('@getItem').its('response.url').should('contain', id); - }); + cy.wait('@postItem').then(({ request: { url } }) => { + expect(url).to.contain(FOLDER.id); + // add after child + expect(url).to.contain(CHILD.id); }); }); - describe('Error handling', () => { - it('error while creating folder does not create in interface', () => { - const FOLDER = PackedFolderItemFactory(); - cy.setUpApi({ items: [FOLDER], postItemError: true }); - const { id } = FOLDER; - - // go to children item - cy.visit(buildItemPath(id)); - - cy.switchMode(ItemLayoutMode.List); + it('cannot create folder with blank name in item', () => { + // create + cy.setUpApi(); + cy.visit(HOME_PATH); + createFolder({ name: ' ' }, { confirm: false }); + + cy.get(`#${ITEM_FORM_CONFIRM_BUTTON_ID}`).should( + 'have.prop', + 'disabled', + true, + ); + }); - // create - createFolder({ name: 'created item' }); + it('description placement should not exist for folder', () => { + // create + cy.setUpApi(); + cy.visit(HOME_PATH); + createFolder({ name: ' ' }, { confirm: false }); - cy.wait('@postItem').then(({ response: { body } }) => { - // check item is created and displayed - cy.get(buildItemsTableRowIdAttribute(body.id)).should('not.exist'); - }); - }); + cy.get(`#${ITEM_SETTING_DESCRIPTION_PLACEMENT_SELECT_ID}`).should( + 'not.exist', + ); }); }); diff --git a/cypress/e2e/item/create/createLink.cy.ts b/cypress/e2e/item/create/createLink.cy.ts index b25dce4fe..aaf66b910 100644 --- a/cypress/e2e/item/create/createLink.cy.ts +++ b/cypress/e2e/item/create/createLink.cy.ts @@ -2,7 +2,6 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { ITEM_FORM_CONFIRM_BUTTON_ID } from '../../../../src/config/selectors'; -import ItemLayoutMode from '../../../../src/enums/itemLayoutMode'; import { GRAASP_LINK_ITEM, GRAASP_LINK_ITEM_NO_PROTOCOL, @@ -16,8 +15,6 @@ describe('Create Link', () => { cy.setUpApi(); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - // create createLink(GRAASP_LINK_ITEM); @@ -34,8 +31,6 @@ describe('Create Link', () => { cy.setUpApi(); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - // create createLink(GRAASP_LINK_ITEM_NO_PROTOCOL); @@ -50,21 +45,21 @@ describe('Create Link', () => { it('create link in item', () => { const FOLDER = PackedFolderItemFactory(); + const CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); - cy.setUpApi({ items: [FOLDER] }); + cy.setUpApi({ items: [FOLDER, CHILD] }); const { id } = FOLDER; // go to children item cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // create createLink(GRAASP_LINK_ITEM); - cy.wait('@postItem').then(() => { - // check item is created and displayed - cy.wait(CREATE_ITEM_PAUSE); + cy.wait('@postItem').then(({ request: { url } }) => { + expect(url).to.contain(FOLDER.id); + // add after child + expect(url).to.contain(CHILD.id); // expect update cy.wait('@getItem').its('response.url').should('contain', id); @@ -80,8 +75,6 @@ describe('Create Link', () => { // go to children item cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // create createLink(INVALID_LINK_ITEM, { confirm: false, diff --git a/cypress/e2e/item/create/createShortcut.cy.ts b/cypress/e2e/item/create/createShortcut.cy.ts index dc122975b..7948a881a 100644 --- a/cypress/e2e/item/create/createShortcut.cy.ts +++ b/cypress/e2e/item/create/createShortcut.cy.ts @@ -9,10 +9,8 @@ import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { ITEM_MENU_SHORTCUT_BUTTON_CLASS, MY_GRAASP_ITEM_PATH, - buildItemMenu, - buildItemMenuButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; -import ItemLayoutMode from '../../../../src/enums/itemLayoutMode'; const IMAGE_ITEM = PackedLocalFileItemFactory(); const FOLDER = PackedFolderItemFactory(); @@ -23,41 +21,14 @@ const createShortcut = ({ id, toItemPath, rootId, -}: { - id: string; - toItemPath: string; - rootId?: string; -}) => { - cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_SHORTCUT_BUTTON_CLASS}`).click(); - cy.handleTreeMenu(toItemPath, rootId); -}; - -const createShortcutInGrid = ({ - id, - toItemPath, -}: { - id: string; - toItemPath?: string; -}) => { - const menuSelector = `#${buildItemMenuButtonId(id)}`; - cy.get(menuSelector).click(); - createShortcut({ id, toItemPath }); -}; - -const createShortcutInList = ({ - id, - toItemPath, - rootId, }: { id: string; toItemPath?: string; rootId?: string; }) => { - // todo: remove on table refactor - cy.wait(500); - const menuSelector = `#${buildItemMenuButtonId(id)}`; - cy.get(menuSelector).click(); - createShortcut({ id, toItemPath, rootId }); + cy.get(buildItemsGridMoreButtonSelector(id)).click(); + cy.get(`.${ITEM_MENU_SHORTCUT_BUTTON_CLASS}`).click(); + cy.handleTreeMenu(toItemPath, rootId); }; const checkCreateShortcutRequest = ({ @@ -85,72 +56,34 @@ const checkCreateShortcutRequest = ({ }; describe('Create Shortcut', () => { - describe('List', () => { - it('create shortcut from Home to Home', () => { - cy.setUpApi({ items: [IMAGE_ITEM] }); - cy.visit(HOME_PATH); - - const { id } = IMAGE_ITEM; - createShortcutInList({ id, toItemPath: MY_GRAASP_ITEM_PATH }); - - checkCreateShortcutRequest({ id }); - }); + it('create shortcut from Home to Home', () => { + cy.setUpApi({ items: [IMAGE_ITEM] }); + cy.visit(HOME_PATH); - it('create shortcut from Home to Item', () => { - cy.setUpApi({ items: [FOLDER, IMAGE_ITEM] }); - cy.visit(HOME_PATH); + const { id } = IMAGE_ITEM; + createShortcut({ id, toItemPath: MY_GRAASP_ITEM_PATH }); - const { id } = IMAGE_ITEM; - const { id: toItemId, path: toItemPath } = FOLDER; - createShortcutInList({ id, toItemPath }); - - checkCreateShortcutRequest({ id, toItemId }); - }); - - it('create shortcut from Item to Item', () => { - cy.setUpApi({ items: [FOLDER, FOLDER2, IMAGE_ITEM_CHILD] }); - cy.visit(buildItemPath(FOLDER.id)); - - const { id } = IMAGE_ITEM_CHILD; - const { id: toItemId, path: toItemPath } = FOLDER2; - createShortcutInList({ id, toItemPath }); - checkCreateShortcutRequest({ id, toItemId }); - }); + checkCreateShortcutRequest({ id }); }); - describe('Grid', () => { - it('create shortcut from Home to Home', () => { - cy.setUpApi({ items: [IMAGE_ITEM] }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - const { id } = IMAGE_ITEM; - createShortcutInGrid({ id, toItemPath: MY_GRAASP_ITEM_PATH }); - checkCreateShortcutRequest({ id }); - }); + it('create shortcut from Home to Item', () => { + cy.setUpApi({ items: [FOLDER, IMAGE_ITEM] }); + cy.visit(HOME_PATH); - it('create shortcut from Home to Item', () => { - cy.setUpApi({ items: [FOLDER, IMAGE_ITEM] }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); + const { id } = IMAGE_ITEM; + const { id: toItemId, path: toItemPath } = FOLDER; + createShortcut({ id, toItemPath }); - const { id } = IMAGE_ITEM; - const { id: toItemId, path: toItemPath } = FOLDER; - createShortcutInGrid({ id, toItemPath }); - - checkCreateShortcutRequest({ id, toItemId }); - }); - - it('create shortcut from Item to Item', () => { - cy.setUpApi({ items: [FOLDER, FOLDER2, IMAGE_ITEM_CHILD] }); - cy.visit(buildItemPath(FOLDER.id)); - cy.switchMode(ItemLayoutMode.Grid); + checkCreateShortcutRequest({ id, toItemId }); + }); - const { id } = IMAGE_ITEM_CHILD; - const { id: toItemId, path: toItemPath } = FOLDER2; - createShortcutInGrid({ id, toItemPath }); + it('create shortcut from Item to Item', () => { + cy.setUpApi({ items: [FOLDER, FOLDER2, IMAGE_ITEM_CHILD] }); + cy.visit(buildItemPath(FOLDER.id)); - checkCreateShortcutRequest({ id, toItemId }); - }); + const { id } = IMAGE_ITEM_CHILD; + const { id: toItemId, path: toItemPath } = FOLDER2; + createShortcut({ id, toItemPath }); + checkCreateShortcutRequest({ id, toItemId }); }); }); diff --git a/cypress/e2e/item/create/importH5p.cy.ts b/cypress/e2e/item/create/importH5p.cy.ts index 238795b43..ad9ecdbd4 100644 --- a/cypress/e2e/item/create/importH5p.cy.ts +++ b/cypress/e2e/item/create/importH5p.cy.ts @@ -5,7 +5,6 @@ import { CREATE_ITEM_BUTTON_ID, H5P_DASHBOARD_UPLOADER_ID, } from '../../../../src/config/selectors'; -import ItemLayoutMode from '../../../../src/enums/itemLayoutMode'; import { createItem } from '../../../support/createUtils'; const NEW_H5P_ITEM = { @@ -18,8 +17,6 @@ describe('Import H5P', () => { cy.setUpApi(); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - // create createItem(NEW_H5P_ITEM); @@ -29,18 +26,23 @@ describe('Import H5P', () => { cy.get(`#${CREATE_ITEM_BUTTON_ID}`).should('be.visible'); }); - it('create file in item', () => { + it('import h5p in item', () => { const FOLDER = PackedFolderItemFactory(); + const CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); - cy.setUpApi({ items: [FOLDER] }); + cy.setUpApi({ items: [FOLDER, CHILD] }); const { id } = FOLDER; cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // create createItem(NEW_H5P_ITEM); + cy.wait('@importH5p').then(({ request: { url } }) => { + expect(url).to.contain(FOLDER.id); + // add after child + expect(url).to.contain(CHILD.id); + }); + // check interface didn't crash cy.wait(3000); cy.get(`#${CREATE_ITEM_BUTTON_ID}`).should('be.visible'); @@ -53,8 +55,6 @@ describe('Import H5P', () => { const { id } = FOLDER; cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // create createItem(NEW_H5P_ITEM); diff --git a/cypress/e2e/item/create/importZip.cy.ts b/cypress/e2e/item/create/importZip.cy.ts index e33adc900..b8b678f65 100644 --- a/cypress/e2e/item/create/importZip.cy.ts +++ b/cypress/e2e/item/create/importZip.cy.ts @@ -2,7 +2,6 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { ZIP_DASHBOARD_UPLOADER_ID } from '../../../../src/config/selectors'; -import ItemLayoutMode from '../../../../src/enums/itemLayoutMode'; import { ZIP_DEFAULT } from '../../../fixtures/files'; import { createItem } from '../../../support/createUtils'; @@ -11,8 +10,6 @@ describe('Import Zip', () => { cy.setUpApi(); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - // create createItem(ZIP_DEFAULT); @@ -28,8 +25,6 @@ describe('Import Zip', () => { const { id } = FOLDER; cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // create createItem(ZIP_DEFAULT); @@ -45,8 +40,6 @@ describe('Import Zip', () => { const { id } = FOLDER; cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // create createItem(ZIP_DEFAULT); diff --git a/cypress/e2e/item/delete/gridRecycleItem.cy.ts b/cypress/e2e/item/delete/gridRecycleItem.cy.ts deleted file mode 100644 index 1777444ec..000000000 --- a/cypress/e2e/item/delete/gridRecycleItem.cy.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PackedFolderItemFactory } from '@graasp/sdk'; - -import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; -import { - ITEM_MENU_RECYCLE_BUTTON_CLASS, - buildItemMenu, - buildItemMenuButtonId, -} from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; - -const recycleItem = (id: string) => { - const menuSelector = `#${buildItemMenuButtonId(id)}`; - cy.get(menuSelector).click(); - cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_RECYCLE_BUTTON_CLASS}`).click(); -}; - -describe('Recycle Item in Grid', () => { - it('recycle item on Home', () => { - const FOLDER = PackedFolderItemFactory(); - - cy.setUpApi({ items: [FOLDER] }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - const { id } = FOLDER; - - // recycle - recycleItem(id); - cy.wait(['@recycleItems', '@getAccessibleItems']); - }); - - it('recycle item inside parent', () => { - const FOLDER = PackedFolderItemFactory(); - const CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); - cy.setUpApi({ items: [FOLDER, CHILD] }); - const { id } = FOLDER; - const { id: idToDelete } = CHILD; - - // go to children item - cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.Grid); - - // recycle - recycleItem(idToDelete); - cy.wait('@recycleItems').then(() => { - // check update - cy.wait('@getItem').its('response.url').should('contain', id); - }); - }); -}); diff --git a/cypress/e2e/item/delete/listDeleteItems.cy.ts b/cypress/e2e/item/delete/listDeleteItems.cy.ts deleted file mode 100644 index a6150c805..000000000 --- a/cypress/e2e/item/delete/listDeleteItems.cy.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { PackedRecycledItemDataFactory } from '@graasp/sdk'; - -import { RECYCLE_BIN_PATH } from '../../../../src/config/paths'; -import { - CONFIRM_DELETE_BUTTON_ID, - ITEMS_TABLE_DELETE_SELECTED_ITEMS_ID, - buildItemsTableRowIdAttribute, -} from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; - -const deleteItems = (itemIds: string[]) => { - // check selected ids - itemIds.forEach((id) => { - cy.get(`${buildItemsTableRowIdAttribute(id)} .ag-checkbox-input`).click(); - }); - - cy.get(`#${ITEMS_TABLE_DELETE_SELECTED_ITEMS_ID}`).click(); - cy.get(`#${CONFIRM_DELETE_BUTTON_ID}`).click(); -}; - -describe('Delete Items in List', () => { - const recycledItemData = [ - PackedRecycledItemDataFactory(), - PackedRecycledItemDataFactory(), - ]; - const items = recycledItemData.map(({ item }) => item); - const itemIds = items.map(({ id }) => id); - it('delete items', () => { - cy.setUpApi({ items, recycledItemData }); - cy.visit(RECYCLE_BIN_PATH); - - cy.switchMode(ItemLayoutMode.List); - - // delete - deleteItems(itemIds); - cy.wait('@deleteItems').then(({ request: { url } }) => { - for (const id of itemIds) { - expect(url).to.contain(id); - } - }); - cy.wait('@getRecycledItems'); - }); -}); diff --git a/cypress/e2e/item/delete/listRecycleItems.cy.ts b/cypress/e2e/item/delete/listRecycleItems.cy.ts deleted file mode 100644 index 4f0951915..000000000 --- a/cypress/e2e/item/delete/listRecycleItems.cy.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PackedFolderItemFactory } from '@graasp/sdk'; - -import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; -import { - ITEMS_TABLE_RECYCLE_SELECTED_ITEMS_ID, - buildItemsTableRowIdAttribute, -} from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; - -const recycleItems = (itemIds: string[]) => { - // check selected ids - itemIds.forEach((id) => { - cy.get(`${buildItemsTableRowIdAttribute(id)} .ag-checkbox-input`).click(); - }); - - cy.get(`#${ITEMS_TABLE_RECYCLE_SELECTED_ITEMS_ID}`).click(); -}; - -const FOLDER = PackedFolderItemFactory(); -const PARENT = PackedFolderItemFactory(); -const CHILD1 = PackedFolderItemFactory({ parentItem: PARENT }); -const CHILD2 = PackedFolderItemFactory({ parentItem: PARENT }); -const items = [FOLDER, PARENT, CHILD1, CHILD2]; - -describe('Recycle Items in List', () => { - it('recycle 2 items in Home', () => { - cy.setUpApi({ items }); - cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); - - // delete - recycleItems([FOLDER.id, PARENT.id]); - cy.wait(['@recycleItems', '@getAccessibleItems']); - }); - - it('recycle 2 items in item', () => { - cy.setUpApi({ items }); - cy.visit(buildItemPath(PARENT.id)); - - cy.switchMode(ItemLayoutMode.List); - - // delete - recycleItems([CHILD1.id, CHILD2.id]); - cy.wait('@recycleItems').then(() => { - // check item is deleted, others are still displayed - cy.wait('@getItem').its('response.url').should('contain', PARENT.id); - }); - }); -}); diff --git a/cypress/e2e/item/delete/listRestoreItem.cy.ts b/cypress/e2e/item/delete/listRestoreItem.cy.ts deleted file mode 100644 index 2e9208f3b..000000000 --- a/cypress/e2e/item/delete/listRestoreItem.cy.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { PackedRecycledItemDataFactory } from '@graasp/sdk'; - -import { RECYCLE_BIN_PATH } from '../../../../src/config/paths'; -import { - ITEMS_TABLE_RESTORE_SELECTED_ITEMS_ID, - RESTORE_ITEMS_BUTTON_CLASS, - buildItemsTableRowIdAttribute, -} from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; - -const restoreItem = (id: string) => { - cy.get( - `${buildItemsTableRowIdAttribute(id)} .${RESTORE_ITEMS_BUTTON_CLASS}`, - ).click(); -}; - -const restoreItems = (itemIds: string[]) => { - // check selected ids - itemIds.forEach((id) => { - cy.get(`${buildItemsTableRowIdAttribute(id)} .ag-checkbox-input`).click(); - }); - - cy.get(`#${ITEMS_TABLE_RESTORE_SELECTED_ITEMS_ID}`).click(); -}; - -describe('Restore Items in List', () => { - it('restore one item', () => { - const recycledItemData = [ - PackedRecycledItemDataFactory(), - PackedRecycledItemDataFactory(), - ]; - cy.setUpApi({ - items: recycledItemData.map(({ item }) => item), - recycledItemData, - }); - cy.visit(RECYCLE_BIN_PATH); - - cy.switchMode(ItemLayoutMode.List); - const { id } = recycledItemData[0].item; - - // restore - restoreItem(id); - cy.wait('@restoreItems').then(({ request: { url } }) => { - expect(url).to.contain(id); - }); - cy.wait('@getRecycledItems'); - }); - - it('restore multiple items', () => { - const recycledItemData = [ - PackedRecycledItemDataFactory(), - PackedRecycledItemDataFactory(), - ]; - const items = recycledItemData.map(({ item }) => item); - cy.setUpApi({ - items, - recycledItemData, - }); - cy.visit(RECYCLE_BIN_PATH); - - cy.switchMode(ItemLayoutMode.List); - - // restore - const itemIds = items.map(({ id }) => id); - restoreItems(itemIds); - cy.wait('@restoreItems').then(({ request: { url } }) => { - for (const id of itemIds) { - expect(url).to.contain(id); - } - }); - cy.wait('@getRecycledItems'); - }); -}); diff --git a/cypress/e2e/item/download/downloadItem.cy.ts b/cypress/e2e/item/download/downloadItem.cy.ts index 4094d063d..8480096fb 100644 --- a/cypress/e2e/item/download/downloadItem.cy.ts +++ b/cypress/e2e/item/download/downloadItem.cy.ts @@ -1,7 +1,6 @@ import { PackedFolderItemFactory, PermissionLevel } from '@graasp/sdk'; import { buildDownloadButtonId } from '@/config/selectors'; -import { ItemLayoutMode } from '@/enums'; import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { SAMPLE_PUBLIC_ITEMS } from '../../../fixtures/items'; @@ -31,7 +30,6 @@ describe('Download Item', () => { it('Grid view', () => { cy.setUpApi({ items: [SHARED_ITEM] }); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); cy.wait('@getAccessibleItems').then( ({ response: { diff --git a/cypress/e2e/item/duplicate/duplicateItem.cy.ts b/cypress/e2e/item/duplicate/duplicateItem.cy.ts index 0aec6602a..b2e77e876 100644 --- a/cypress/e2e/item/duplicate/duplicateItem.cy.ts +++ b/cypress/e2e/item/duplicate/duplicateItem.cy.ts @@ -3,50 +3,43 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { getParentsIdsFromPath } from '@/utils/item'; import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; -import ItemLayoutMode from '../../../../src/enums/itemLayoutMode'; import duplicateItem from '../../../support/actionsUtils'; describe('duplicate Item in Home', () => { - Object.values([ItemLayoutMode.Grid, ItemLayoutMode.List]).forEach((view) => { - it(`duplicate item on Home in ${view} view`, () => { - const FOLDER = PackedFolderItemFactory(); - cy.setUpApi({ items: [FOLDER] }); - cy.visit(HOME_PATH); - cy.switchMode(view); - - // duplicate - const { id: duplicateItemId } = FOLDER; - duplicateItem({ id: duplicateItemId }); - - cy.wait('@copyItems').then(({ request: { url, body } }) => { - expect(url).to.contain(duplicateItemId); - // as we duplicate on home parentId will be undefined - expect(body.parentId).to.equal(undefined); - }); + it(`duplicate item on Home`, () => { + const FOLDER = PackedFolderItemFactory(); + cy.setUpApi({ items: [FOLDER] }); + cy.visit(HOME_PATH); + + // duplicate + const { id: duplicateItemId } = FOLDER; + duplicateItem({ id: duplicateItemId }); + + cy.wait('@copyItems').then(({ request: { url, body } }) => { + expect(url).to.contain(duplicateItemId); + // as we duplicate on home parentId will be undefined + expect(body.parentId).to.equal(undefined); }); }); }); describe('duplicate Item in item', () => { - Object.values([ItemLayoutMode.Grid, ItemLayoutMode.List]).forEach((view) => { - it(`duplicate item in item in ${view} view`, () => { - const FOLDER = PackedFolderItemFactory(); - const CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); - cy.setUpApi({ items: [FOLDER, CHILD] }); - const { id, path } = FOLDER; - const parentsIds = getParentsIdsFromPath(path); - - // go to children item - cy.visit(buildItemPath(id)); - cy.switchMode(view); - - // duplicate - const { id: duplicateItemId } = CHILD; - duplicateItem({ id: duplicateItemId }); - - cy.wait('@copyItems').then(({ request: { url, body } }) => { - expect(url).to.contain(duplicateItemId); - expect(body.parentId).to.equal(parentsIds[0]); - }); + it(`duplicate item in item`, () => { + const FOLDER = PackedFolderItemFactory(); + const CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); + cy.setUpApi({ items: [FOLDER, CHILD] }); + const { id, path } = FOLDER; + const parentsIds = getParentsIdsFromPath(path); + + // go to children item + cy.visit(buildItemPath(id)); + + // duplicate + const { id: duplicateItemId } = CHILD; + duplicateItem({ id: duplicateItemId }); + + cy.wait('@copyItems').then(({ request: { url, body } }) => { + expect(url).to.contain(duplicateItemId); + expect(body.parentId).to.equal(parentsIds[0]); }); }); }); diff --git a/cypress/e2e/item/edit/editApp.cy.ts b/cypress/e2e/item/edit/editApp.cy.ts index 7b393bb54..a2e99bae9 100644 --- a/cypress/e2e/item/edit/editApp.cy.ts +++ b/cypress/e2e/item/edit/editApp.cy.ts @@ -1,5 +1,4 @@ import { - ItemType, PackedAppItemFactory, PackedFolderItemFactory, buildAppExtra, @@ -12,10 +11,9 @@ import { ITEM_MAIN_CLASS, TEXT_EDITOR_CLASS, buildEditButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; -import { buildAppItemLinkForTest } from '../../../fixtures/apps'; -import { CURRENT_USER, MEMBERS } from '../../../fixtures/members'; +import { CURRENT_USER } from '../../../fixtures/members'; import { EDIT_ITEM_PAUSE } from '../../../support/constants'; import { editCaptionFromViewPage, editItem } from '../../../support/editUtils'; @@ -32,21 +30,6 @@ const GRAASP_APP_ITEM = PackedAppItemFactory({ creator: CURRENT_USER, }); -const GRAASP_APP_PARENT_FOLDER = PackedFolderItemFactory({ - name: 'graasp app parent', -}); - -const APP_USING_CONTEXT_ITEM = PackedAppItemFactory({ - name: 'my app', - extra: { - [ItemType.APP]: { - url: `${Cypress.env('VITE_GRAASP_API_HOST')}/${buildAppItemLinkForTest('app.html')}`, - }, - }, - creator: MEMBERS.ANNA, - parentItem: GRAASP_APP_PARENT_FOLDER, -}); - describe('Edit App', () => { describe('View Page', () => { beforeEach(() => { @@ -81,137 +64,65 @@ describe('Edit App', () => { }); }); - describe('List', () => { - it('edit app on Home', () => { - const itemToEdit = GRAASP_APP_ITEM; - cy.setUpApi({ items: [itemToEdit] }); - cy.visit(HOME_PATH); + it('edit app on Home', () => { + const itemToEdit = GRAASP_APP_ITEM; + cy.setUpApi({ items: [itemToEdit] }); + cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); - // edit - editItem( - { - ...itemToEdit, - ...newFields, - }, - ItemLayoutMode.List, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(newFields.name); - cy.wait(EDIT_ITEM_PAUSE); - cy.wait('@getAccessibleItems'); - }, - ); + // edit + editItem({ + ...itemToEdit, + ...newFields, }); - it('edit app in item', () => { - const parentItem = PackedFolderItemFactory(); - const itemToEdit = PackedAppItemFactory({ parentItem }); - cy.setUpApi({ - items: [parentItem, itemToEdit], - }); - // go to children item - cy.visit(buildItemPath(parentItem.id)); - - cy.switchMode(ItemLayoutMode.List); - - // edit - editItem( - { - ...itemToEdit, - ...newFields, - }, - ItemLayoutMode.List, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(newFields.name); - cy.get('@getItem') - .its('response.url') - .should('contain', parentItem.id); + cy.wait('@editItem').then( + ({ + response: { + body: { id, name }, }, - ); - }); + }) => { + // check item is edited and updated + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(newFields.name); + cy.wait(EDIT_ITEM_PAUSE); + cy.wait('@getAccessibleItems'); + }, + ); }); - describe('Grid', () => { - it('edit app on Home', () => { - const itemToEdit = GRAASP_APP_ITEM; - cy.setUpApi({ items: [itemToEdit] }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - // edit - editItem( - { - ...itemToEdit, - ...newFields, - }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(newFields.name); - }, - ); + it('edit app in item', () => { + const parentItem = PackedFolderItemFactory(); + const itemToEdit = PackedAppItemFactory({ parentItem }); + cy.setUpApi({ + items: [parentItem, itemToEdit], }); - - it('edit app in item', () => { - const itemToEdit = APP_USING_CONTEXT_ITEM; - const parent = GRAASP_APP_PARENT_FOLDER; - cy.setUpApi({ items: [parent, itemToEdit] }); - // go to children item - cy.visit(buildItemPath(parent.id)); - cy.switchMode(ItemLayoutMode.Grid); - - // edit - editItem( - { - ...itemToEdit, - ...newFields, + // go to children item + cy.visit(buildItemPath(parentItem.id)); + + // edit + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); + editItem( + { + ...itemToEdit, + ...newFields, + }, + 'ul', + ); + + cy.wait('@editItem').then( + ({ + response: { + body: { id, name }, }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(newFields.name); - cy.get('@getItem').its('response.url').should('contain', parent.id); - }, - ); - }); + }) => { + // check item is edited and updated + cy.wait(EDIT_ITEM_PAUSE); + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(newFields.name); + cy.get('@getItem').its('response.url').should('contain', parentItem.id); + }, + ); }); }); diff --git a/cypress/e2e/item/edit/editDocument.cy.ts b/cypress/e2e/item/edit/editDocument.cy.ts index 1c7298182..292d77ea2 100644 --- a/cypress/e2e/item/edit/editDocument.cy.ts +++ b/cypress/e2e/item/edit/editDocument.cy.ts @@ -11,8 +11,8 @@ import { ITEM_FORM_CONFIRM_BUTTON_ID, TEXT_EDITOR_CLASS, buildEditButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; import { CAPTION_EDIT_PAUSE, EDIT_ITEM_PAUSE, @@ -41,138 +41,66 @@ const GRAASP_DOCUMENT_ITEM_CHILD = PackedDocumentItemFactory({ }); describe('Edit Document', () => { - describe('List', () => { - it('edit on Home', () => { - cy.setUpApi({ items: [GRAASP_DOCUMENT_ITEM] }); - cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); + it('edit on Home', () => { + cy.setUpApi({ items: [GRAASP_DOCUMENT_ITEM] }); + cy.visit(HOME_PATH); - const itemToEdit = GRAASP_DOCUMENT_ITEM; + const itemToEdit = GRAASP_DOCUMENT_ITEM; - // edit - editItem( - { - ...itemToEdit, - ...newFields, - }, - ItemLayoutMode.List, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name, extra }, - }, - }) => { - // check item is edited and updated - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(newFields.name); - expect(getDocumentExtra(extra)?.content).to.contain(content); - cy.wait(EDIT_ITEM_PAUSE); - cy.wait('@getAccessibleItems'); - }, - ); + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); + // edit + editItem({ + ...itemToEdit, + ...newFields, }); - it('edit in folder', () => { - const parent = GRAASP_FOLDER_PARENT; - const itemToEdit = GRAASP_DOCUMENT_ITEM_CHILD; - cy.setUpApi({ items: [parent, itemToEdit] }); - // go to children item - cy.visit(buildItemPath(parent.id)); - - cy.switchMode(ItemLayoutMode.List); - - // edit - editItem( - { - ...itemToEdit, - ...newFields, + cy.wait('@editItem').then( + ({ + response: { + body: { id, name, extra }, }, - ItemLayoutMode.List, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name, extra }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(newFields.name); - expect(getDocumentExtra(extra)?.content).to.contain(content); - cy.get('@getItem').its('response.url').should('contain', parent.id); - }, - ); - }); + }) => { + // check item is edited and updated + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(newFields.name); + expect(getDocumentExtra(extra)?.content).to.contain(content); + cy.wait(EDIT_ITEM_PAUSE); + cy.wait('@getAccessibleItems'); + }, + ); }); - describe('Grid', () => { - it('edit on Home', () => { - const itemToEdit = GRAASP_DOCUMENT_ITEM; - cy.setUpApi({ items: [itemToEdit] }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - // edit - editItem( - { - ...itemToEdit, - ...newFields, - }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name, extra }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(newFields.name); - expect(getDocumentExtra(extra)?.content).to.contain(content); - }, - ); - }); - - it('edit in folder', () => { - const parent = GRAASP_FOLDER_PARENT; - const itemToEdit = GRAASP_DOCUMENT_ITEM_CHILD; - cy.setUpApi({ items: [parent, itemToEdit] }); - cy.visit(buildItemPath(parent.id)); - cy.switchMode(ItemLayoutMode.Grid); - - // edit - editItem( - { - ...itemToEdit, - ...newFields, + it('edit in folder', () => { + const parent = GRAASP_FOLDER_PARENT; + const itemToEdit = GRAASP_DOCUMENT_ITEM_CHILD; + cy.setUpApi({ items: [parent, itemToEdit] }); + // go to children item + cy.visit(buildItemPath(parent.id)); + + // edit + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); + editItem( + { + ...itemToEdit, + ...newFields, + }, + 'ul', + ); + + cy.wait('@editItem').then( + ({ + response: { + body: { id, name, extra }, }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name, extra }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(newFields.name); - expect(getDocumentExtra(extra)?.content).to.contain(content); - cy.get('@getItem').its('response.url').should('contain', parent.id); - }, - ); - }); + }) => { + // check item is edited and updated + cy.wait(EDIT_ITEM_PAUSE); + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(newFields.name); + expect(getDocumentExtra(extra)?.content).to.contain(content); + cy.get('@getItem').its('response.url').should('contain', parent.id); + }, + ); }); describe('View Page', () => { diff --git a/cypress/e2e/item/edit/editEtherpad.cy.ts b/cypress/e2e/item/edit/editEtherpad.cy.ts index 63c55415e..1f6a72335 100644 --- a/cypress/e2e/item/edit/editEtherpad.cy.ts +++ b/cypress/e2e/item/edit/editEtherpad.cy.ts @@ -1,7 +1,8 @@ import { PackedEtherpadItemFactory } from '@graasp/sdk'; +import { buildItemsGridMoreButtonSelector } from '@/config/selectors'; + import { HOME_PATH } from '../../../../src/config/paths'; -import { ItemLayoutMode } from '../../../../src/enums'; import { EDIT_ITEM_PAUSE } from '../../../support/constants'; import { editItem } from '../../../support/editUtils'; @@ -16,68 +17,30 @@ describe('Edit Etherpad', () => { cy.setUpApi({ items: [GRAASP_ETHERPAD_ITEM] }); }); - describe('List', () => { - it('edit etherpad on Home', () => { - cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); - - const itemToEdit = GRAASP_ETHERPAD_ITEM; + it('edit etherpad on Home', () => { + cy.visit(HOME_PATH); - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, - }, - ItemLayoutMode.List, - ); + const itemToEdit = GRAASP_ETHERPAD_ITEM; - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - }, - ); + // edit + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); + editItem({ + ...itemToEdit, + ...EDITED_FIELDS, }); - }); - describe('Grid', () => { - it('edit etherpad on Home', () => { - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - const itemToEdit = GRAASP_ETHERPAD_ITEM; - - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, + cy.wait('@editItem').then( + ({ + response: { + body: { id, name }, }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - }, - ); - }); + }) => { + // check item is edited and updated + cy.wait(EDIT_ITEM_PAUSE); + cy.get('@getAccessibleItems'); + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(EDITED_FIELDS.name); + }, + ); }); }); diff --git a/cypress/e2e/item/edit/editFile.cy.ts b/cypress/e2e/item/edit/editFile.cy.ts index 6695a9a6d..90ff108bf 100644 --- a/cypress/e2e/item/edit/editFile.cy.ts +++ b/cypress/e2e/item/edit/editFile.cy.ts @@ -12,8 +12,8 @@ import { TEXT_EDITOR_CLASS, buildDescriptionPlacementId, buildEditButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; import { MOCK_IMAGE_URL, MOCK_VIDEO_URL } from '../../../fixtures/fileLinks'; import { ICON_FILEPATH, VIDEO_FILEPATH } from '../../../fixtures/files'; import { EDIT_ITEM_PAUSE } from '../../../support/constants'; @@ -112,68 +112,30 @@ describe('Edit File', () => { }); }); - describe('List', () => { - it('edit file on Home', () => { - cy.visit(HOME_PATH); + it('edit file on Home', () => { + cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); + const itemToEdit = IMAGE_ITEM; - const itemToEdit = IMAGE_ITEM; - - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, - }, - ItemLayoutMode.List, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - cy.wait(EDIT_ITEM_PAUSE); - cy.wait('@getAccessibleItems'); - }, - ); + // edit + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); + editItem({ + ...itemToEdit, + ...EDITED_FIELDS, }); - }); - describe('Grid', () => { - it('edit file on Home', () => { - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - const itemToEdit = VIDEO_ITEM_S3; - - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, + cy.wait('@editItem').then( + ({ + response: { + body: { id, name }, }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - cy.wait(EDIT_ITEM_PAUSE); - cy.wait('@getAccessibleItems'); - }, - ); - }); + }) => { + // check item is edited and updated + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(EDITED_FIELDS.name); + cy.wait(EDIT_ITEM_PAUSE); + cy.wait('@getAccessibleItems'); + }, + ); }); }); diff --git a/cypress/e2e/item/edit/editFolder.cy.ts b/cypress/e2e/item/edit/editFolder.cy.ts index 351475a85..7b18b5ed7 100644 --- a/cypress/e2e/item/edit/editFolder.cy.ts +++ b/cypress/e2e/item/edit/editFolder.cy.ts @@ -2,10 +2,10 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { + EDIT_ITEM_BUTTON_CLASS, ITEM_FORM_CONFIRM_BUTTON_ID, - buildEditButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; import { EDIT_ITEM_PAUSE } from '../../../support/constants'; import { editItem } from '../../../support/editUtils'; @@ -14,164 +14,84 @@ const EDITED_FIELDS = { }; describe('Edit Folder', () => { - describe('List', () => { - it('confirm with empty name', () => { - const item = PackedFolderItemFactory(); - cy.setUpApi({ items: [item] }); - cy.visit(HOME_PATH); - - // click edit button - const itemId = item.id; - // todo: remove once the table is refactored - cy.wait(500); - cy.get(`#${buildEditButtonId(itemId)}`).click(); - - cy.fillFolderModal( - { - // put an empty name for the folder - name: '', - }, - { confirm: false }, - ); - - // check that the button can not be clicked - cy.get(`#${ITEM_FORM_CONFIRM_BUTTON_ID}`).should('be.disabled'); - }); - - it('edit folder on Home', () => { - const item = PackedFolderItemFactory(); - cy.setUpApi({ items: [item] }); - cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); - - const itemToEdit = item; - const newDescription = 'new description'; - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, - description: newDescription, - }, - ItemLayoutMode.List, - ); + it('confirm with empty name', () => { + const item = PackedFolderItemFactory(); + cy.setUpApi({ items: [item] }); + cy.visit(HOME_PATH); + + // click edit button + const itemId = item.id; + cy.get(buildItemsGridMoreButtonSelector(itemId)).click(); + cy.get(`.${EDIT_ITEM_BUTTON_CLASS}`).click(); + + cy.fillFolderModal( + { + // put an empty name for the folder + name: '', + }, + { confirm: false }, + ); + + // check that the button can not be clicked + cy.get(`#${ITEM_FORM_CONFIRM_BUTTON_ID}`).should('be.disabled'); + }); - cy.wait('@editItem').then( - ({ - response: { - body: { id, name, description }, - }, - }) => { - // check item is edited and updated - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - expect(description).to.contain(newDescription); - cy.wait(EDIT_ITEM_PAUSE); - cy.wait('@getAccessibleItems'); - }, - ); + it('edit folder on Home', () => { + const item = PackedFolderItemFactory(); + cy.setUpApi({ items: [item] }); + cy.visit(HOME_PATH); + + const itemToEdit = item; + const newDescription = 'new description'; + // edit + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); + editItem({ + ...itemToEdit, + ...EDITED_FIELDS, + description: newDescription, }); - it('edit folder in item', () => { - const parentItem = PackedFolderItemFactory(); - const itemToEdit = PackedFolderItemFactory({ parentItem }); - cy.setUpApi({ items: [parentItem, itemToEdit] }); - // go to children item - cy.visit(buildItemPath(itemToEdit.id)); - - cy.switchMode(ItemLayoutMode.List); - - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, - }, - ItemLayoutMode.List, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - cy.get('@getItem') - .its('response.url') - .should('contain', itemToEdit.id); + cy.wait('@editItem').then( + ({ + response: { + body: { id, name, description }, }, - ); - }); + }) => { + // check item is edited and updated + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(EDITED_FIELDS.name); + expect(description).to.contain(newDescription); + cy.wait(EDIT_ITEM_PAUSE); + cy.wait('@getAccessibleItems'); + }, + ); }); - describe('Grid', () => { - it('edit folder on Home', () => { - const itemToEdit = PackedFolderItemFactory(); - cy.setUpApi({ items: [itemToEdit] }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, - }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - }, - ); + it('edit folder in item', () => { + const parentItem = PackedFolderItemFactory(); + const itemToEdit = PackedFolderItemFactory({ parentItem }); + cy.setUpApi({ items: [parentItem, itemToEdit] }); + // go to children item + cy.visit(buildItemPath(itemToEdit.id)); + + // edit + editItem({ + ...itemToEdit, + ...EDITED_FIELDS, }); - it('edit folder in item', () => { - const parentItem = PackedFolderItemFactory(); - const itemToEdit = PackedFolderItemFactory({ parentItem }); - cy.setUpApi({ items: [parentItem, itemToEdit] }); - // go to children item - cy.visit(buildItemPath(itemToEdit.id)); - cy.switchMode(ItemLayoutMode.Grid); - - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, + cy.wait('@editItem').then( + ({ + response: { + body: { id, name }, }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - cy.get('@getItem') - .its('response.url') - .should('contain', itemToEdit.id); - }, - ); - }); + }) => { + // check item is edited and updated + cy.wait(EDIT_ITEM_PAUSE); + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(EDITED_FIELDS.name); + cy.get('@getItem').its('response.url').should('contain', itemToEdit.id); + }, + ); }); }); diff --git a/cypress/e2e/item/edit/editH5p.cy.ts b/cypress/e2e/item/edit/editH5p.cy.ts index 9b6330cd4..c9b149134 100644 --- a/cypress/e2e/item/edit/editH5p.cy.ts +++ b/cypress/e2e/item/edit/editH5p.cy.ts @@ -1,7 +1,8 @@ import { PackedH5PItemFactory } from '@graasp/sdk'; +import { buildItemsGridMoreButtonSelector } from '@/config/selectors'; + import { HOME_PATH } from '../../../../src/config/paths'; -import { ItemLayoutMode } from '../../../../src/enums'; import { EDIT_ITEM_PAUSE } from '../../../support/constants'; import { editItem } from '../../../support/editUtils'; @@ -16,68 +17,30 @@ describe('Edit H5P', () => { cy.setUpApi({ items: [GRAASP_H5P_ITEM] }); }); - describe('List', () => { - it('edit h5p on Home', () => { - cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); - - const itemToEdit = GRAASP_H5P_ITEM; + it('edit h5p on Home', () => { + cy.visit(HOME_PATH); - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, - }, - ItemLayoutMode.List, - ); + const itemToEdit = GRAASP_H5P_ITEM; - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - }, - ); + // edit + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); + editItem({ + ...itemToEdit, + ...EDITED_FIELDS, }); - }); - describe('Grid', () => { - it('edit h5p on Home', () => { - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - const itemToEdit = GRAASP_H5P_ITEM; - - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, + cy.wait('@editItem').then( + ({ + response: { + body: { id, name }, }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - }, - ); - }); + }) => { + // check item is edited and updated + cy.wait(EDIT_ITEM_PAUSE); + cy.get('@getAccessibleItems'); + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(EDITED_FIELDS.name); + }, + ); }); }); diff --git a/cypress/e2e/item/edit/editLink.cy.ts b/cypress/e2e/item/edit/editLink.cy.ts index bafc0676c..057273f5e 100644 --- a/cypress/e2e/item/edit/editLink.cy.ts +++ b/cypress/e2e/item/edit/editLink.cy.ts @@ -7,13 +7,10 @@ import { ITEM_MAIN_CLASS, TEXT_EDITOR_CLASS, buildEditButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; import { CURRENT_USER } from '../../../fixtures/members'; -import { - EDIT_ITEM_PAUSE, - ITEM_LOADING_PAUSE, -} from '../../../support/constants'; +import { EDIT_ITEM_PAUSE } from '../../../support/constants'; import { editCaptionFromViewPage, editItem } from '../../../support/editUtils'; const EDITED_FIELDS = { @@ -43,7 +40,6 @@ describe('Edit Link', () => { const { id } = GRAASP_LINK_ITEM; cy.visit(buildItemPath(id)); const caption = 'new caption'; - cy.wait(ITEM_LOADING_PAUSE); editCaptionFromViewPage({ id, caption }); cy.wait(`@editItem`).then(({ request: { url, body } }) => { expect(url).to.contain(id); @@ -67,68 +63,31 @@ describe('Edit Link', () => { }); }); - describe('List', () => { - it('edit link on Home', () => { - cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); + it('edit link on Home', () => { + cy.visit(HOME_PATH); - const itemToEdit = GRAASP_LINK_ITEM; - - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, - }, - ItemLayoutMode.List, - ); + const itemToEdit = GRAASP_LINK_ITEM; - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - }, - ); + // edit + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); + editItem({ + ...itemToEdit, + ...EDITED_FIELDS, }); - }); - describe('Grid', () => { - it('edit link on Home', () => { - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - const itemToEdit = GRAASP_LINK_ITEM; - - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); + cy.wait('@editItem').then( + ({ + response: { + body: { id, name }, }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - }, - ); - }); + }) => { + // check item is edited and updated + cy.wait(EDIT_ITEM_PAUSE); + cy.get('@getAccessibleItems'); + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(EDITED_FIELDS.name); + }, + ); }); }); diff --git a/cypress/e2e/item/edit/editShortcut.cy.ts b/cypress/e2e/item/edit/editShortcut.cy.ts index b69ba3956..7214d6d4e 100644 --- a/cypress/e2e/item/edit/editShortcut.cy.ts +++ b/cypress/e2e/item/edit/editShortcut.cy.ts @@ -1,7 +1,8 @@ import { PackedShortcutItemFactory } from '@graasp/sdk'; +import { buildItemsGridMoreButtonSelector } from '@/config/selectors'; + import { HOME_PATH } from '../../../../src/config/paths'; -import { ItemLayoutMode } from '../../../../src/enums'; import { EDIT_ITEM_PAUSE } from '../../../support/constants'; import { editItem } from '../../../support/editUtils'; @@ -16,68 +17,30 @@ describe('Edit Shortcut', () => { cy.setUpApi({ items: [SHORTCUT] }); }); - describe('List', () => { - it('edit shortcut on Home', () => { - cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); - - const itemToEdit = SHORTCUT; + it('edit shortcut on Home', () => { + cy.visit(HOME_PATH); - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, - }, - ItemLayoutMode.List, - ); + const itemToEdit = SHORTCUT; - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - }, - ); + // edit + cy.get(buildItemsGridMoreButtonSelector(itemToEdit.id)).click(); + editItem({ + ...itemToEdit, + ...EDITED_FIELDS, }); - }); - describe('Grid', () => { - it('edit shortcut on Home', () => { - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - const itemToEdit = SHORTCUT; - - // edit - editItem( - { - ...itemToEdit, - ...EDITED_FIELDS, + cy.wait('@editItem').then( + ({ + response: { + body: { id, name }, }, - ItemLayoutMode.Grid, - ); - - cy.wait('@editItem').then( - ({ - response: { - body: { id, name }, - }, - }) => { - // check item is edited and updated - cy.wait(EDIT_ITEM_PAUSE); - cy.get('@getAccessibleItems'); - expect(id).to.equal(itemToEdit.id); - expect(name).to.equal(EDITED_FIELDS.name); - }, - ); - }); + }) => { + // check item is edited and updated + cy.wait(EDIT_ITEM_PAUSE); + cy.get('@getAccessibleItems'); + expect(id).to.equal(itemToEdit.id); + expect(name).to.equal(EDITED_FIELDS.name); + }, + ); }); }); diff --git a/cypress/e2e/item/flag/flagItem.cy.ts b/cypress/e2e/item/flag/flagItem.cy.ts index bfc6a09c1..f9b26f7d8 100644 --- a/cypress/e2e/item/flag/flagItem.cy.ts +++ b/cypress/e2e/item/flag/flagItem.cy.ts @@ -5,23 +5,14 @@ import { HOME_PATH } from '../../../../src/config/paths'; import { ITEM_MENU_FLAG_BUTTON_CLASS, buildFlagListItemId, - buildItemMenu, - buildItemMenuButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; import { BUILDER } from '../../../../src/langs/constants'; import { CURRENT_USER } from '../../../fixtures/members'; const openFlagItemModal = (itemId: string) => { - // todo: remove on table refactor - cy.wait(500); - const menuSelector = `#${buildItemMenuButtonId(itemId)}`; - cy.get(menuSelector).click(); - - const menuFlagButton = cy.get( - `#${buildItemMenu(itemId)} .${ITEM_MENU_FLAG_BUTTON_CLASS}`, - ); - - menuFlagButton.click(); + cy.get(buildItemsGridMoreButtonSelector(itemId)).click(); + cy.get(`.${ITEM_MENU_FLAG_BUTTON_CLASS}`).click(); }; const flagItem = (itemId: string, type: FlagType) => { diff --git a/cypress/e2e/item/hide/hideItem.cy.ts b/cypress/e2e/item/hide/hideItem.cy.ts index 640943638..acd5524ab 100644 --- a/cypress/e2e/item/hide/hideItem.cy.ts +++ b/cypress/e2e/item/hide/hideItem.cy.ts @@ -8,10 +8,8 @@ import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { HIDDEN_ITEM_BUTTON_CLASS, buildHideButtonId, - buildItemMenu, - buildItemMenuButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; import { MEMBERS } from '../../../fixtures/members'; import { ItemForTest } from '../../../support/types'; @@ -53,110 +51,53 @@ const ITEM = PackedFolderItemFactory( ); const toggleHideButton = (itemId: string, isHidden = false) => { - // table re-renders when this resolves, so we wait for the call to be made - cy.wait('@getManyPublishItemInformations'); - const menuSelector = `#${buildItemMenuButtonId(itemId)}`; - cy.get(menuSelector).click(); + cy.get(buildItemsGridMoreButtonSelector(itemId)).click(); - cy.get(`#${buildItemMenu(itemId)} .${HIDDEN_ITEM_BUTTON_CLASS}`) + cy.get(`.${HIDDEN_ITEM_BUTTON_CLASS}`) .should('have.attr', 'data-cy', buildHideButtonId(isHidden)) .click(); }; -describe('Hiding Item', () => { - describe('Successfully hide item in List', () => { - beforeEach(() => { - cy.setUpApi({ items: [ITEM, HIDDEN_ITEM, CHILD_HIDDEN_ITEM] }); - }); - - it('Hide an item', () => { - cy.visit(HOME_PATH); - - toggleHideButton(ITEM.id, false); - - cy.wait(`@postItemTag-${ItemTagType.Hidden}`).then( - ({ request: { url } }) => { - expect(url).to.contain(ItemTagType.Hidden); - expect(url).to.contain(ITEM.id); - }, - ); - }); - - it('Show an item', () => { - cy.visit(HOME_PATH); - const item = HIDDEN_ITEM; - - // make sure to wait for the tags to be fetched - toggleHideButton(item.id, true); - - cy.wait(`@deleteItemTag-${ItemTagType.Hidden}`).then( - ({ request: { url } }) => { - expect(url).to.contain(ItemTagType.Hidden); - expect(url).to.contain(item.id); - }, - ); - }); - - it('Cannot hide child of hidden item', () => { - cy.visit(buildItemPath(HIDDEN_ITEM.id)); - cy.get(`#${buildItemMenuButtonId(CHILD_HIDDEN_ITEM.id)}`).click(); - cy.get( - `#${buildItemMenu(CHILD_HIDDEN_ITEM.id)} .${HIDDEN_ITEM_BUTTON_CLASS}`, - ).should(($menuItem) => { - const classList = Array.from($menuItem[0].classList); - // eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions - expect(classList.some((c) => c.includes('disabled'))).to.be.true; - }); - }); +describe('Hide Item', () => { + beforeEach(() => { + cy.setUpApi({ items: [ITEM, HIDDEN_ITEM, CHILD_HIDDEN_ITEM] }); }); - describe('Successfully hide item in Grid', () => { - beforeEach(() => { - cy.setUpApi({ items: [ITEM, HIDDEN_ITEM, CHILD_HIDDEN_ITEM] }); - }); + it('Hide an item', () => { + cy.visit(HOME_PATH); - it('Hide an item', () => { - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - const item = ITEM; + toggleHideButton(ITEM.id, false); - toggleHideButton(item.id, false); - - cy.wait(`@postItemTag-${ItemTagType.Hidden}`).then( - ({ request: { url } }) => { - expect(url).to.contain(ItemTagType.Hidden); - expect(url).to.contain(item.id); - }, - ); - }); - - it('Show an Item', () => { - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - const item = HIDDEN_ITEM; + cy.wait(`@postItemTag-${ItemTagType.Hidden}`).then( + ({ request: { url } }) => { + expect(url).to.contain(ItemTagType.Hidden); + expect(url).to.contain(ITEM.id); + }, + ); + }); - toggleHideButton(item.id, true); + it('Show an item', () => { + cy.visit(HOME_PATH); + const item = HIDDEN_ITEM; - cy.wait(`@deleteItemTag-${ItemTagType.Hidden}`).then( - ({ request: { url } }) => { - expect(url).to.contain(item.id); - expect(url).to.contain(ItemTagType.Hidden); - }, - ); - }); + // make sure to wait for the tags to be fetched + toggleHideButton(item.id, true); - it('Cannot hide child of hidden item', () => { - cy.visit(buildItemPath(HIDDEN_ITEM.id)); - cy.switchMode(ItemLayoutMode.Grid); + cy.wait(`@deleteItemTag-${ItemTagType.Hidden}`).then( + ({ request: { url } }) => { + expect(url).to.contain(ItemTagType.Hidden); + expect(url).to.contain(item.id); + }, + ); + }); - cy.get(`#${buildItemMenuButtonId(CHILD_HIDDEN_ITEM.id)}`).click(); - cy.get( - `#${buildItemMenu(CHILD_HIDDEN_ITEM.id)} .${HIDDEN_ITEM_BUTTON_CLASS}`, - ).should(($menuItem) => { - const classList = Array.from($menuItem[0].classList); - // eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions - expect(classList.some((c) => c.includes('disabled'))).to.be.true; - }); + it('Cannot hide child of hidden item', () => { + cy.visit(buildItemPath(HIDDEN_ITEM.id)); + cy.get(buildItemsGridMoreButtonSelector(CHILD_HIDDEN_ITEM.id)).click(); + cy.get(`.${HIDDEN_ITEM_BUTTON_CLASS}`).should(($menuItem) => { + const classList = Array.from($menuItem[0].classList); + // eslint-disable-next-line no-unused-expressions, @typescript-eslint/no-unused-expressions + expect(classList.some((c) => c.includes('disabled'))).to.be.true; }); }); }); diff --git a/cypress/e2e/item/home/home.cy.ts b/cypress/e2e/item/home/home.cy.ts index caf667136..58d2ff6b5 100644 --- a/cypress/e2e/item/home/home.cy.ts +++ b/cypress/e2e/item/home/home.cy.ts @@ -3,23 +3,23 @@ import { PackedLocalFileItemFactory, } from '@graasp/sdk'; +import { SortingOptions } from '@/components/table/types'; import { ITEM_PAGE_SIZE } from '@/config/constants'; import i18n from '../../../../src/config/i18n'; import { HOME_PATH, ITEMS_PATH } from '../../../../src/config/paths'; import { - ACCESSIBLE_ITEMS_NEXT_PAGE_BUTTON_SELECTOR, ACCESSIBLE_ITEMS_ONLY_ME_ID, - DROPZONE_HELPER_ID, - ITEMS_GRID_PAGINATION_ID, - ITEMS_TABLE_ROW, + CREATE_ITEM_BUTTON_ID, + DROPZONE_SELECTOR, + HOME_LOAD_MORE_BUTTON_SELECTOR, ITEM_SEARCH_INPUT_ID, + SORTING_ORDERING_SELECTOR_ASC, + SORTING_ORDERING_SELECTOR_DESC, + SORTING_SELECT_SELECTOR, buildItemCard, - buildItemsTableRowIdAttribute, - buildItemsTableRowSelector, buildMapViewId, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; import { generateOwnItems } from '../../../fixtures/items'; import { CURRENT_USER } from '../../../fixtures/members'; import { NAVIGATION_LOAD_PAUSE } from '../../../support/constants'; @@ -52,292 +52,173 @@ describe('Home', () => { cy.setUpApi({ items: generateOwnItems(30), }); - i18n.changeLanguage(CURRENT_USER.extra.lang as string); cy.visit(`${HOME_PATH}?mode=map`); cy.get(`#${buildMapViewId()}`, { timeout: 10000 }).should('be.visible'); }); - describe('Grid', () => { - describe('Features', () => { - beforeEach(() => { - cy.setUpApi({ - items: generateOwnItems(30), - }); - i18n.changeLanguage(CURRENT_USER.extra.lang as string); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - }); - - it('Show only created by me checkbox should trigger refetch', () => { - cy.wait('@getAccessibleItems').then(({ request: { url } }) => { - expect(url).not.to.contain(CURRENT_USER.id); - }); + it('visit empty Home', () => { + cy.setUpApi({ + items: [], + }); - cy.get(`#${ACCESSIBLE_ITEMS_ONLY_ME_ID}`).click(); + cy.visit(HOME_PATH); + cy.get(`[role="dropzone"]`).should('be.visible'); + cy.get(`#${CREATE_ITEM_BUTTON_ID}`).should('be.visible'); + }); - cy.wait('@getAccessibleItems').then(({ request: { url } }) => { - expect(url).to.contain(CURRENT_USER.id); - }); + describe('Features', () => { + beforeEach(() => { + cy.setUpApi({ + items: generateOwnItems(30), }); + i18n.changeLanguage(CURRENT_USER.extra.lang as string); + cy.visit(HOME_PATH); + }); - describe('Search', () => { - it('Search should trigger refetch', () => { - const searchText = 'mysearch'; - cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(searchText); - - cy.wait(['@getAccessibleItems', '@getAccessibleItems']).then( - ([ - _first, - { - request: { url }, - }, - ]) => { - expect(url).to.contain(searchText); - }, - ); - }); - - it('Search on second page should reset page number', () => { - const searchText = 'mysearch'; - interceptAccessibleItemsSearch(searchText); - - cy.wait('@getAccessibleItems'); - // navigate to seconde page - cy.get(`#${ITEMS_GRID_PAGINATION_ID} > ul > li`).eq(2).click(); - - cy.wait('@getAccessibleItems').then(({ request: { url } }) => { - expect(url).to.contain('page=2'); - }); - cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(searchText); - - // using our custom interceptor with the search parameter we can distinguish the complete - // search request from possibly other incomplete search requests - cy.wait('@getAccessibleSearch').then(({ request: { query } }) => { - expect(query.name).to.eq(searchText); - expect(query.page).to.eq('1'); - }); - cy.get(`#${buildItemCard(ownItems[0].id)}`).should('be.visible'); - }); + it('Enabling show only created by me should trigger refetch', () => { + cy.wait('@getAccessibleItems').then(({ request: { url } }) => { + expect(url).not.to.contain(CURRENT_USER.id); }); - describe('Pagination', () => { - const checkGridPagination = ( - items: ItemForTest[], - itemsPerPage: number = ITEM_PAGE_SIZE, - ) => { - const numberPages = Math.ceil(items.length / itemsPerPage); - - // for each page - for (let i = 0; i < numberPages; i += 1) { - // navigate to page - cy.get(`#${ITEMS_GRID_PAGINATION_ID} > ul > li`) - .eq(i + 1) // leftmost li is "prev" button - .click(); - // compute items that should be on this page - const shouldDisplay = items.slice( - i * itemsPerPage, - (i + 1) * itemsPerPage, - ); - // compute items that should not be on this page - const shouldNotDisplay = items.filter( - (it) => !shouldDisplay.includes(it), - ); - - shouldDisplay.forEach((item) => { - cy.get(`#${buildItemCard(item.id)}`).should('exist'); - }); - - shouldNotDisplay.forEach((item) => { - cy.get(`#${buildItemCard(item.id)}`).should('not.exist'); - }); - } - }; + cy.get(`#${ACCESSIBLE_ITEMS_ONLY_ME_ID}`).click(); - it('shows only items of each page', () => { - // using default items per page count - checkGridPagination(ownItems); - }); + cy.wait('@getAccessibleItems').then(({ request: { url } }) => { + expect(url).to.contain(CURRENT_USER.id); }); }); - describe('Navigation', () => { - beforeEach(() => { - cy.setUpApi({ items: ITEMS }); - i18n.changeLanguage(CURRENT_USER.extra.lang as string); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); + it('Sorting & ordering', () => { + cy.wait('@getAccessibleItems'); + + cy.get(`${SORTING_SELECT_SELECTOR} input`).should( + 'have.value', + SortingOptions.ItemUpdatedAt, + ); + cy.get(SORTING_ORDERING_SELECTOR_DESC).should('be.visible'); + + // change sorting + cy.get(SORTING_SELECT_SELECTOR).click(); + cy.get('li[data-value="item.name"]').click(); + cy.wait('@getAccessibleItems').then(({ request: { url } }) => { + expect(url).to.contain('item.name'); + expect(url).to.contain('desc'); }); - it('visit Home', () => { - cy.wait('@getAccessibleItems').then(({ response: { body } }) => { - // check item is created and displayed - for (const item of body.data) { - cy.get(`#${buildItemCard(item.id)}`).should('exist'); - } - }); - - // visit child - const { id: childId } = FOLDER; - cy.goToItemInGrid(childId); - - // should get children - cy.wait('@getChildren').then(({ response: { body } }) => { - // check item is created and displayed - for (const item of body) { - cy.get(`#${buildItemCard(item.id)}`).should('exist'); - } - }); - - // visit child - const { id: childChildId } = FOLDER_CHILD; - cy.goToItemInGrid(childChildId); - - // expect dropzone - cy.get(`#${DROPZONE_HELPER_ID}`).should('exist'); - - // return parent with navigation and should display children - cy.wait(NAVIGATION_LOAD_PAUSE); - cy.goToItemWithNavigation(childId); - // should get children - cy.wait('@getChildren').then(() => { - // check item is created and displayed - for (const item of [IMAGE_ITEM_CHILD, FOLDER_CHILD]) { - cy.get(`#${buildItemCard(item.id)}`).should('exist'); - } - }); + // change ordering + cy.get(SORTING_ORDERING_SELECTOR_DESC).click(); + cy.get(SORTING_ORDERING_SELECTOR_ASC).should('be.visible'); + cy.wait('@getAccessibleItems').then(({ request: { url } }) => { + expect(url).to.contain('asc'); }); }); - }); - describe('List', () => { - describe('Navigation', () => { - beforeEach(() => { - cy.setUpApi({ items: ITEMS }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - }); + describe('Search', () => { + it('Search should trigger refetch', () => { + cy.wait('@getAccessibleItems'); - it('visit Home', () => { - // visit child - const { id: childId } = FOLDER; - cy.goToItemInList(childId); + const searchText = 'mysearch'; + cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(searchText); - // should get children - cy.wait('@getChildren').then(({ response: { body } }) => { - // check item is created and displayed - for (const item of body) { - cy.get(buildItemsTableRowIdAttribute(item.id)).should('exist'); - } + cy.wait('@getAccessibleItems').then(({ request: { url } }) => { + expect(url).to.contain(searchText); }); + }); - // visit child - const { id: childChildId } = FOLDER_CHILD; - cy.goToItemInList(childChildId); + it('Search on second page should reset page number', () => { + const searchText = 'mysearch'; + interceptAccessibleItemsSearch(searchText); - // expect no children - cy.get(ITEMS_TABLE_ROW).should('not.exist'); + cy.wait('@getAccessibleItems'); + // navigate to second page + cy.get(HOME_LOAD_MORE_BUTTON_SELECTOR).click(); - // return parent with navigation and should display children - cy.goToItemWithNavigation(childId); - // should get children - cy.wait('@getChildren').then(({ response: { body } }) => { - // check item is created and displayed - for (const item of body) { - cy.get(buildItemsTableRowIdAttribute(item.id)).should('exist'); - } + cy.wait('@getAccessibleItems').then(({ request: { url } }) => { + expect(url).to.contain('page=2'); }); - }); - }); + cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(searchText); - describe('Features', () => { - beforeEach(() => { - cy.setUpApi({ - items: generateOwnItems(30), + // using our custom interceptor with the search parameter we can distinguish the complete + // search request from possibly other incomplete search requests + cy.wait('@getAccessibleSearch').then(({ request: { query } }) => { + expect(query.name).to.eq(searchText); + expect(query.page).to.eq('1'); }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); + cy.get(`#${buildItemCard(ownItems[0].id)}`).should('be.visible'); }); + }); - it('Show only created by me checkbox should trigger refetch', () => { - cy.wait('@getAccessibleItems').then(({ request: { url } }) => { - expect(url).not.to.contain(CURRENT_USER.id); - }); + describe('Pagination', () => { + const checkGridPagination = ( + items: ItemForTest[], + itemsPerPage: number = ITEM_PAGE_SIZE, + ) => { + const numberPages = Math.ceil(items.length / itemsPerPage); + // for each page + for (let i = 0; i < numberPages; i += 1) { + // compute items that should be on this page + const shouldDisplay = items.slice(0, (i + 1) * itemsPerPage); + + shouldDisplay.forEach((item) => { + cy.get(`#${buildItemCard(item.id)}`).should('exist'); + }); - cy.get(`#${ACCESSIBLE_ITEMS_ONLY_ME_ID}`).click(); + // navigate to page + // button does not exist for last "page" + if (i !== numberPages - 1) { + cy.get(HOME_LOAD_MORE_BUTTON_SELECTOR).click(); + } + } + }; - cy.wait('@getAccessibleItems').then(({ request: { url } }) => { - expect(url).to.contain(CURRENT_USER.id); - }); + it('shows only items of each page', () => { + // using default items per page count + checkGridPagination(ownItems); }); + }); + }); - describe('Search', () => { - it('Search should trigger refetch', () => { - const searchText = 'mysearch'; - cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(searchText); - - cy.wait(['@getAccessibleItems', '@getAccessibleItems']).then( - ([ - _first, - { - request: { url }, - }, - ]) => { - expect(url).to.contain(searchText); - }, - ); - }); - - it('Search on second page should reset page number', () => { - const searchText = 'mysearch'; - interceptAccessibleItemsSearch(searchText); - - cy.wait('@getAccessibleItems'); - // navigate to second page - cy.get(ACCESSIBLE_ITEMS_NEXT_PAGE_BUTTON_SELECTOR).click(); + describe('Navigation', () => { + it('visit Home', () => { + cy.setUpApi({ items: ITEMS }); + i18n.changeLanguage(CURRENT_USER.extra.lang as string); + cy.visit(HOME_PATH); + + cy.wait('@getAccessibleItems').then(({ response: { body } }) => { + // check item is created and displayed + for (const item of body.data) { + cy.get(`#${buildItemCard(item.id)}`).should('be.visible'); + } + }); - cy.wait('@getAccessibleItems').then(({ request: { url } }) => { - expect(url).to.contain('page=2'); - }); - cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(searchText); + // visit child + const { id: childId } = FOLDER; + cy.goToItemInCard(childId); - cy.wait('@getAccessibleSearch').then(({ request: { query } }) => { - expect(query.name).to.eq(searchText); - expect(query.page).to.eq('1'); - }); - }); + // should get children + cy.wait('@getChildren').then(({ response: { body } }) => { + // check item is created and displayed + for (const item of body) { + cy.get(`#${buildItemCard(item.id)}`).should('be.visible'); + } }); - describe('Pagination', () => { - const items = generateOwnItems(30); - const numberPages = Math.ceil(items.length / ITEM_PAGE_SIZE); - - it('shows only items of each page', () => { - // for each page - for (let i = 0; i < numberPages; i += 1) { - // compute items that should be on this page - const shouldDisplay = items.slice( - i * ITEM_PAGE_SIZE, - (i + 1) * ITEM_PAGE_SIZE, - ); - // compute items that should not be on this page - const shouldNotDisplay = items.filter( - (it) => !shouldDisplay.includes(it), - ); - - shouldDisplay.forEach((item) => { - cy.get(buildItemsTableRowSelector(item.id)).should('exist'); - }); - - shouldNotDisplay.forEach((item) => { - cy.get(buildItemsTableRowSelector(item.id)).should('not.exist'); - }); - // navigate to next page - if (i !== numberPages - 1) { - cy.get(ACCESSIBLE_ITEMS_NEXT_PAGE_BUTTON_SELECTOR).click(); - } - } - }); + // visit child + const { id: childChildId } = FOLDER_CHILD; + cy.goToItemInCard(childChildId); + + // expect dropzone + cy.get(DROPZONE_SELECTOR).should('exist'); + + // return parent with navigation and should display children + cy.wait(NAVIGATION_LOAD_PAUSE); + cy.goToItemWithNavigation(childId); + // should get children + cy.wait('@getChildren').then(() => { + // check item is created and displayed + for (const item of [IMAGE_ITEM_CHILD, FOLDER_CHILD]) { + cy.get(`#${buildItemCard(item.id)}`).should('exist'); + } }); }); }); diff --git a/cypress/e2e/item/home/layoutMode.cy.ts b/cypress/e2e/item/home/layoutMode.cy.ts new file mode 100644 index 000000000..4e15045a2 --- /dev/null +++ b/cypress/e2e/item/home/layoutMode.cy.ts @@ -0,0 +1,53 @@ +import { HOME_PATH } from '@/config/paths'; +import { buildItemCard, buildMapViewId } from '@/config/selectors'; +import { ItemLayoutMode } from '@/enums'; + +import { generateOwnItems } from '../../../fixtures/items'; + +const ITEMS = generateOwnItems(30); + +describe('Home screen', () => { + beforeEach(() => { + cy.setUpApi({ + items: ITEMS, + }); + }); + + it('Home screen list layout mode', () => { + cy.visit(HOME_PATH); + + // default mode is list + cy.get(`#${buildItemCard(ITEMS[0].id)}`); + + // go to map + cy.switchMode(ItemLayoutMode.Map); + cy.get(`#${buildMapViewId()}`, { timeout: 10000 }).should('be.visible'); + + // go to list + cy.switchMode(ItemLayoutMode.List); + cy.get(`#${buildItemCard(ITEMS[0].id)}`); + }); + + it('Home screen grid layout mode', () => { + cy.visit(HOME_PATH); + + // go to grid + cy.switchMode(ItemLayoutMode.Grid); + cy.get(`#${buildItemCard(ITEMS[0].id)}`); + }); + + it('Home screen map layout mode', () => { + cy.visit(HOME_PATH); + + // go to map + cy.switchMode(ItemLayoutMode.Map); + cy.get(`#${buildMapViewId()}`, { timeout: 10000 }).should('be.visible'); + }); + + it('visit Home on map by default', () => { + // access map directly + cy.visit(`${HOME_PATH}?mode=map`); + + cy.get(`#${buildMapViewId()}`, { timeout: 10000 }).should('be.visible'); + }); +}); diff --git a/cypress/e2e/item/move/gridMoveItem.cy.ts b/cypress/e2e/item/move/gridMoveItem.cy.ts deleted file mode 100644 index 647fa3fbd..000000000 --- a/cypress/e2e/item/move/gridMoveItem.cy.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { - PackedFolderItemFactory, - PackedLocalFileItemFactory, -} from '@graasp/sdk'; - -import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; -import { - ITEM_MENU_MOVE_BUTTON_CLASS, - MY_GRAASP_ITEM_PATH, - buildItemMenu, - buildItemMenuButtonId, -} from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; - -const moveItem = ({ - id: movedItemId, - toItemPath, -}: { - id: string; - toItemPath: string; -}) => { - const menuSelector = `#${buildItemMenuButtonId(movedItemId)}`; - cy.get(menuSelector).click(); - cy.get( - `#${buildItemMenu(movedItemId)} .${ITEM_MENU_MOVE_BUTTON_CLASS}`, - ).click(); - - cy.handleTreeMenu(toItemPath); -}; - -const IMAGE_ITEM = PackedLocalFileItemFactory(); -const FOLDER = PackedFolderItemFactory(); -const IMAGE_ITEM_CHILD = PackedLocalFileItemFactory({ parentItem: FOLDER }); -const FOLDER2 = PackedFolderItemFactory(); - -const items = [IMAGE_ITEM, FOLDER, FOLDER2, IMAGE_ITEM_CHILD]; - -describe('Move Item in Grid', () => { - it('move item from Home', () => { - cy.setUpApi({ items }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - - // move - const { id: movedItem } = FOLDER2; - const { id: toItem, path: toItemPath } = FOLDER; - moveItem({ id: movedItem, toItemPath }); - - cy.wait('@moveItems').then(({ request: { url, body } }) => { - expect(body.parentId).to.equal(toItem); - expect(url).to.contain(movedItem); - }); - }); - - it('move item from item', () => { - cy.setUpApi({ items }); - - // go to children item - cy.visit(buildItemPath(FOLDER.id)); - cy.switchMode(ItemLayoutMode.Grid); - - // move - const { id: movedItem } = IMAGE_ITEM_CHILD; - const { id: toItem, path: toItemPath } = FOLDER2; - moveItem({ id: movedItem, toItemPath }); - - cy.wait('@moveItems').then(({ request: { body, url } }) => { - expect(body.parentId).to.equal(toItem); - expect(url).to.contain(movedItem); - }); - }); - - it('move item to Home', () => { - cy.setUpApi({ items }); - - // go to children item - cy.visit(buildItemPath(FOLDER.id)); - cy.switchMode(ItemLayoutMode.Grid); - - // move - const { id: movedItem } = IMAGE_ITEM_CHILD; - moveItem({ id: movedItem, toItemPath: MY_GRAASP_ITEM_PATH }); - - cy.wait('@moveItems').then(({ request: { body, url } }) => { - expect(body.parentId).to.equal(undefined); - expect(url).to.contain(movedItem); - }); - }); -}); diff --git a/cypress/e2e/item/move/listMoveMultiple.cy.ts b/cypress/e2e/item/move/listMoveMultiple.cy.ts deleted file mode 100644 index 09e3befbb..000000000 --- a/cypress/e2e/item/move/listMoveMultiple.cy.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - PackedFolderItemFactory, - PackedLocalFileItemFactory, -} from '@graasp/sdk'; - -import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; -import { - ITEMS_TABLE_MOVE_SELECTED_ITEMS_ID, - MY_GRAASP_ITEM_PATH, - buildItemsTableRowIdAttribute, -} from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; - -const moveItems = ({ - itemIds, - toItemPath, -}: { - itemIds: string[]; - toItemPath: string; -}) => { - // check selected ids - itemIds.forEach((id) => { - cy.get(`${buildItemsTableRowIdAttribute(id)} input`).click(); - }); - - cy.get(`#${ITEMS_TABLE_MOVE_SELECTED_ITEMS_ID}`).click(); - cy.handleTreeMenu(toItemPath); -}; - -const IMAGE_ITEM = PackedLocalFileItemFactory(); -const FOLDER = PackedFolderItemFactory(); -const IMAGE_ITEM_CHILD = PackedLocalFileItemFactory({ parentItem: FOLDER }); -const IMAGE_ITEM_CHILD2 = PackedLocalFileItemFactory({ parentItem: FOLDER }); -const FOLDER_CHILD = PackedFolderItemFactory({ parentItem: FOLDER }); -const FOLDER2 = PackedFolderItemFactory(); -const FOLDER3 = PackedFolderItemFactory(); - -const items = [ - IMAGE_ITEM, - FOLDER, - FOLDER3, - FOLDER2, - IMAGE_ITEM_CHILD, - IMAGE_ITEM_CHILD2, - FOLDER_CHILD, -]; - -describe('Move Items in List', () => { - it('Move items on Home', () => { - cy.setUpApi({ items }); - cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); - - // move - const itemIds = [FOLDER3.id, FOLDER2.id]; - const { id: toItem, path: toItemPath } = FOLDER; - moveItems({ itemIds, toItemPath }); - - cy.wait('@moveItems').then(({ request: { url, body } }) => { - expect(body.parentId).to.equal(toItem); - itemIds.forEach((movedItem) => expect(url).to.contain(movedItem)); - }); - }); - - it('Move items in item', () => { - cy.setUpApi({ items }); - const { id: start } = FOLDER; - - // go to children item - cy.visit(buildItemPath(start)); - - cy.switchMode(ItemLayoutMode.List); - - // move - const itemIds = [IMAGE_ITEM_CHILD.id, IMAGE_ITEM_CHILD2.id]; - const { id: toItem, path: toItemPath } = FOLDER_CHILD; - moveItems({ itemIds, toItemPath }); - - cy.wait('@moveItems').then(({ request: { body, url } }) => { - expect(body.parentId).to.equal(toItem); - itemIds.forEach((movedItem) => expect(url).to.contain(movedItem)); - }); - }); - - it('Move items to Home', () => { - cy.setUpApi({ items }); - const { id: start } = FOLDER; - - // go to children item - cy.visit(buildItemPath(start)); - - cy.switchMode(ItemLayoutMode.List); - - // move - const itemIds = [IMAGE_ITEM_CHILD.id, IMAGE_ITEM_CHILD2.id]; - moveItems({ itemIds, toItemPath: MY_GRAASP_ITEM_PATH }); - - cy.wait('@moveItems').then(({ request: { body, url } }) => { - expect(body.parentId).to.equal(undefined); - itemIds.forEach((movedItem) => expect(url).to.contain(movedItem)); - - // TODO: this is still selected if we do not get the feedbacks - // commenting it for now, but should be fixed in the future - // itemIds.forEach((id) => { - // cy.get(`${buildItemsTableRowIdAttribute(id)}`).should('not.exist'); - // }); - }); - }); -}); diff --git a/cypress/e2e/item/move/listMoveItem.cy.ts b/cypress/e2e/item/move/moveItem.cy.ts similarity index 85% rename from cypress/e2e/item/move/listMoveItem.cy.ts rename to cypress/e2e/item/move/moveItem.cy.ts index 4b3d1e256..b6a379d1f 100644 --- a/cypress/e2e/item/move/listMoveItem.cy.ts +++ b/cypress/e2e/item/move/moveItem.cy.ts @@ -7,11 +7,9 @@ import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { ITEM_MENU_MOVE_BUTTON_CLASS, MY_GRAASP_ITEM_PATH, - buildItemMenu, - buildItemMenuButtonId, + buildItemsGridMoreButtonSelector, buildNavigationModalItemId, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; const IMAGE_ITEM = PackedLocalFileItemFactory(); const FOLDER = PackedFolderItemFactory(); @@ -22,12 +20,8 @@ const FOLDER2 = PackedFolderItemFactory(); const items = [IMAGE_ITEM, FOLDER, FOLDER2, CHILD, CHILD_CHILD]; const openMoveModal = ({ id: movedItemId }: { id: string }) => { - // todo: remove on table refactor - cy.wait(1000); - cy.get(`#${buildItemMenuButtonId(movedItemId)}`).click(); - cy.get( - `#${buildItemMenu(movedItemId)} .${ITEM_MENU_MOVE_BUTTON_CLASS}`, - ).click(); + cy.get(buildItemsGridMoreButtonSelector(movedItemId)).click(); + cy.get(`.${ITEM_MENU_MOVE_BUTTON_CLASS}`).click(); }; const moveItem = ({ @@ -43,13 +37,11 @@ const moveItem = ({ cy.handleTreeMenu(toItemPath, rootId); }; -describe('Move Item in List', () => { +describe('Move Item', () => { it('move item on Home', () => { cy.setUpApi({ items }); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - // move const { id: movedItem } = FOLDER2; const { id: toItem, path: toItemPath } = FOLDER; @@ -68,8 +60,6 @@ describe('Move Item in List', () => { // go to children item cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // move const { id: movedItem } = CHILD; const { id: toItem, path: toItemPath } = FOLDER2; @@ -88,8 +78,6 @@ describe('Move Item in List', () => { // go to children item cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - const { id: movedItemId } = CHILD; const { id: parentId } = FOLDER; const { id: childId } = CHILD_CHILD; @@ -121,8 +109,6 @@ describe('Move Item in List', () => { // go to children item cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // move const { id: movedItem } = CHILD; moveItem({ id: movedItem, toItemPath: MY_GRAASP_ITEM_PATH }); diff --git a/cypress/e2e/item/order/reorderItems.cy.ts b/cypress/e2e/item/order/reorderItems.cy.ts index 195a2c65a..7c04e951e 100644 --- a/cypress/e2e/item/order/reorderItems.cy.ts +++ b/cypress/e2e/item/order/reorderItems.cy.ts @@ -1,13 +1,8 @@ -import { PackedFolderItemFactory } from '@graasp/sdk'; +import { HttpMethod, PackedFolderItemFactory } from '@graasp/sdk'; import { buildItemPath } from '../../../../src/config/paths'; -import { - ROW_DRAGGER_CLASS, - buildItemsTableId, - buildItemsTableRowId, - buildItemsTableRowSelector, -} from '../../../../src/config/selectors'; -import { ROW_HEIGHT } from '../../../support/constants'; +import { buildItemsTableId } from '../../../../src/config/selectors'; +import { ID_FORMAT } from '../../../support/utils'; const PARENT = PackedFolderItemFactory(); const CHILDREN = [ @@ -16,88 +11,36 @@ const CHILDREN = [ PackedFolderItemFactory({ parentItem: PARENT }), ]; const ITEM_REORDER_ITEMS = [PARENT, ...CHILDREN]; - -const reorderAndCheckItem = ( - id: string, - currentPosition: number, - newPosition: number, -) => { - const dragIcon = `${buildItemsTableRowSelector( - id, - )} .${ROW_DRAGGER_CLASS} svg`; - - cy.wait(['@getItem', '@getChildren', '@getItemMemberships']); - - cy.dragAndDrop(dragIcon, 0, (newPosition - currentPosition) * ROW_HEIGHT); - - cy.wait('@editItem').then( - ({ - response: { - body: { extra }, - }, - }) => { - expect(extra.folder.childrenOrder[newPosition]).to.equal(id); - }, - ); -}; +const API_HOST = Cypress.env('VITE_GRAASP_API_HOST'); describe('Order Items', () => { - describe('Move Item', () => { - beforeEach(() => { - cy.setUpApi({ - items: ITEM_REORDER_ITEMS, - }); - cy.visit(buildItemPath(PARENT.id)); - }); - - // flaky test is skipped - it.skip('move item to a spot below', () => { - const currentPosition = 0; - const newPosition = 1; - - const { id: childId } = CHILDREN[currentPosition]; - - reorderAndCheckItem(childId, currentPosition, newPosition); - }); + // todo/bug: difficult to test reordering with drag and drop - // flaky test is skipped - it.skip('move first item to last spot', () => { - const currentPosition = 0; - const newPosition = 2; - - const { id: childId } = CHILDREN[currentPosition]; - - reorderAndCheckItem(childId, currentPosition, newPosition); - }); - - // flaky test is skipped - it.skip('move middle item to top spot', () => { - const currentPosition = 1; - const newPosition = 0; - - const { id: childId } = CHILDREN[currentPosition]; - - reorderAndCheckItem(childId, currentPosition, newPosition); - }); - }); - - describe('Check Order', () => { - it('check item order in folder with non-existing item in ordering', () => { + describe('Check default order', () => { + it('check item order in folder', () => { cy.setUpApi({ items: ITEM_REORDER_ITEMS, }); - cy.visit(buildItemPath(PARENT.id)); + // mock children call to return ordered items since order is premade by backend + const orderedItems = [CHILDREN[1], CHILDREN[0], CHILDREN[2]]; + cy.intercept( + { + method: HttpMethod.Get, + url: new RegExp(`${API_HOST}/items/${ID_FORMAT}/children`), + }, + ({ reply }) => reply(orderedItems), + ).as('getChildren'); - const tableBody = `#${buildItemsTableId(PARENT.id)}`; + cy.visit(buildItemPath(PARENT.id)); - CHILDREN.forEach(({ id }, index) => { - // this will find multiple row instances because ag-grid renders several - // this should be okay as all of them should have the same row-index - cy.get(tableBody) - .find(`[row-id=${buildItemsTableRowId(id)}]`) - .should('have.attr', 'row-index', index); - }); + cy.get(`#${buildItemsTableId(PARENT.id)}`) + .find(`h5`) + .then(($elements) => { + for (let i = 0; i < $elements.length; i += 1) { + expect($elements[i].innerText).to.equal(orderedItems[i].name); + } + }); }); }); }); diff --git a/cypress/e2e/item/pin/pinItem.cy.ts b/cypress/e2e/item/pin/pinItem.cy.ts index 7403721c5..fcc59c3b5 100644 --- a/cypress/e2e/item/pin/pinItem.cy.ts +++ b/cypress/e2e/item/pin/pinItem.cy.ts @@ -2,112 +2,73 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { + ITEM_HEADER_ID, PIN_ITEM_BUTTON_CLASS, buildDownloadButtonId, - buildItemMenu, - buildItemMenuButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; const togglePinButton = (itemId: string) => { - // todo: remove on table refactor - cy.wait(500); - cy.get(`#${buildItemMenuButtonId(itemId)}`).click(); - cy.get(`#${buildItemMenu(itemId)} .${PIN_ITEM_BUTTON_CLASS}`).click(); + cy.get(buildItemsGridMoreButtonSelector(itemId)).click(); + cy.get(`.${PIN_ITEM_BUTTON_CLASS}`).click(); }; const PINNED_ITEM = PackedFolderItemFactory({ settings: { isPinned: true } }); const ITEM = PackedFolderItemFactory({ settings: { isPinned: false } }); describe('Anonymous', () => { - const itemId = ITEM.id; + const PUBLIC_TTEM = PackedFolderItemFactory( + { settings: { isPinned: false } }, + { permission: null, publicTag: {} }, + ); + const itemId = PUBLIC_TTEM.id; beforeEach(() => { - cy.setUpApi({ currentMember: null, items: [ITEM] }); + cy.setUpApi({ currentMember: null, items: [PUBLIC_TTEM] }); cy.visit(buildItemPath(itemId)); }); it("Can see item but can't pin", () => { cy.get(`#${buildDownloadButtonId(itemId)}`).should('be.visible'); - cy.get(`#${buildItemMenuButtonId(itemId)}`).should('not.exist'); + cy.get(`#${ITEM_HEADER_ID} [data-testid="MoreVertIcon"]`).should( + 'not.exist', + ); }); }); describe('Pinning Item', () => { - describe('Successfully pinning item in List', () => { - beforeEach(() => { - cy.setUpApi({ items: [PINNED_ITEM, ITEM] }); - cy.visit(HOME_PATH); - }); - - it('Pin an item', () => { - const item = ITEM; - - togglePinButton(item.id); - - cy.wait(`@editItem`).then( - ({ - request: { - body: { settings }, - }, - }) => { - expect(settings.isPinned).to.equals(true); - }, - ); - }); - - it('Unpin Item', () => { - const item = PINNED_ITEM; - - togglePinButton(item.id); - - cy.wait('@editItem').then( - ({ - request: { - body: { settings }, - }, - }) => { - expect(settings.isPinned).to.equals(false); - }, - ); - }); + beforeEach(() => { + cy.setUpApi({ items: [PINNED_ITEM, ITEM] }); + cy.visit(HOME_PATH); }); - describe('Successfully pinning item in Grid', () => { - beforeEach(() => { - cy.setUpApi({ items: [PINNED_ITEM, ITEM] }); - cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.Grid); - }); + it('Pin an item', () => { + const item = ITEM; - it('Pin an item', () => { - const item = ITEM; + togglePinButton(item.id); - togglePinButton(item.id); - - cy.wait(`@editItem`).then( - ({ - request: { - body: { settings }, - }, - }) => { - expect(settings.isPinned).to.equals(true); + cy.wait(`@editItem`).then( + ({ + request: { + body: { settings }, }, - ); - }); + }) => { + expect(settings.isPinned).to.equals(true); + }, + ); + }); - it('Unpin Item', () => { - const item = PINNED_ITEM; + it('Unpin Item', () => { + const item = PINNED_ITEM; - togglePinButton(item.id); + togglePinButton(item.id); - cy.wait('@editItem').then( - ({ - request: { - body: { settings }, - }, - }) => { - expect(settings.isPinned).to.equals(false); + cy.wait('@editItem').then( + ({ + request: { + body: { settings }, }, - ); - }); + }) => { + expect(settings.isPinned).to.equals(false); + }, + ); }); }); diff --git a/cypress/e2e/item/publish/viewPublished.cy.ts b/cypress/e2e/item/publish/viewPublished.cy.ts new file mode 100644 index 000000000..a8341386e --- /dev/null +++ b/cypress/e2e/item/publish/viewPublished.cy.ts @@ -0,0 +1,116 @@ +import { PackedFolderItemFactory } from '@graasp/sdk'; + +import { SortingOptions } from '@/components/table/types'; +import { BUILDER } from '@/langs/constants'; + +import i18n, { BUILDER_NAMESPACE } from '../../../../src/config/i18n'; +import { PUBLISHED_ITEMS_PATH } from '../../../../src/config/paths'; +import { + CREATE_ITEM_BUTTON_ID, + ITEM_SEARCH_INPUT_ID, + PUBLISHED_ITEMS_ERROR_ALERT_ID, + PUBLISHED_ITEMS_ID, + SORTING_ORDERING_SELECTOR_ASC, + SORTING_ORDERING_SELECTOR_DESC, + SORTING_SELECT_SELECTOR, + buildItemCard, +} from '../../../../src/config/selectors'; +import { PublishedItemFactory } from '../../../fixtures/items'; +import { CURRENT_USER } from '../../../fixtures/members'; + +const items = [ + PublishedItemFactory(PackedFolderItemFactory({ creator: CURRENT_USER })), + PublishedItemFactory(PackedFolderItemFactory({ creator: CURRENT_USER })), + PublishedItemFactory(PackedFolderItemFactory({ creator: CURRENT_USER })), +]; +const publishedItemData = items.map(({ published }) => published); + +describe('Published Items', () => { + describe('Member has no published items', () => { + it('Show empty table', () => { + cy.setUpApi({ + items: [PackedFolderItemFactory()], + }); + cy.visit(PUBLISHED_ITEMS_PATH); + i18n.changeLanguage(CURRENT_USER.extra.lang as string); + const text = i18n.t(BUILDER.PUBLISHED_ITEMS_EMPTY, { + ns: BUILDER_NAMESPACE, + }); + cy.get(`#${PUBLISHED_ITEMS_ID}`).should('contain', text); + }); + }); + + describe('Member has recycled items', () => { + beforeEach(() => { + cy.setUpApi({ + items, + publishedItemData, + }); + i18n.changeLanguage(CURRENT_USER.extra.lang as string); + cy.visit(PUBLISHED_ITEMS_PATH); + }); + + it('Empty search', () => { + i18n.changeLanguage(CURRENT_USER.extra.lang as string); + const searchText = 'mysearch'; + cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(searchText); + const text = i18n.t(BUILDER.PUBLISHED_ITEMS_NOT_FOUND_SEARCH, { + search: searchText, + ns: BUILDER_NAMESPACE, + }); + cy.get(`#${PUBLISHED_ITEMS_ID}`).should('contain', text); + }); + + it('New button should not exist', () => { + cy.get(`#${CREATE_ITEM_BUTTON_ID}`).should('not.exist'); + }); + + it('check recycled item layout', () => { + for (const { id } of items) { + cy.get(`#${buildItemCard(id)}`).should('be.visible'); + } + }); + + it('Sorting & Ordering', () => { + cy.get(`${SORTING_SELECT_SELECTOR} input`).should( + 'have.value', + SortingOptions.ItemUpdatedAt, + ); + cy.get(SORTING_ORDERING_SELECTOR_DESC).should('be.visible'); + + cy.get(SORTING_SELECT_SELECTOR).click(); + cy.get('li[data-value="item.name"]').click(); + + // check items are ordered by name + cy.get(`#${PUBLISHED_ITEMS_ID} h5`).then(($e) => { + items.sort((a, b) => (a.name < b.name ? 1 : -1)); + for (let idx = 0; idx < items.length; idx += 1) { + expect($e[idx].innerText).to.eq(items[idx].name); + } + }); + + // change ordering + cy.get(SORTING_ORDERING_SELECTOR_DESC).click(); + cy.get(SORTING_ORDERING_SELECTOR_ASC).should('be.visible'); + cy.get(`#${PUBLISHED_ITEMS_ID} h5`).then(($e) => { + items.reverse(); + for (let idx = 0; idx < items.length; idx += 1) { + expect($e[idx].innerText).to.eq(items[idx].name); + } + }); + }); + }); + + describe('Error Handling', () => { + it('check recycled item layout with server error', () => { + cy.setUpApi({ + items, + publishedItemData, + getPublishedItems: true, + }); + cy.visit(PUBLISHED_ITEMS_PATH); + + cy.get(`#${PUBLISHED_ITEMS_ERROR_ALERT_ID}`).should('exist'); + }); + }); +}); diff --git a/cypress/e2e/item/settings/itemSettings.cy.ts b/cypress/e2e/item/settings/itemSettings.cy.ts index b18141ab0..64178b756 100644 --- a/cypress/e2e/item/settings/itemSettings.cy.ts +++ b/cypress/e2e/item/settings/itemSettings.cy.ts @@ -33,8 +33,7 @@ import { SETTINGS_PINNED_TOGGLE_ID, SETTINGS_SAVE_ACTIONS_TOGGLE_ID, buildDescriptionPlacementId, - buildItemMenu, - buildItemMenuButtonId, + buildItemsGridMoreButtonSelector, buildSettingsButtonId, } from '../../../../src/config/selectors'; import { ITEM_WITH_CHATBOX_MESSAGES } from '../../../fixtures/chatbox'; @@ -64,7 +63,8 @@ describe('Item Settings', () => { it('settings page redirects to item', () => { // manual click to verify settings button works correctly cy.visit(buildItemSettingsPath(item.id)); - cy.get(`.${ITEM_MAIN_CLASS}`).should('contain', item.name); + // name could have ellipsis + cy.get(`.${ITEM_MAIN_CLASS}`).should('contain', item.name.slice(0, 10)); }); }); @@ -482,18 +482,6 @@ describe('Item Settings', () => { }); describe('in item menu', () => { - const openItemMenu = (itemId: string) => { - cy.get(`#${buildItemMenuButtonId(itemId)}`).click(); - // There is a weird behaviour that scroll on menu button click, causing the close of the menu item. - // To avoid that, the menu button is click again if the menu is not visible. - cy.get(`#${buildItemMenu(itemId)}`).then(($itemMenu) => { - // If the item menu is not visible, click on the button again - if (!$itemMenu.is(':visible')) { - cy.get(`#${buildItemMenuButtonId(itemId)}`).click(); - } - }); - cy.get(`#${buildItemMenu(itemId)}`).should('be.visible'); - }; describe('read', () => { const item = PackedFolderItemFactory( {}, @@ -508,7 +496,7 @@ describe('Item Settings', () => { cy.visit('/'); }); it('does not have access to settings', () => { - openItemMenu(itemId); + cy.get(buildItemsGridMoreButtonSelector(itemId)).click(); cy.get(`#${buildSettingsButtonId(itemId)}`).should('not.exist'); }); }); @@ -526,7 +514,7 @@ describe('Item Settings', () => { cy.visit('/'); }); it('has access to settings', () => { - openItemMenu(itemId); + cy.get(buildItemsGridMoreButtonSelector(itemId)).click(); cy.get(`#${buildSettingsButtonId(itemId)}`).should('be.visible'); cy.get(`#${buildSettingsButtonId(itemId)}`).click(); cy.url().should('contain', buildItemSettingsPath(itemId)); diff --git a/cypress/e2e/item/share/itemLogin.cy.ts b/cypress/e2e/item/share/itemLogin.cy.ts index 25f3226f2..47cc74b5d 100644 --- a/cypress/e2e/item/share/itemLogin.cy.ts +++ b/cypress/e2e/item/share/itemLogin.cy.ts @@ -7,16 +7,11 @@ import { import { v4 } from 'uuid'; -import { - SETTINGS, - SETTINGS_ITEM_LOGIN_DEFAULT, -} from '../../../../src/config/constants'; +import { SETTINGS_ITEM_LOGIN_DEFAULT } from '../../../../src/config/constants'; import { buildItemPath } from '../../../../src/config/paths'; import { ITEM_LOGIN_SCREEN_FORBIDDEN_ID, ITEM_LOGIN_SIGN_IN_BUTTON_ID, - ITEM_LOGIN_SIGN_IN_MEMBER_ID_ID, - ITEM_LOGIN_SIGN_IN_MODE_ID, ITEM_LOGIN_SIGN_IN_PASSWORD_ID, ITEM_LOGIN_SIGN_IN_USERNAME_ID, SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID, @@ -39,11 +34,6 @@ const addItemLoginSchema = ( }, }); -const changeSignInMode = (mode: string) => { - cy.get(`#${ITEM_LOGIN_SIGN_IN_MODE_ID}`).click(); - cy.get(`li[data-value="${mode}"]`).click(); -}; - const checkItemLoginScreenLayout = ( itemLoginSchema: | ItemLoginSchemaType @@ -59,19 +49,11 @@ const checkItemLoginScreenLayout = ( const fillItemLoginScreenLayout = ({ username, password, - memberId, }: { username?: string; password?: string; - memberId?: string; }) => { - if (!memberId) { - changeSignInMode(SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.PSEUDONYM); - cy.get(`#${ITEM_LOGIN_SIGN_IN_USERNAME_ID}`).clear().type(username); - } else { - changeSignInMode(SETTINGS.ITEM_LOGIN.SIGN_IN_MODE.MEMBER_ID); - cy.get(`#${ITEM_LOGIN_SIGN_IN_MEMBER_ID_ID}`).clear().type(memberId); - } + cy.get(`#${ITEM_LOGIN_SIGN_IN_USERNAME_ID}`).clear().type(username); if (password) { cy.get(`#${ITEM_LOGIN_SIGN_IN_PASSWORD_ID}`).clear().type(password); @@ -124,7 +106,7 @@ describe('Item Login', () => { describe('User is signed out', () => { describe('Display Item Login Screen', () => { - it('username or member id', () => { + it('username', () => { const item = addItemLoginSchema( PackedFolderItemFactory({}, { permission: null }), ItemLoginSchemaType.Username, @@ -138,19 +120,13 @@ describe('Item Login', () => { }); cy.wait('@postItemLogin'); - // use memberid - fillItemLoginScreenLayout({ - memberId: v4(), - }); - cy.wait('@postItemLogin'); - // use username to check no member id is incorrectly sent fillItemLoginScreenLayout({ username: 'username', }); cy.wait('@postItemLogin'); }); - it('username or member id and password', () => { + it('username and password', () => { const item = addItemLoginSchema( PackedFolderItemFactory({}, { permission: null }), ItemLoginSchemaType.UsernameAndPassword, @@ -165,13 +141,6 @@ describe('Item Login', () => { }); cy.wait('@postItemLogin'); - // use memberid - fillItemLoginScreenLayout({ - memberId: v4(), - password: 'password', - }); - cy.wait('@postItemLogin'); - // use username to check no member id is incorrectly sent fillItemLoginScreenLayout({ username: 'username', diff --git a/cypress/e2e/item/delete/listDeleteItem.cy.ts b/cypress/e2e/item/trash/deleteItem.cy.ts similarity index 73% rename from cypress/e2e/item/delete/listDeleteItem.cy.ts rename to cypress/e2e/item/trash/deleteItem.cy.ts index a98326bcd..d0bdc875f 100644 --- a/cypress/e2e/item/delete/listDeleteItem.cy.ts +++ b/cypress/e2e/item/trash/deleteItem.cy.ts @@ -3,19 +3,15 @@ import { PackedRecycledItemDataFactory } from '@graasp/sdk'; import { RECYCLE_BIN_PATH } from '../../../../src/config/paths'; import { CONFIRM_DELETE_BUTTON_ID, - ITEM_DELETE_BUTTON_CLASS, - buildItemsTableRowIdAttribute, + buildItemCard, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; const deleteItem = (id: string) => { - cy.get( - `${buildItemsTableRowIdAttribute(id)} .${ITEM_DELETE_BUTTON_CLASS}`, - ).click(); + cy.get(`#${buildItemCard(id)} [data-testid="DeleteIcon"]`).click(); cy.get(`#${CONFIRM_DELETE_BUTTON_ID}`).click(); }; -describe('Delete Item in List', () => { +describe('Delete Item', () => { it('delete item', () => { const recycledItemData = [ PackedRecycledItemDataFactory(), @@ -27,7 +23,6 @@ describe('Delete Item in List', () => { }); cy.visit(RECYCLE_BIN_PATH); - cy.switchMode(ItemLayoutMode.List); const { id } = recycledItemData[0].item; // delete diff --git a/cypress/e2e/item/delete/listRecycleItem.cy.ts b/cypress/e2e/item/trash/recycleItem.cy.ts similarity index 68% rename from cypress/e2e/item/delete/listRecycleItem.cy.ts rename to cypress/e2e/item/trash/recycleItem.cy.ts index 233202ba9..4c2086fb7 100644 --- a/cypress/e2e/item/delete/listRecycleItem.cy.ts +++ b/cypress/e2e/item/trash/recycleItem.cy.ts @@ -3,33 +3,25 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { ITEM_MENU_RECYCLE_BUTTON_CLASS, - buildItemMenu, - buildItemMenuButtonId, + buildItemsGridMoreButtonSelector, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; const recycleItem = (id: string) => { - // I need this wait because the table reloads and I lose the menu - // todo: remove on table refactor - cy.wait(500); - cy.get(`#${buildItemMenuButtonId(id)}`).click(); - cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_RECYCLE_BUTTON_CLASS}`).click(); + 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 in List', () => { +describe('Recycle Item', () => { it('recycle item on Home', () => { cy.setUpApi({ items }); cy.visit(HOME_PATH); - cy.switchMode(ItemLayoutMode.List); - const { id } = items[0]; - // delete recycleItem(id); cy.wait('@recycleItems').then(({ request: { url } }) => { expect(url).to.contain(id); @@ -45,8 +37,6 @@ describe('Recycle Item in List', () => { // go to children item cy.visit(buildItemPath(id)); - cy.switchMode(ItemLayoutMode.List); - // delete recycleItem(idToDelete); cy.wait('@recycleItems').then(({ request: { url } }) => { diff --git a/cypress/e2e/item/trash/restoreItem.cy.ts b/cypress/e2e/item/trash/restoreItem.cy.ts new file mode 100644 index 000000000..5f5b062de --- /dev/null +++ b/cypress/e2e/item/trash/restoreItem.cy.ts @@ -0,0 +1,34 @@ +import { PackedRecycledItemDataFactory } from '@graasp/sdk'; + +import { RECYCLE_BIN_PATH } from '../../../../src/config/paths'; +import { + RESTORE_ITEMS_BUTTON_CLASS, + buildItemCard, +} from '../../../../src/config/selectors'; + +const restoreItem = (id: string) => { + cy.get(`#${buildItemCard(id)} .${RESTORE_ITEMS_BUTTON_CLASS}`).click(); +}; + +describe('Restore Items', () => { + it('restore one item', () => { + const recycledItemData = [ + PackedRecycledItemDataFactory(), + PackedRecycledItemDataFactory(), + ]; + cy.setUpApi({ + items: recycledItemData.map(({ item }) => item), + recycledItemData, + }); + cy.visit(RECYCLE_BIN_PATH); + + const { id } = recycledItemData[0].item; + + // restore + restoreItem(id); + cy.wait('@restoreItems').then(({ request: { url } }) => { + expect(url).to.contain(id); + }); + cy.wait('@getRecycledItems'); + }); +}); diff --git a/cypress/e2e/item/trash/viewTrash.cy.ts b/cypress/e2e/item/trash/viewTrash.cy.ts new file mode 100644 index 000000000..3f2884f07 --- /dev/null +++ b/cypress/e2e/item/trash/viewTrash.cy.ts @@ -0,0 +1,112 @@ +import { PackedRecycledItemDataFactory } from '@graasp/sdk'; + +import { SortingOptions } from '@/components/table/types'; +import { BUILDER } from '@/langs/constants'; + +import i18n, { BUILDER_NAMESPACE } from '../../../../src/config/i18n'; +import { RECYCLE_BIN_PATH } from '../../../../src/config/paths'; +import { + CREATE_ITEM_BUTTON_ID, + ITEM_SEARCH_INPUT_ID, + RECYCLED_ITEMS_ERROR_ALERT_ID, + RECYCLED_ITEMS_ROOT_CONTAINER, + SORTING_ORDERING_SELECTOR_ASC, + SORTING_ORDERING_SELECTOR_DESC, + SORTING_SELECT_SELECTOR, + buildItemCard, +} from '../../../../src/config/selectors'; +import { CURRENT_USER } from '../../../fixtures/members'; + +const recycledItemData = [ + PackedRecycledItemDataFactory(), + PackedRecycledItemDataFactory(), + PackedRecycledItemDataFactory(), +]; + +describe('View trash', () => { + describe('Member has no recycled items', () => { + it('Show empty table', () => { + cy.setUpApi({ + items: recycledItemData.map(({ item }) => item), + }); + cy.visit(RECYCLE_BIN_PATH); + i18n.changeLanguage(CURRENT_USER.extra.lang as string); + const text = i18n.t(BUILDER.TRASH_NO_ITEM, { ns: BUILDER_NAMESPACE }); + cy.get(`#${RECYCLED_ITEMS_ROOT_CONTAINER}`).should('contain', text); + }); + }); + + describe('Member has recycled items', () => { + beforeEach(() => { + cy.setUpApi({ + items: recycledItemData.map(({ item }) => item), + recycledItemData, + }); + i18n.changeLanguage(CURRENT_USER.extra.lang as string); + cy.visit(RECYCLE_BIN_PATH); + }); + + it('Empty search', () => { + i18n.changeLanguage(CURRENT_USER.extra.lang as string); + const searchText = 'mysearch'; + cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(searchText); + const text = i18n.t(BUILDER.TRASH_NO_ITEM_SEARCH, { + search: searchText, + ns: BUILDER_NAMESPACE, + }); + cy.get(`#${RECYCLED_ITEMS_ROOT_CONTAINER}`).should('contain', text); + }); + + it('New button should not exist', () => { + cy.get(`#${CREATE_ITEM_BUTTON_ID}`).should('not.exist'); + }); + + it('check recycled item layout', () => { + for (const { item } of recycledItemData) { + cy.get(`#${buildItemCard(item.id)}`).should('be.visible'); + } + }); + + it('Sorting & Ordering', () => { + cy.get(`${SORTING_SELECT_SELECTOR} input`).should( + 'have.value', + SortingOptions.ItemUpdatedAt, + ); + cy.get(SORTING_ORDERING_SELECTOR_DESC).should('be.visible'); + + cy.get(SORTING_SELECT_SELECTOR).click(); + cy.get('li[data-value="item.name"]').click(); + + // check items are ordered by name + cy.get(`#${RECYCLED_ITEMS_ROOT_CONTAINER} h5`).then(($e) => { + recycledItemData.sort((a, b) => (a.item.name < b.item.name ? 1 : -1)); + for (let idx = 0; idx < recycledItemData.length; idx += 1) { + expect($e[idx].innerText).to.eq(recycledItemData[idx].item.name); + } + }); + + // change ordering + cy.get(SORTING_ORDERING_SELECTOR_DESC).click(); + cy.get(SORTING_ORDERING_SELECTOR_ASC).should('be.visible'); + cy.get(`#${RECYCLED_ITEMS_ROOT_CONTAINER} h5`).then(($e) => { + recycledItemData.reverse(); + for (let idx = 0; idx < recycledItemData.length; idx += 1) { + expect($e[idx].innerText).to.eq(recycledItemData[idx].item.name); + } + }); + }); + }); + + describe('Error Handling', () => { + it('check recycled item layout with server error', () => { + cy.setUpApi({ + items: recycledItemData.map(({ item }) => item), + recycledItemData, + getRecycledItemsError: true, + }); + cy.visit(RECYCLE_BIN_PATH); + + cy.get(`#${RECYCLED_ITEMS_ERROR_ALERT_ID}`).should('exist'); + }); + }); +}); diff --git a/cypress/e2e/item/upload/dropzoneUpload.cy.ts b/cypress/e2e/item/upload/dropzoneUpload.cy.ts index bc8e98df6..355640da5 100644 --- a/cypress/e2e/item/upload/dropzoneUpload.cy.ts +++ b/cypress/e2e/item/upload/dropzoneUpload.cy.ts @@ -1,7 +1,7 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; import { buildItemPath } from '@/config/paths'; -import { DROPZONE_HELPER_ID } from '@/config/selectors'; +import { DROPZONE_SELECTOR } from '@/config/selectors'; import { SAMPLE_PUBLIC_ITEMS } from '../../../fixtures/items'; @@ -13,7 +13,7 @@ describe('Dropzone Helper Visibility', () => { it('should display the dropzone on the home screen when no items', () => { cy.visit('/'); - cy.get(`#${DROPZONE_HELPER_ID}`).should('be.visible'); + cy.get(DROPZONE_SELECTOR).should('be.visible'); }); }); @@ -22,13 +22,13 @@ describe('Dropzone Helper Visibility', () => { const ITEMS = [PackedFolderItemFactory()]; cy.setUpApi({ items: ITEMS }); cy.visit(buildItemPath(ITEMS[0].id)); - cy.get(`#${DROPZONE_HELPER_ID}`).should('be.visible'); + cy.get(DROPZONE_SELECTOR).should('be.visible'); }); it('should hide dropzone helper when no items (logged out)', () => { cy.setUpApi({ ...SAMPLE_PUBLIC_ITEMS, currentMember: null }); cy.visit(buildItemPath(SAMPLE_PUBLIC_ITEMS.items[2].id)); - cy.get(`#${DROPZONE_HELPER_ID}`).should('not.exist'); + cy.get(DROPZONE_SELECTOR).should('not.exist'); }); }); }); diff --git a/cypress/e2e/item/view/viewFile.cy.ts b/cypress/e2e/item/view/viewFile.cy.ts index bdc5f6fc6..1b47f3060 100644 --- a/cypress/e2e/item/view/viewFile.cy.ts +++ b/cypress/e2e/item/view/viewFile.cy.ts @@ -1,6 +1,5 @@ import { HOME_PATH } from '../../../../src/config/paths'; -import { buildItemsTableRowIdAttribute } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; +import { buildItemCard } from '../../../../src/config/selectors'; import { IMAGE_ITEM_DEFAULT, IMAGE_ITEM_S3, @@ -18,39 +17,31 @@ describe('View Files', () => { items: [IMAGE_ITEM_DEFAULT, VIDEO_ITEM_DEFAULT, PDF_ITEM_DEFAULT], }); cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); }); it('image', () => { // item is displayed in table - cy.get(buildItemsTableRowIdAttribute(IMAGE_ITEM_DEFAULT.id)).should( - 'exist', - ); + cy.get(`#${buildItemCard(IMAGE_ITEM_DEFAULT.id)}`).should('exist'); // item metadata - cy.goToItemInList(IMAGE_ITEM_DEFAULT.id); + cy.goToItemInCard(IMAGE_ITEM_DEFAULT.id); expectFileViewScreenLayout({ item: IMAGE_ITEM_DEFAULT }); }); it('video', () => { // item is displayed in table - cy.get(buildItemsTableRowIdAttribute(VIDEO_ITEM_DEFAULT.id)).should( - 'exist', - ); + cy.get(`#${buildItemCard(VIDEO_ITEM_DEFAULT.id)}`).should('exist'); // item metadata - cy.goToItemInList(VIDEO_ITEM_DEFAULT.id); + cy.goToItemInCard(VIDEO_ITEM_DEFAULT.id); expectFileViewScreenLayout({ item: VIDEO_ITEM_DEFAULT }); }); it('pdf', () => { // item is displayed in table - cy.get(buildItemsTableRowIdAttribute(PDF_ITEM_DEFAULT.id)).should( - 'exist', - ); + cy.get(`#${buildItemCard(PDF_ITEM_DEFAULT.id)}`).should('exist'); // item metadata - cy.goToItemInList(PDF_ITEM_DEFAULT.id); + cy.goToItemInCard(PDF_ITEM_DEFAULT.id); expectFileViewScreenLayout({ item: PDF_ITEM_DEFAULT }); }); }); @@ -61,33 +52,31 @@ describe('View Files', () => { items: [IMAGE_ITEM_S3, VIDEO_ITEM_S3, PDF_ITEM_S3], }); cy.visit(HOME_PATH); - - cy.switchMode(ItemLayoutMode.List); }); it('image', () => { // item is displayed in table - cy.get(buildItemsTableRowIdAttribute(IMAGE_ITEM_S3.id)).should('exist'); + cy.get(`#${buildItemCard(IMAGE_ITEM_S3.id)}`).should('exist'); // item metadata - cy.goToItemInList(IMAGE_ITEM_S3.id); + cy.goToItemInCard(IMAGE_ITEM_S3.id); expectFileViewScreenLayout({ item: IMAGE_ITEM_S3 }); }); it('video', () => { // item is displayed in table - cy.get(buildItemsTableRowIdAttribute(VIDEO_ITEM_S3.id)).should('exist'); + cy.get(`#${buildItemCard(VIDEO_ITEM_S3.id)}`).should('exist'); // item metadata - cy.goToItemInList(VIDEO_ITEM_S3.id); + cy.goToItemInCard(VIDEO_ITEM_S3.id); expectFileViewScreenLayout({ item: VIDEO_ITEM_S3 }); }); it('pdf', () => { // item is displayed in table - cy.get(buildItemsTableRowIdAttribute(PDF_ITEM_S3.id)).should('exist'); + cy.get(`#${buildItemCard(PDF_ITEM_S3.id)}`).should('exist'); // item metadata - cy.goToItemInList(PDF_ITEM_S3.id); + cy.goToItemInCard(PDF_ITEM_S3.id); expectFileViewScreenLayout({ item: PDF_ITEM_S3 }); }); }); diff --git a/cypress/e2e/item/view/viewFolder.cy.ts b/cypress/e2e/item/view/viewFolder.cy.ts index 41b5ca21c..a09d33b6b 100644 --- a/cypress/e2e/item/view/viewFolder.cy.ts +++ b/cypress/e2e/item/view/viewFolder.cy.ts @@ -1,12 +1,17 @@ import { PackedFolderItemFactory } from '@graasp/sdk'; +import { SortingOptionsForFolder } from '../../../../src/components/table/types'; import i18n from '../../../../src/config/i18n'; import { buildItemPath } from '../../../../src/config/paths'; import { + CREATE_ITEM_BUTTON_ID, ITEM_SEARCH_INPUT_ID, NAVIGATION_HOME_ID, + SORTING_ORDERING_SELECTOR_ASC, + SORTING_ORDERING_SELECTOR_DESC, + SORTING_SELECT_SELECTOR, buildItemCard, - buildItemsTableRowIdAttribute, + buildItemsTableId, buildMapViewId, } from '../../../../src/config/selectors'; import { ItemLayoutMode } from '../../../../src/enums'; @@ -18,9 +23,11 @@ const item1 = PackedFolderItemFactory(); const child1 = PackedFolderItemFactory({ parentItem }); const child2 = PackedFolderItemFactory({ parentItem }); -const children = [child1, child2]; +const child3 = PackedFolderItemFactory({ parentItem }); +const child4 = PackedFolderItemFactory({ parentItem }); +const children = [child1, child2, child3, child4]; -const items = [parentItem, item1, child1, child2]; +const items = [parentItem, item1, ...children]; describe('View Folder', () => { it('View folder on map by default', () => { @@ -31,98 +38,139 @@ describe('View Folder', () => { const { id } = parentItem; cy.visit(buildItemPath(id, { mode: ItemLayoutMode.Map })); - // wait on getting geoloc cy.get(`#${buildMapViewId(id)}`, { timeout: 10000 }).should('be.visible'); }); + it('View empty folder', () => { + cy.setUpApi({ + items: [parentItem], + }); - describe('Grid', () => { - beforeEach(() => { - cy.setUpApi({ - items, - }); - i18n.changeLanguage(CURRENT_USER.extra.lang as string); + const { id } = parentItem; + cy.visit(buildItemPath(id)); + + cy.get(`[role="dropzone"]`).should('be.visible'); + cy.get(`#${CREATE_ITEM_BUTTON_ID}`).should('be.visible'); + }); + + beforeEach(() => { + cy.setUpApi({ + items, }); + i18n.changeLanguage(CURRENT_USER.extra.lang as string); + }); - it('visit item by id', () => { - const { id } = parentItem; - cy.visit(buildItemPath(id, { mode: ItemLayoutMode.Grid })); - - // should get current item - cy.wait('@getItem'); - - // should get children - cy.wait('@getChildren').then(() => { - // check all children are created and displayed - for (const item of children) { - cy.get(`#${buildItemCard(item.id)}`).should('exist'); - } - }); - expectFolderViewScreenLayout({ item: parentItem }); - - // visit home - cy.get(`#${NAVIGATION_HOME_ID}`).click(); - - // should get accessible items - cy.wait('@getAccessibleItems').then(({ response: { body } }) => { - // check item is created and displayed - for (const item of body.data) { - cy.get(`#${buildItemCard(item.id)}`).should('exist'); - } - }); + it('visit item by id', () => { + const { id } = parentItem; + cy.visit(buildItemPath(id, { mode: ItemLayoutMode.Grid })); + + // should get current item + cy.wait('@getItem'); + + // should get children + cy.wait('@getChildren').then(() => { + // check all children are created and displayed + for (const item of children) { + cy.get(`#${buildItemCard(item.id)}`).should('exist'); + } }); + expectFolderViewScreenLayout({ item: parentItem }); - it('search', () => { - const { id } = parentItem; - cy.visit(buildItemPath(id, { mode: ItemLayoutMode.Grid })); + // visit home + cy.get(`#${NAVIGATION_HOME_ID}`).click(); - cy.get(`#${buildItemCard(child1.id)}`).should('be.visible'); - cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(child1.name); - cy.get(`#${buildItemCard(child1.id)}`).should('be.visible'); + // should get accessible items + cy.wait('@getAccessibleItems').then(({ response: { body } }) => { + // check item is created and displayed + for (const item of body.data) { + cy.get(`#${buildItemCard(item.id)}`).should('exist'); + } }); }); - describe('List', () => { - beforeEach(() => { - cy.setUpApi({ - items, - }); - }); + it('search', () => { + const { id } = parentItem; + cy.visit(buildItemPath(id, { mode: ItemLayoutMode.Grid })); + + cy.get(`#${buildItemCard(child1.id)}`).should('be.visible'); + + cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(child1.name); + cy.get(`#${buildItemCard(child1.id)}`).should('be.visible'); + }); - describe('Navigation', () => { - it('visit folder by id', () => { - const { id } = parentItem; - cy.visit(buildItemPath(id, { mode: ItemLayoutMode.List })); - - // should get current item - cy.wait('@getItem'); - // should get children - cy.wait('@getChildren').then(({ response: { body } }) => { - // check all children are created and displayed - for (const item of body) { - cy.get(buildItemsTableRowIdAttribute(item.id)).should('exist'); - } - }); - - expectFolderViewScreenLayout({ item: parentItem }); - // visit home - cy.get(`#${NAVIGATION_HOME_ID}`).click(); - - cy.wait('@getAccessibleItems').then(({ response: { body } }) => { - // check item is created and displayed - for (const item of body.data) { - cy.get(buildItemsTableRowIdAttribute(item.id)).should('exist'); - } - }); - }); + it('Sorting & Ordering', () => { + const { id } = parentItem; + cy.visit(buildItemPath(id)); + + cy.get(`${SORTING_SELECT_SELECTOR} input`).should( + 'have.value', + SortingOptionsForFolder.Order, + ); + cy.get(SORTING_ORDERING_SELECTOR_ASC).should('be.visible'); + + cy.get(SORTING_SELECT_SELECTOR).click(); + cy.get('li[data-value="item.name"]').click(); + + // check items are ordered by name + cy.get(`#${buildItemsTableId(parentItem.id)} h5`).then(($e) => { + children.sort((a, b) => (a.name > b.name ? 1 : -1)); + for (let idx = 0; idx < children.length; idx += 1) { + expect($e[idx].innerText).to.eq(children[idx].name); + } }); - it('search', () => { - const { id } = parentItem; - cy.visit(buildItemPath(id, { mode: ItemLayoutMode.List })); + // change ordering + cy.get(SORTING_ORDERING_SELECTOR_ASC).click(); + cy.get(SORTING_ORDERING_SELECTOR_DESC).should('be.visible'); + cy.get(`#${buildItemsTableId(parentItem.id)} h5`).then(($e) => { + children.reverse(); + for (let idx = 0; idx < children.length; idx += 1) { + expect($e[idx].innerText).to.eq(children[idx].name); + } + }); + }); +}); - cy.get(buildItemsTableRowIdAttribute(child1.id)).should('be.visible'); - cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(child1.name); - cy.get(buildItemsTableRowIdAttribute(child1.id)).should('be.visible'); +describe('Folder Layout mode', () => { + beforeEach(() => { + cy.setUpApi({ + items: [parentItem, child1], }); + cy.visit(buildItemPath(parentItem.id)); + }); + + it('list', () => { + // default mode is list + cy.get(`#${buildItemCard(child1.id)}`); + + // go to map + cy.switchMode(ItemLayoutMode.Map); + + // go to list + cy.switchMode(ItemLayoutMode.List); + cy.get(`#${buildItemCard(child1.id)}`); }); + + it('grid', () => { + cy.switchMode(ItemLayoutMode.Grid); + cy.get(`#${buildItemCard(child1.id)}`); + }); + + it('map', () => { + cy.switchMode(ItemLayoutMode.Map); + cy.get(`#${buildMapViewId(parentItem.id)}`, { timeout: 10000 }).should( + 'be.visible', + ); + }); +}); + +it('visit Home on map by default', () => { + cy.setUpApi({ + items: [parentItem, child1], + }); + // access map directly + cy.visit(buildItemPath(parentItem.id, { mode: ItemLayoutMode.Map })); + + cy.get(`#${buildMapViewId(parentItem.id)}`, { timeout: 10000 }).should( + 'be.visible', + ); }); diff --git a/cypress/e2e/item/view/viewThumbnails.cy.ts b/cypress/e2e/item/view/viewThumbnails.cy.ts index ee93dda92..f247cb564 100644 --- a/cypress/e2e/item/view/viewThumbnails.cy.ts +++ b/cypress/e2e/item/view/viewThumbnails.cy.ts @@ -4,9 +4,7 @@ import { HOME_PATH } from '../../../../src/config/paths'; import { HEADER_MEMBER_MENU_BUTTON_ID, buildItemCard, - buildNameCellRendererId, } from '../../../../src/config/selectors'; -import { ItemLayoutMode } from '../../../../src/enums'; import { MEMBERS } from '../../../fixtures/members'; import { ITEM_THUMBNAIL_LINK } from '../../../fixtures/thumbnails/links'; @@ -29,19 +27,6 @@ describe('View Thumbnails', () => { cy.visit(HOME_PATH); - // check default material icon - // first item doesn't have a thumbnail so it displays the material icon - cy.get( - `#${buildNameCellRendererId(ITEM_WITHOUT_THUMBNAIL.id)} svg path`, - ).should('exist'); - - // the second item has a defined thumbnail - cy.get(`#${buildNameCellRendererId(ITEM_WITH_THUMBNAIL.id)} img`).should( - 'exist', - ); - - // GRID - cy.switchMode(ItemLayoutMode.Grid); // first element has default folder svg cy.get(`#${buildItemCard(ITEM_WITH_THUMBNAIL.id)} svg path`).should( 'exist', diff --git a/cypress/fixtures/chatbox.ts b/cypress/fixtures/chatbox.ts index c42e3564c..13cccbe86 100644 --- a/cypress/fixtures/chatbox.ts +++ b/cypress/fixtures/chatbox.ts @@ -15,7 +15,7 @@ import { CURRENT_USER, MEMBERS } from './members'; const item: FolderItemType = { ...DEFAULT_FOLDER_ITEM, type: ItemType.FOLDER, - extra: { [ItemType.FOLDER]: { childrenOrder: [] } }, + extra: { [ItemType.FOLDER]: {} }, id: 'adf09f5a-5688-11eb-ae93-0242ac130004', path: 'adf09f5a_5688_11eb_ae93_0242ac130004', name: 'item with chatbox messages', diff --git a/cypress/fixtures/items.ts b/cypress/fixtures/items.ts index 1d30da986..a0165474e 100644 --- a/cypress/fixtures/items.ts +++ b/cypress/fixtures/items.ts @@ -16,7 +16,7 @@ import { CURRENT_USER, MEMBERS } from './members'; export const DEFAULT_FOLDER_ITEM = PackedFolderItemFactory({ name: 'default folder', - extra: { [ItemType.FOLDER]: { childrenOrder: [] } }, + extra: { [ItemType.FOLDER]: {} }, creator: CURRENT_USER, }); @@ -254,7 +254,7 @@ export const PublishedItemFactory = ( ...itemToPublish, published: { id: 'ecbfbd2a-5688-12eb-ae93-0242ac130002', - item, + item: itemToPublish, createdAt: new Date().toISOString(), creator: itemToPublish.creator, totalViews: 0, diff --git a/cypress/support/actionsUtils.ts b/cypress/support/actionsUtils.ts index d9df4768d..f0957b980 100644 --- a/cypress/support/actionsUtils.ts +++ b/cypress/support/actionsUtils.ts @@ -1,15 +1,11 @@ import { ITEM_MENU_DUPLICATE_BUTTON_CLASS, - buildItemMenu, - buildItemMenuButtonId, + buildItemsGridMoreButtonSelector, } from '@/config/selectors'; const duplicateItem = ({ id }: { id: string }): void => { - // sorry I need this timeout otherwise the table reload and lose the click.. - // todo: to remove on table refactor - cy.wait(500); - cy.get(`#${buildItemMenuButtonId(id)}`).click(); - cy.get(`#${buildItemMenu(id)} .${ITEM_MENU_DUPLICATE_BUTTON_CLASS}`).click(); + cy.get(buildItemsGridMoreButtonSelector(id)).click(); + cy.get(`.${ITEM_MENU_DUPLICATE_BUTTON_CLASS}`).click(); }; export default duplicateItem; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 8d01085e3..82bc05e39 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -2,6 +2,8 @@ import { CookieKeys } from '@graasp/sdk'; import 'cypress-localstorage-commands'; +import { ItemLayoutMode } from '@/enums'; + import { LAYOUT_MODE_BUTTON_ID } from '../../src/config/selectors'; import { APPS_LIST } from '../fixtures/apps/apps'; import { SAMPLE_CATEGORIES } from '../fixtures/categories'; @@ -63,6 +65,7 @@ import { mockGetOwnItems, mockGetParents, mockGetPublishItemInformations, + mockGetPublishItemsForMember, mockGetRecycledItems, mockGetSharedItems, mockGetShortLinksItem, @@ -104,6 +107,7 @@ Cypress.Commands.add( items = [], recycledItemData = [], bookmarkedItems = [], + publishedItemData = [], members = Object.values(MEMBERS), currentMember = CURRENT_USER, mentions = SAMPLE_MENTIONS, @@ -164,6 +168,8 @@ Cypress.Commands.add( patchShortLinkError = false, deleteShortLinkError = false, importH5pError = false, + getRecycledItemsError = false, + getPublishedItems = false, } = {}) => { const cachedItems = JSON.parse(JSON.stringify(items)); const cachedMembers = JSON.parse(JSON.stringify(members)); @@ -275,7 +281,7 @@ Cypress.Commands.add( mockRecycleItems(items, recycleItemsError); - mockGetRecycledItems(recycledItemData); + mockGetRecycledItems(recycledItemData, getRecycledItemsError); mockRestoreItems(recycledItemData, restoreItemsError); @@ -348,29 +354,25 @@ Cypress.Commands.add( mockGetLinkMetadata(); mockImportH5p(importH5pError); + + mockGetPublishItemsForMember(publishedItemData, getPublishedItems); }, ); -const ItemLayoutMode = { - Grid: 'grid', - List: 'list', -}; -const DEFAULT_ITEM_LAYOUT_MODE = ItemLayoutMode.List; - Cypress.Commands.add('switchMode', (mode) => { - if (DEFAULT_ITEM_LAYOUT_MODE !== mode) { - cy.get(`#${LAYOUT_MODE_BUTTON_ID}`).click({ force: true }); - switch (mode) { - case ItemLayoutMode.Grid: - cy.get(`li[value="${ItemLayoutMode.Grid}"]`).click({ force: true }); - break; - case ItemLayoutMode.List: - cy.get(`li[value="${ItemLayoutMode.List}"]`).click({ force: true }); - break; - default: - console.error(`invalid mode ${mode} provided`); - break; - } + cy.get(`#${LAYOUT_MODE_BUTTON_ID}`).click({ force: true }); + switch (mode) { + case ItemLayoutMode.Grid: + cy.get(`li[value="${ItemLayoutMode.Grid}"]`).click({ force: true }); + break; + case ItemLayoutMode.List: + cy.get(`li[value="${ItemLayoutMode.List}"]`).click({ force: true }); + break; + case ItemLayoutMode.Map: + cy.get(`li[value="${ItemLayoutMode.Map}"]`).click({ force: true }); + break; + default: + throw new Error(`invalid mode ${mode} provided`); } }); diff --git a/cypress/support/commands/item.ts b/cypress/support/commands/item.ts index 51472ad06..c31a9cce1 100644 --- a/cypress/support/commands/item.ts +++ b/cypress/support/commands/item.ts @@ -27,7 +27,6 @@ import { CUSTOM_APP_URL, NEW_APP_NAME, } from '../../fixtures/apps/apps'; -import { TREE_VIEW_PAUSE } from '../constants'; Cypress.Commands.add( 'fillShareForm', @@ -62,8 +61,6 @@ Cypress.Commands.add( (toItemPath, treeRootId = HOME_MODAL_ITEM_ID) => { const ids = getParentsIdsFromPath(toItemPath); - cy.wait(TREE_VIEW_PAUSE); - [MY_GRAASP_ITEM_PATH, ...ids].forEach((value, idx, array) => { cy.get(`#${treeRootId}`).then(($tree) => { // click on the element diff --git a/cypress/support/commands/navigation.ts b/cypress/support/commands/navigation.ts index 81c50f9a7..1208c8320 100644 --- a/cypress/support/commands/navigation.ts +++ b/cypress/support/commands/navigation.ts @@ -1,16 +1,14 @@ import { buildItemPath } from '../../../src/config/paths'; import { NAVIGATION_HOME_LINK_ID, - buildItemLink, - buildItemsTableRowIdAttribute, + buildItemCard, } from '../../../src/config/selectors'; -Cypress.Commands.add('goToItemInGrid', (id) => { - cy.get(`#${buildItemLink(id)}`).click(); -}); - -Cypress.Commands.add('goToItemInList', (id) => { - cy.get(buildItemsTableRowIdAttribute(id)).click(); +Cypress.Commands.add('goToItemInCard', (id) => { + // card component might have many click zone + cy.get(`#${buildItemCard(id)} a[href="${buildItemPath(id)}"]`) + .first() + .click(); }); Cypress.Commands.add('goToHome', () => { diff --git a/cypress/support/constants.ts b/cypress/support/constants.ts index 82fdf9402..5e424d29e 100644 --- a/cypress/support/constants.ts +++ b/cypress/support/constants.ts @@ -7,9 +7,7 @@ export const PAGE_LOAD_WAITING_PAUSE = 3000; export const REQUEST_FAILURE_LOADING_TIME = 1500; export const FILE_LOADING_PAUSE = 2000; export const TREE_VIEW_PAUSE = 1500; -export const ITEM_LOADING_PAUSE = 2000; export const WEBSOCKETS_DELAY_TIME = 1500; -export const WAIT_FOR_ITEM_TABLE_ROW_TIME = 7000; export const EDIT_TAG_REQUEST_TIMEOUT = 10000; @@ -19,7 +17,6 @@ export const REQUEST_FAILURE_TIME = 2500; export const REDIRECTION_TIME = 500; export const CAPTION_EDIT_PAUSE = 2000; -export const ROW_HEIGHT = 48; export const TABLE_MEMBERSHIP_RENDER_TIME = 1000; export const FIXTURES_THUMBNAILS_FOLDER = './thumbnails'; export const CHATBOX_LOADING_TIME = 5000; diff --git a/cypress/support/editUtils.ts b/cypress/support/editUtils.ts index 9d92c7400..bfc8f542c 100644 --- a/cypress/support/editUtils.ts +++ b/cypress/support/editUtils.ts @@ -1,17 +1,14 @@ import { DiscriminatedItem, ItemType } from '@graasp/sdk'; -import { DEFAULT_ITEM_LAYOUT_MODE } from '@/enums/itemLayoutMode'; - import { + EDIT_ITEM_BUTTON_CLASS, EDIT_MODAL_ID, ITEM_FORM_CONFIRM_BUTTON_ID, TEXT_EDITOR_CLASS, buildEditButtonId, } from '../../src/config/selectors'; -import { ItemLayoutMode } from '../../src/enums'; import { CAPTION_EDIT_PAUSE } from './constants'; -// bug: use string for type to fit usage export const editItem = ( payload: { id: string; @@ -20,22 +17,10 @@ export const editItem = ( displayName: string; description: string; }, - mode = DEFAULT_ITEM_LAYOUT_MODE, + container: string = '', ): void => { - // todo: remove on table refactor - cy.wait(500); - const { id, type } = payload; - switch (mode) { - case ItemLayoutMode.Grid: { - const button = `#${buildEditButtonId(id)}`; - cy.get(button).click(); - break; - } - case ItemLayoutMode.List: - default: { - cy.get(`#${buildEditButtonId(id)}`).click(); - } - } + const { type } = payload; + cy.get(`${container} .${EDIT_ITEM_BUTTON_CLASS}`).click(); switch (type) { case ItemType.H5P: diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 5bd6f411d..7eb6bda45 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -45,8 +45,7 @@ declare global { clickTreeMenuItem(value: string): void; handleTreeMenu(path: string, rootId?: string): void; switchMode(mode: string): void; - goToItemInGrid(path: string): void; - goToItemInList(path: string): void; + goToItemInCard(path: string): void; goToItemWithNavigation(id: string): void; goToHome(): void; diff --git a/cypress/support/server.ts b/cypress/support/server.ts index ba41f0120..4f8cfae32 100644 --- a/cypress/support/server.ts +++ b/cypress/support/server.ts @@ -56,12 +56,12 @@ const { buildGetItemRoute, GET_OWN_ITEMS_ROUTE, buildPostItemMembershipRoute, - buildGetMember, + buildGetMemberRoute, buildPostManyItemMembershipsRoute, ITEMS_ROUTE, buildUploadFilesRoute, buildDownloadFilesRoute, - GET_CURRENT_MEMBER_ROUTE, + buildGetCurrentMemberRoute, GET_BOOKMARKED_ITEMS_ROUTE, SIGN_OUT_ROUTE, buildPostItemLoginSignInRoute, @@ -69,7 +69,7 @@ const { buildGetItemMembershipsForItemsRoute, buildGetItemTagsRoute, buildPostItemTagRoute, - buildPatchMember, + buildPatchMemberRoute, SHARED_ITEM_WITH_ROUTE, buildEditItemMembershipRoute, buildDeleteItemMembershipRoute, @@ -81,7 +81,7 @@ const { GET_RECYCLED_ITEMS_DATA_ROUTE, buildDeleteItemTagRoute, buildDeleteItemsRoute, - buildGetMembersRoute, + buildGetMembersByIdRoute, buildUploadItemThumbnailRoute, buildUploadAvatarRoute, buildImportZipRoute, @@ -94,6 +94,7 @@ const { buildPatchInvitationRoute, buildResendInvitationRoute, buildPostUserCSVUploadRoute, + buildGetPublishedItemsForMemberRoute, buildItemPublishRoute, buildUpdateMemberPasswordRoute, buildPostItemValidationRoute, @@ -146,7 +147,7 @@ export const mockGetCurrentMember = ( cy.intercept( { method: HttpMethod.Get, - url: `${API_HOST}/${GET_CURRENT_MEMBER_ROUTE}`, + url: `${API_HOST}/${buildGetCurrentMemberRoute()}`, }, ({ reply }) => { if (shouldThrowError) { @@ -203,6 +204,7 @@ export const mockGetAccessibleItems = (items: ItemForTest[]): void => { export const mockGetRecycledItems = ( recycledItemData: RecycledItemData[], + shouldThrowError: boolean, ): void => { cy.intercept( { @@ -210,6 +212,11 @@ export const mockGetRecycledItems = ( url: `${API_HOST}/${GET_RECYCLED_ITEMS_DATA_ROUTE}`, }, (req) => { + if (shouldThrowError) { + req.reply({ statusCode: StatusCodes.BAD_REQUEST }); + return; + } + req.reply(recycledItemData); }, ).as('getRecycledItems'); @@ -670,7 +677,7 @@ export const mockGetMember = (members: Member[]): void => { cy.intercept( { method: HttpMethod.Get, - url: new RegExp(`${API_HOST}/${buildGetMember(ID_FORMAT)}$`), + url: new RegExp(`${API_HOST}/${buildGetMemberRoute(ID_FORMAT)}$`), }, ({ url, reply }) => { const memberId = url.slice(API_HOST.length).split('/')[2]; @@ -695,7 +702,7 @@ export const mockGetMembers = (members: Member[]): void => { cy.intercept( { method: HttpMethod.Get, - url: `${API_HOST}/${buildGetMembersRoute([''])}*`, + url: `${API_HOST}/${buildGetMembersByIdRoute([''])}*`, }, ({ url, reply }) => { const memberIds = new URL(url).searchParams.getAll('id'); @@ -772,7 +779,7 @@ export const mockEditMember = ( cy.intercept( { method: HttpMethod.Patch, - url: new RegExp(`${API_HOST}/${buildPatchMember(ID_FORMAT)}`), + url: new RegExp(`${API_HOST}/${buildPatchMemberRoute(ID_FORMAT)}`), }, ({ reply }) => { if (shouldThrowError) { @@ -1988,6 +1995,31 @@ export const mockGetManyPublishItemInformations = ( ).as('getManyPublishItemInformations'); }; +export const mockGetPublishItemsForMember = ( + publishedItemData: ItemPublished[], + shoulThrow = false, +): void => { + cy.intercept( + { + method: HttpMethod.Get, + url: new RegExp( + `${API_HOST}/${buildGetPublishedItemsForMemberRoute(ID_FORMAT)}`, + ), + }, + ({ reply, url }) => { + if (shoulThrow) { + return reply({ statusCode: StatusCodes.INTERNAL_SERVER_ERROR }); + } + + const memberId = url.slice(API_HOST.length).split('/')[4]; + const published = publishedItemData + .filter((p) => p.item.creator.id === memberId) + .map((i) => i.item); + return reply(published); + }, + ).as('getPublishedItemsForMember'); +}; + export const mockGetLatestValidationGroup = ( _items: ItemForTest[], itemValidationGroups: ItemValidationGroup[], diff --git a/package.json b/package.json index f75091fd9..4011f9ed8 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,10 @@ "@emotion/styled": "11.11.5", "@graasp/chatbox": "3.1.0", "@graasp/map": "1.16.0", - "@graasp/query-client": "3.14.0", - "@graasp/sdk": "4.15.1", + "@graasp/query-client": "3.15.2", + "@graasp/sdk": "4.17.0", "@graasp/translations": "1.31.0", - "@graasp/ui": "4.20.2", + "@graasp/ui": "4.21.0", "@mui/icons-material": "5.16.0", "@mui/lab": "5.0.0-alpha.170", "@mui/material": "5.16.0", @@ -119,8 +119,8 @@ "@types/react-dom": "18.3.0", "@types/uuid": "10.0.0", "@types/validator": "13.11.10", - "@typescript-eslint/eslint-plugin": "7.15.0", - "@typescript-eslint/parser": "7.15.0", + "@typescript-eslint/eslint-plugin": "7.16.0", + "@typescript-eslint/parser": "7.16.0", "@vitejs/plugin-react": "4.3.1", "concurrently": "8.2.2", "cypress": "13.11.0", diff --git a/src/components/App.tsx b/src/components/App.tsx index b2542aa5c..3bf138549 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -23,10 +23,10 @@ import { useCurrentUserContext } from './context/CurrentUserContext'; import Main from './main/Main'; import Redirect from './main/Redirect'; import BookmarkedItemsScreen from './pages/BookmarkedItemsScreen'; -import HomeScreen from './pages/HomeScreen'; import MapItemsScreen from './pages/MapItemsScreen'; import PublishedItemsScreen from './pages/PublishedItemsScreen'; import RecycledItemsScreen from './pages/RecycledItemsScreen'; +import HomeScreen from './pages/home/HomeScreen'; import ItemPageLayout from './pages/item/ItemPageLayout'; import ItemScreen from './pages/item/ItemScreen'; import ItemScreenLayout from './pages/item/ItemScreenLayout'; diff --git a/src/components/Root.tsx b/src/components/Root.tsx index 89611e447..d21a76c5c 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -35,7 +35,7 @@ const Root = (): JSX.Element => ( - + }> @@ -50,7 +50,7 @@ const Root = (): JSX.Element => ( {import.meta.env.DEV && import.meta.env.MODE !== 'test' && ( - + )} diff --git a/src/components/common/BookmarkButton.tsx b/src/components/common/BookmarkButton.tsx index 93013c2ec..18b7887d0 100644 --- a/src/components/common/BookmarkButton.tsx +++ b/src/components/common/BookmarkButton.tsx @@ -8,7 +8,6 @@ import { import { useBuilderTranslation } from '../../config/i18n'; import { hooks, mutations } from '../../config/queryClient'; -import { BOOKMARKED_ITEM_BUTTON_CLASS } from '../../config/selectors'; import { BUILDER } from '../../langs/constants'; import { useCurrentUserContext } from '../context/CurrentUserContext'; @@ -59,7 +58,6 @@ const BookmarkButton = ({ return ( { + const { mutate: copyItems } = mutations.useCopyItems(); + const { t: translateBuilder } = useBuilderTranslation(); + + const handleDuplicate = () => { + const parentsIds = getParentsIdsFromPath(item.path, { ignoreSelf: true }); + // get the close parent if not then undefined + const to = parentsIds.length + ? parentsIds[parentsIds.length - 1] + : undefined; + + copyItems({ + ids: [item.id], + to, + }); + }; + + return ( + + + + + {translateBuilder(BUILDER.ITEM_MENU_DUPLICATE_MENU_ITEM)} + + ); +}; + +export default DuplicateButton; diff --git a/src/components/common/EditButton.tsx b/src/components/common/EditButton.tsx deleted file mode 100644 index 27bddfe7e..000000000 --- a/src/components/common/EditButton.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { useState } from 'react'; - -import { Dialog } from '@mui/material'; - -import { DiscriminatedItem, ItemType } from '@graasp/sdk'; -import { EditButton as GraaspEditButton } from '@graasp/ui'; - -import { useBuilderTranslation } from '../../config/i18n'; -import { - EDIT_ITEM_BUTTON_CLASS, - EDIT_MODAL_ID, - buildEditButtonId, -} from '../../config/selectors'; -import { BUILDER } from '../../langs/constants'; -import BaseItemForm from '../item/form/BaseItemForm'; -import DocumentForm from '../item/form/DocumentForm'; -import EditModalWrapper, { - EditModalContentType, -} from '../item/form/EditModalWrapper'; -import FileForm from '../item/form/FileForm'; -import NameForm from '../item/form/NameForm'; - -type Props = { - item: DiscriminatedItem; -}; - -const EditButton = ({ item }: Props): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - const [open, setOpen] = useState(false); - - const handleEdit = () => { - setOpen(true); - }; - - const typeToFormComponent = (): EditModalContentType => { - switch (item.type) { - case ItemType.DOCUMENT: - return DocumentForm; - case ItemType.LOCAL_FILE: - case ItemType.S3_FILE: - return FileForm; - case ItemType.SHORTCUT: - return NameForm; - case ItemType.FOLDER: - case ItemType.LINK: - case ItemType.APP: - case ItemType.ETHERPAD: - case ItemType.H5P: - default: - return BaseItemForm; - } - }; - - return ( - <> - { - setOpen(false); - }} - id={EDIT_MODAL_ID} - open={open} - maxWidth="sm" - fullWidth - > - - - - - ); -}; - -export default EditButton; diff --git a/src/components/common/FlagButton.tsx b/src/components/common/FlagButton.tsx new file mode 100644 index 000000000..adfa3657e --- /dev/null +++ b/src/components/common/FlagButton.tsx @@ -0,0 +1,32 @@ +import { useContext } from 'react'; + +import FlagIcon from '@mui/icons-material/Flag'; +import { ListItemIcon, MenuItem } from '@mui/material'; + +import { PackedItem } from '@graasp/sdk'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { ITEM_MENU_FLAG_BUTTON_CLASS } from '@/config/selectors'; +import { BUILDER } from '@/langs/constants'; + +import { FlagItemModalContext } from '../context/FlagItemModalContext'; + +const FlagButton = ({ item }: { item: PackedItem }): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + const { openModal: openFlagModal } = useContext(FlagItemModalContext); + const handleFlag = () => { + openFlagModal?.(item.id); + }; + + return ( + + + + + {translateBuilder(BUILDER.ITEM_MENU_FLAG_MENU_ITEM)} + + ); +}; + +export default FlagButton; diff --git a/src/components/common/MoveButton.tsx b/src/components/common/MoveButton.tsx deleted file mode 100644 index 3dbd6be83..000000000 --- a/src/components/common/MoveButton.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useState } from 'react'; - -import { DiscriminatedItem } from '@graasp/sdk'; -import { - ActionButton, - ActionButtonVariant, - ColorVariants, - MoveButton as GraaspMoveButton, - type NavigationElement, -} from '@graasp/ui'; - -import { mutations } from '@/config/queryClient'; -import { getDirectParentId } from '@/utils/item'; -import { computeButtonText } from '@/utils/itemSelection'; - -import { useBuilderTranslation } from '../../config/i18n'; -import { - ITEM_MENU_MOVE_BUTTON_CLASS, - ITEM_MOVE_BUTTON_CLASS, -} from '../../config/selectors'; -import { BUILDER } from '../../langs/constants'; -import ItemSelectionModal, { - ItemSelectionModalProps, -} from '../main/itemSelectionModal/ItemSelectionModal'; - -type MoveButtonProps = { - itemIds: string[]; - color?: ColorVariants; - id?: string; - type?: ActionButtonVariant; - onClick?: () => void; -}; - -const MoveButton = ({ - itemIds, - color = 'primary', - id, - type = ActionButton.ICON_BUTTON, - onClick, -}: MoveButtonProps): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - const { mutate: moveItems } = mutations.useMoveItems(); - - const [open, setOpen] = useState(false); - - const openMoveModal = () => { - setOpen(true); - }; - - const onClose = () => { - setOpen(false); - }; - - const onConfirm: ItemSelectionModalProps['onConfirm'] = (destination) => { - moveItems({ - ids: itemIds, - to: destination, - }); - onClose(); - }; - - const handleMove = () => { - openMoveModal(); - onClick?.(); - }; - - const isDisabled = ( - items: DiscriminatedItem[], - item: NavigationElement, - homeId: string, - ) => { - if (items?.length) { - // cannot move inside self and below - const moveInSelf = items.some((i) => item.path.includes(i.path)); - - // cannot move in same direct parent - // todo: not opti because we only have the ids from the table - const directParentIds = items.map((i) => getDirectParentId(i.path)); - const moveInDirectParent = directParentIds.includes(item.id); - - // cannot move to home if was already on home - let moveToHome = false; - - moveToHome = item.id === homeId && !getDirectParentId(items[0].path); - - return moveInSelf || moveInDirectParent || moveToHome; - } - return false; - }; - - const buttonText = (name?: string) => - computeButtonText({ - translateBuilder, - translateKey: BUILDER.MOVE_BUTTON, - name, - }); - - return ( - <> - - {itemIds && open && ( - - )} - - ); -}; - -export default MoveButton; diff --git a/src/components/common/SelectTypes.tsx b/src/components/common/SelectTypes.tsx index 19c2ee9c3..0eb0b1aeb 100644 --- a/src/components/common/SelectTypes.tsx +++ b/src/components/common/SelectTypes.tsx @@ -54,7 +54,7 @@ export const SelectTypes = (): JSX.Element => { const renderValues = (value: typeof itemTypes) => ( {value.map((type) => ( - + ))} ); @@ -71,6 +71,9 @@ export const SelectTypes = (): JSX.Element => { input={} renderValue={renderValues} MenuProps={MenuProps} + sx={{ + borderRadius: 40, + }} > {types.map((type) => ( diff --git a/src/components/file/FileUploader.tsx b/src/components/file/FileUploader.tsx index 2b3c42428..2baa796f9 100644 --- a/src/components/file/FileUploader.tsx +++ b/src/components/file/FileUploader.tsx @@ -3,7 +3,7 @@ import { useParams } from 'react-router'; import { Box } from '@mui/material'; -import { MAX_NUMBER_OF_FILES_UPLOAD } from '@graasp/sdk'; +import { DiscriminatedItem, MAX_NUMBER_OF_FILES_UPLOAD } from '@graasp/sdk'; import { FileDropper } from '@graasp/ui'; import { AxiosProgressEvent } from 'axios'; @@ -18,7 +18,9 @@ type Props = { onError?: (e: Error) => void; buttons?: JSX.Element; onStart?: () => void; + /** id of the component */ id?: string; + previousItemId?: DiscriminatedItem['id']; }; const FileUploader = ({ @@ -28,6 +30,7 @@ const FileUploader = ({ onStart, buttons, id, + previousItemId, }: Props): JSX.Element | null => { const { t } = useBuilderTranslation(); const { itemId: parentItemId } = useParams(); @@ -73,6 +76,7 @@ const FileUploader = ({ await uploadFiles({ files: [files[idx]], id: parentItemId, + previousItemId, onUploadProgress: updateForManyFiles(idx), }); } catch (e) { @@ -83,7 +87,7 @@ const FileUploader = ({ }; return ( - + { diff --git a/src/components/file/FileUploaderOverlay.tsx b/src/components/file/FileUploaderOverlay.tsx deleted file mode 100644 index a8e6d3f89..000000000 --- a/src/components/file/FileUploaderOverlay.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { DragEventHandler, useEffect, useState } from 'react'; - -import { Box, styled } from '@mui/material'; - -import { FILE_UPLOAD_MAX_FILES } from '../../config/constants'; -import { UPLOADER_ID } from '../../config/selectors'; -import { useUploadWithProgress } from '../hooks/uploadWithProgress'; -import FileUploader from './FileUploader'; - -const StyledContainer = styled(Box)(({ theme }) => ({ - display: 'none', - - // used to position the file dropper above the rest of the content - position: 'absolute', - // sets the borders of the container to stick to the border of the parent - top: 0, - bottom: 0, - left: 0, - right: 0, - - margin: theme.spacing(5), - - boxSizing: 'border-box', - - // show above drawer - zIndex: theme.zIndex.drawer + 1, - opacity: 0.8, -})); - -const FileUploaderOverlay = (): JSX.Element | null => { - const [isDragging, setIsDragging] = useState(false); - const [isValid, setIsValid] = useState(true); - const { - update, - close: closeNotification, - closeAndShowError, - show, - } = useUploadWithProgress(); - - const closeUploader = () => { - setIsDragging(false); - }; - - const handleWindowDragEnter = () => { - setIsDragging(true); - }; - - const handleDragEnd = () => { - closeUploader(); - }; - - const handleDragEnter: DragEventHandler = (event) => { - // detect whether the dragged files number exceeds limit - if (event?.dataTransfer?.items) { - const nbFiles = event.dataTransfer.items.length; - - if (nbFiles > FILE_UPLOAD_MAX_FILES) { - return setIsValid(false); - } - } - - return setIsValid(true); - }; - - useEffect(() => { - window.addEventListener('dragenter', handleWindowDragEnter); - window.addEventListener('mouseout', handleDragEnd); - - return () => { - window.removeEventListener('dragenter', handleWindowDragEnter); - window.removeEventListener('mouseout', handleDragEnd); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const handleDrop = () => { - // todo: trigger error that only MAX_FILES was uploaded - // or cancel drop - closeUploader(); - }; - - const buildSx = () => { - let sx = {}; - if (isDragging) { - sx = { ...sx, display: 'flex' }; - } - if (!isValid) { - sx = { - ...sx, - '& div button': { - backgroundColor: 'red !important', - }, - }; - } - return sx; - }; - - return ( - handleDragEnter(e)} - onDragEnd={() => handleDragEnd()} - onDragLeave={() => handleDragEnd()} - onDrop={handleDrop} - > - - - ); -}; - -export default FileUploaderOverlay; diff --git a/src/components/hooks/uploadWithProgress.ts b/src/components/hooks/uploadWithProgress.ts new file mode 100644 index 000000000..f780f189f --- /dev/null +++ b/src/components/hooks/uploadWithProgress.ts @@ -0,0 +1,50 @@ +import { useRef } from 'react'; +import { Id, toast } from 'react-toastify'; + +import { AxiosProgressEvent } from 'axios'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { BUILDER } from '@/langs/constants'; + +export const useUploadWithProgress = (): { + update: (p: AxiosProgressEvent) => void; + close: (e?: Error) => void; + show: (p?: number) => void; +} => { + const { t: translateBuilder } = useBuilderTranslation(); + + // we need to keep a reference of the toastId to be able to update it + const toastId = useRef(null); + + const show = (progress = 0) => { + toastId.current = toast.info(translateBuilder(BUILDER.UPLOADING), { + progress, + position: 'bottom-left', + }); + }; + + const update = ({ progress }: AxiosProgressEvent) => { + // check if we already displayed a toast + if (toastId.current === null && progress && progress < 1) { + show(progress); + } + if (toastId.current) { + toast.update(toastId.current, { progress }); + } + }; + + const close = (error?: Error) => { + // show correct feedback message + if (error) { + toast.error(error.message); + } else if (toastId.current) { + toast.done(toastId.current); + } + // delete reference + if (toastId.current) { + toastId.current = null; + } + }; + + return { show, update, close }; +}; diff --git a/src/components/hooks/uploadWithProgress.tsx b/src/components/hooks/uploadWithProgress.tsx deleted file mode 100644 index 33fb16d47..000000000 --- a/src/components/hooks/uploadWithProgress.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useRef } from 'react'; -import { Id, toast } from 'react-toastify'; - -import type { AxiosProgressEvent } from 'axios'; - -import { useBuilderTranslation } from '@/config/i18n'; -import { BUILDER } from '@/langs/constants'; - -export const useUploadWithProgress = (): { - update: (p: AxiosProgressEvent) => void; - close: () => void; - closeAndShowError: (e: Error) => void; - show: (p?: number) => void; -} => { - const { t: translateBuilder } = useBuilderTranslation(); - - // we need to keep a reference of the toastId to be able to update it - const toastId = useRef(null); - - const show = (progress = 0) => { - toastId.current = toast.info( - translateBuilder(BUILDER.UPLOAD_NOTIFICATION_LOADING), - { - progress, - position: 'bottom-left', - }, - ); - }; - - const update = (e: AxiosProgressEvent) => { - const { progress } = e; - // check if we already displayed a toast - if (toastId.current === null && progress && progress < 1) { - show(progress); - } - if (toastId.current) { - toast.update(toastId.current, { progress }); - } - }; - - const close = () => { - if (toastId.current) { - toast.done(toastId.current); - // does not work correctly in chrome, workaround solution to close the notification - toast.update(toastId.current, { - type: 'success', - render: translateBuilder(BUILDER.UPLOAD_NOTIFICATION_COMPLETE), - autoClose: 1000, - progress: null, - }); - toastId.current = null; - } - }; - const closeAndShowError = (e: Error) => { - close(); - toast.error(e.message); - if (toastId.current) { - toastId.current = null; - } - }; - - return { show, update, close, closeAndShowError }; -}; diff --git a/src/components/item/FolderContent.tsx b/src/components/item/FolderContent.tsx new file mode 100644 index 000000000..d0c2d9d91 --- /dev/null +++ b/src/components/item/FolderContent.tsx @@ -0,0 +1,220 @@ +import { Alert, Box, Stack, Typography } from '@mui/material'; + +import { + PackedItem, + PermissionLevel, + PermissionLevelCompare, +} from '@graasp/sdk'; +import { Loader } from '@graasp/ui'; + +import { useBuilderTranslation, useEnumsTranslation } from '@/config/i18n'; +import { hooks } from '@/config/queryClient'; +import { + ITEM_SCREEN_ERROR_ALERT_ID, + buildItemsTableId, +} from '@/config/selectors'; +import { ItemLayoutMode, Ordering } from '@/enums'; +import { BUILDER } from '@/langs/constants'; + +import ErrorAlert from '../common/ErrorAlert'; +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 { 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 { useItemSearch } from './ItemSearch'; +import ModeButton from './header/ModeButton'; + +type Props = { + item: PackedItem; + searchText: string; + items?: PackedItem[]; + sortBy: SortingOptionsForFolder; +}; + +const Content = ({ item, searchText, items, sortBy }: Props) => { + const { mode } = useLayoutContext(); + const { itemTypes } = useFilterItemsContext(); + + const enableEditing = item.permission + ? PermissionLevelCompare.lte(PermissionLevel.Write, item.permission) + : false; + + if (mode === ItemLayoutMode.Map) { + return ( + <> + + + + + + ); + } + + if (items?.length) { + return ( + <> + + {Boolean(enableEditing && !searchText && !itemTypes?.length) && ( + + + + )} + + ); + } + + // no items to show because of filters + if (!items?.length && (searchText.length || itemTypes.length)) { + return ; + } + + // no items show drop zone + if ( + item.permission && + PermissionLevelCompare.gte(item.permission, PermissionLevel.Write) + ) { + return ( + + } /> + + ); + } + + return null; +}; + +/** + * Helper component to render typed folder items + */ +const FolderContent = ({ item }: { item: PackedItem }): JSX.Element => { + const { t: translateEnums } = useEnumsTranslation(); + const { shouldDisplayItem } = useFilterItemsContext(); + const { t: translateBuilder } = useBuilderTranslation(); + + const { + data: children, + isLoading, + isError, + } = hooks.useChildren(item.id, { + ordered: true, + }); + + const itemSearch = useItemSearch(); + + const { ordering, setOrdering, setSortBy, sortBy, sortFn } = + useSorting({ + sortBy: SortingOptionsForFolder.Order, + ordering: Ordering.ASC, + }); + + // TODO: use hook's filter when available + const folderChildren = children + ?.filter( + (f) => + shouldDisplayItem(f.type) && + f.name.toLowerCase().includes(itemSearch.text.toLowerCase()), + ) + .sort(sortFn); + + if (children) { + return ( + <> + + + {item.name} + + + {itemSearch.input} + + + + + + + + + + {sortBy && setSortBy && ( + + translateEnums(t1).localeCompare(translateEnums(t2)), + )} + setOrdering={setOrdering} + /> + )} + + + + + + + ); + } + + if (isLoading) { + return ; + } + + if (isError) { + return ; + } + + return ( + + {translateBuilder(BUILDER.ITEMS_TABLE_EMPTY_MESSAGE)} + + ); +}; + +export default FolderContent; diff --git a/src/components/item/ItemContent.tsx b/src/components/item/ItemContent.tsx index f7573381a..49102c57e 100644 --- a/src/components/item/ItemContent.tsx +++ b/src/components/item/ItemContent.tsx @@ -9,14 +9,12 @@ import { Context, DocumentItemType, EtherpadItemType, - FolderItemType, H5PItemType, ItemType, LinkItemType, LocalFileItemType, Member, PermissionLevel, - PermissionLevelCompare, S3FileItemType, buildPdfViewerLink, getH5PExtra, @@ -40,21 +38,16 @@ import { DOCUMENT_ITEM_TEXT_EDITOR_ID, ITEM_SCREEN_ERROR_ALERT_ID, buildFileItemId, - buildItemsTableId, } from '../../config/selectors'; import ErrorAlert from '../common/ErrorAlert'; import { useCurrentUserContext } from '../context/CurrentUserContext'; -import { useFilterItemsContext } from '../context/FilterItemsContext'; -import ItemActions from '../main/ItemActions'; -import Items from '../main/Items'; -import NewItemButton from '../main/NewItemButton'; import { OutletType } from '../pages/item/type'; -import { useItemSearch } from './ItemSearch'; +import FolderContent from './FolderContent'; import FileAlignmentSetting from './settings/file/FileAlignmentSetting'; import FileMaxWidthSetting from './settings/file/FileMaxWidthSetting'; import { SettingVariant } from './settings/settingTypes'; -const { useChildren, useFileContentUrl, useEtherpad } = hooks; +const { useFileContentUrl, useEtherpad } = hooks; const StyledContainer = styled(Container)(() => ({ flexGrow: 1, @@ -163,65 +156,6 @@ const AppContent = ({ /> ); -/** - * Helper component to render typed folder items - */ -const FolderContent = ({ - item, - enableEditing, -}: { - item: FolderItemType; - enableEditing: boolean; -}): JSX.Element => { - const { shouldDisplayItem } = useFilterItemsContext(); - - const { - data: children, - isLoading, - isError, - } = useChildren(item.id, { - ordered: true, - }); - const itemSearch = useItemSearch(); - const { canWrite, canAdmin } = useOutletContext(); - - // TODO: use hook's filter when available - const folderChildren = children?.filter( - (f) => - shouldDisplayItem(f.type) && - f.name.toLowerCase().includes(itemSearch.text.toLowerCase()), - ); - - if (isLoading) { - return ; - } - - if (isError) { - return ; - } - - return ( - ] - : [itemSearch.input] - } - // todo: not exactly correct, since you could have write rights on some child, - // but it's more tedious to check permissions over all selected items - ToolbarActions={enableEditing ? ItemActions : undefined} - totalCount={folderChildren?.length} - showDropzoneHelper - /> - ); -}; - /** * Helper component to render typed H5P items */ @@ -299,16 +233,7 @@ const ItemContent = (): JSX.Element => { case ItemType.APP: return ; case ItemType.FOLDER: - return ( - - ); + return ; case ItemType.H5P: { return ; diff --git a/src/components/item/ItemMain.tsx b/src/components/item/ItemMain.tsx index 7c3801d3d..b6d5f018c 100644 --- a/src/components/item/ItemMain.tsx +++ b/src/components/item/ItemMain.tsx @@ -1,13 +1,12 @@ import { Helmet } from 'react-helmet-async'; -import { Box, Divider, Typography, styled } from '@mui/material'; +import { Container, Divider, Stack, Typography, styled } from '@mui/material'; import { PackedItem } from '@graasp/sdk'; import { DrawerHeader } from '@graasp/ui'; import { BUILDER } from '@/langs/constants'; -import { RIGHT_MENU_WIDTH } from '../../config/constants'; import { useBuilderTranslation } from '../../config/i18n'; import { ITEM_MAIN_CLASS } from '../../config/selectors'; import Chatbox from '../common/Chatbox'; @@ -15,20 +14,21 @@ import { useLayoutContext } from '../context/LayoutContext'; import ItemPanel from './ItemPanel'; import ItemHeader from './header/ItemHeader'; -const StyledContainer = styled(Box)<{ open: boolean }>(({ theme, open }) => { +const StyledContainer = styled(Container)<{ open: boolean }>(({ + theme, + open, +}) => { const openStyles = open ? { transition: theme.transitions.create('margin', { easing: theme.transitions.easing.easeOut, duration: theme.transitions.duration.enteringScreen, }), - marginRight: RIGHT_MENU_WIDTH, } : {}; return { flexGrow: 1, - marginRight: 0, display: 'flex', flexDirection: 'column', height: '100%', @@ -58,14 +58,13 @@ const ItemMain = ({ id, children, item }: Props): JSX.Element => { {item.name} - + {isChatboxMenuOpen && ( { setIsChatboxMenuOpen(false); }} - // todo direction="rtl" > @@ -83,7 +82,7 @@ const ItemMain = ({ id, children, item }: Props): JSX.Element => { {children} - + ); }; diff --git a/src/components/item/ItemPanel.tsx b/src/components/item/ItemPanel.tsx index e6a8b1391..73ae538fd 100644 --- a/src/components/item/ItemPanel.tsx +++ b/src/components/item/ItemPanel.tsx @@ -3,8 +3,8 @@ import { Drawer, Toolbar, styled } from '@mui/material'; import { RIGHT_MENU_WIDTH } from '../../config/constants'; import { ITEM_PANEL_ID } from '../../config/selectors'; -const StyledDrawer = styled(Drawer)(({ theme }) => ({ - width: RIGHT_MENU_WIDTH, +const StyledDrawer = styled(Drawer)(({ theme, open }) => ({ + width: open ? RIGHT_MENU_WIDTH : 0, flexShrink: 0, // todo: move this to the UI theme. '.MuiDrawer-paper': { diff --git a/src/components/item/MapView.tsx b/src/components/item/MapView.tsx index 133a9a037..e1ceb8377 100644 --- a/src/components/item/MapView.tsx +++ b/src/components/item/MapView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Skeleton, Stack, Typography } from '@mui/material'; @@ -10,82 +10,15 @@ import { hooks, mutations } from '@/config/queryClient'; import { buildMapViewId } from '../../config/selectors'; import NewItemModal from '../main/NewItemModal'; +import { useCurrentLocation } from '../map/useCurrentLocation'; type Props = { - parentId?: DiscriminatedItem['id']; - title?: string; - height?: string; viewItem: (item: DiscriminatedItem) => void; viewItemInBuilder: (item: DiscriminatedItem) => void; enableGeolocation?: boolean; -}; - -const options = { - enableHighAccuracy: true, - timeout: 5000, - maximumAge: 0, -}; - -const useCurrentLocation = (enableGeolocation = true) => { - const [hasFetchedCurrentLocation, setHasFetchedCurrentLocation] = - useState(false); - - const [currentPosition, setCurrentPosition] = useState<{ - lat: number; - lng: number; - }>(); - - const getCurrentPosition = () => { - const success = (pos: { - coords: { latitude: number; longitude: number }; - }) => { - const crd = pos.coords; - setCurrentPosition({ lat: crd.latitude, lng: crd.longitude }); - setHasFetchedCurrentLocation(true); - }; - - navigator.geolocation.getCurrentPosition( - success, - (err: { code: number; message: string }) => { - // eslint-disable-next-line no-console - console.warn(`ERROR(${err.code}): ${err.message}`); - setHasFetchedCurrentLocation(true); - }, - options, - ); - }; - - // get current location - useEffect(() => { - if (enableGeolocation) { - if (navigator.permissions) { - // check permissions - // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/permissions#examples - navigator.permissions - .query({ name: 'geolocation' }) - .then(({ state }) => { - if (state === 'denied') { - console.error('geolocation denied:', state); - setHasFetchedCurrentLocation(true); - } - // allows granted and prompt values (safari) - else { - getCurrentPosition(); - } - }) - .catch((e) => { - console.error('geolocation denied:', e); - setHasFetchedCurrentLocation(true); - }); - } else { - // navigator.permissions does not exist in safari - // still try to get position for webview's ios - getCurrentPosition(); - } - } - }, [enableGeolocation]); - - return { hasFetchedCurrentLocation, currentPosition }; + parentId?: DiscriminatedItem['id']; + title?: string; + height?: string; }; const MapView = ({ diff --git a/src/components/item/copy/CopyButton.tsx b/src/components/item/copy/CopyButton.tsx new file mode 100644 index 000000000..85b809b23 --- /dev/null +++ b/src/components/item/copy/CopyButton.tsx @@ -0,0 +1,37 @@ +import { + ActionButtonVariant, + ColorVariants, + CopyButton as GraaspCopyButton, +} from '@graasp/ui'; + +import { useBuilderTranslation } from '../../../config/i18n'; +import { + ITEM_COPY_BUTTON_CLASS, + ITEM_MENU_COPY_BUTTON_CLASS, +} from '../../../config/selectors'; +import { BUILDER } from '../../../langs/constants'; + +export type Props = { + color?: ColorVariants; + id?: string; + onClick?: () => void; + type?: ActionButtonVariant; +}; + +const CopyButton = ({ color, id, type, onClick }: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + return ( + + ); +}; + +export default CopyButton; diff --git a/src/components/item/copy/CopyModal.tsx b/src/components/item/copy/CopyModal.tsx new file mode 100644 index 000000000..1d590188e --- /dev/null +++ b/src/components/item/copy/CopyModal.tsx @@ -0,0 +1,49 @@ +import { DiscriminatedItem } from '@graasp/sdk'; + +import { mutations } from '@/config/queryClient'; +import { computeButtonText } from '@/utils/itemSelection'; + +import { useBuilderTranslation } from '../../../config/i18n'; +import { BUILDER } from '../../../langs/constants'; +import ItemSelectionModal, { + ItemSelectionModalProps, +} from '../../main/itemSelectionModal/ItemSelectionModal'; + +export const CopyModal = ({ + itemIds, + open, + onClose, +}: { + open: boolean; + onClose: () => void; + itemIds: DiscriminatedItem['id'][]; +}): JSX.Element => { + const { mutate: copyItems } = mutations.useCopyItems(); + const { t: translateBuilder } = useBuilderTranslation(); + + const onConfirm: ItemSelectionModalProps['onConfirm'] = (destination) => { + copyItems({ + ids: itemIds, + to: destination, + }); + onClose(); + }; + + const buttonText = (name?: string) => + computeButtonText({ + translateBuilder, + translateKey: BUILDER.COPY_BUTTON, + name, + }); + + return ( + + ); +}; diff --git a/src/components/item/edit/EditButton.tsx b/src/components/item/edit/EditButton.tsx new file mode 100644 index 000000000..bb084d737 --- /dev/null +++ b/src/components/item/edit/EditButton.tsx @@ -0,0 +1,35 @@ +import { DiscriminatedItem } from '@graasp/sdk'; +import { + ActionButtonVariant, + EditButton as GraaspEditButton, +} from '@graasp/ui'; + +import { useBuilderTranslation } from '../../../config/i18n'; +import { + EDIT_ITEM_BUTTON_CLASS, + buildEditButtonId, +} from '../../../config/selectors'; +import { BUILDER } from '../../../langs/constants'; + +type Props = { + item: DiscriminatedItem; + type: ActionButtonVariant; + onClick?: () => void; +}; + +const EditButton = ({ item, onClick, type = 'icon' }: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + return ( + + ); +}; + +export default EditButton; diff --git a/src/components/item/form/EditModalWrapper.tsx b/src/components/item/edit/EditModal.tsx similarity index 77% rename from src/components/item/form/EditModalWrapper.tsx rename to src/components/item/edit/EditModal.tsx index 0f1885fac..cca128f93 100644 --- a/src/components/item/form/EditModalWrapper.tsx +++ b/src/components/item/edit/EditModal.tsx @@ -1,15 +1,16 @@ -import { ComponentType as CT, Dispatch, useState } from 'react'; +import { ComponentType as CT, useState } from 'react'; import { toast } from 'react-toastify'; import { Button, + Dialog, DialogActions, DialogContent, DialogTitle, } from '@mui/material'; import { routines } from '@graasp/query-client'; -import { DiscriminatedItem } from '@graasp/sdk'; +import { DiscriminatedItem, ItemType } from '@graasp/sdk'; import { COMMON, FAILURE_MESSAGES } from '@graasp/translations'; import isEqual from 'lodash.isequal'; @@ -20,11 +21,16 @@ import notifier from '@/config/notifier'; import { mutations } from '@/config/queryClient'; import { EDIT_ITEM_MODAL_CANCEL_BUTTON_ID, + EDIT_MODAL_ID, ITEM_FORM_CONFIRM_BUTTON_ID, } from '@/config/selectors'; import { isItemValid } from '@/utils/item'; import { BUILDER } from '../../../langs/constants'; +import BaseItemForm from '../form/BaseItemForm'; +import DocumentForm from '../form/DocumentForm'; +import FileForm from '../form/FileForm'; +import NameForm from '../form/NameForm'; const { editItemRoutine } = routines; @@ -36,17 +42,12 @@ export interface EditModalContentPropType { export type EditModalContentType = CT; type Props = { - ComponentType: EditModalContentType; - item: DiscriminatedItem; - setOpen: Dispatch; + onClose: () => void; + open: boolean; }; -const EditModalWrapper = ({ - item, - setOpen, - ComponentType, -}: Props): JSX.Element => { +const EditModal = ({ item, onClose, open }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { t: translateCommon } = useCommonTranslation(); @@ -56,9 +57,24 @@ const EditModalWrapper = ({ // so only necessary properties are sent when editing const [updatedItem, setUpdatedItem] = useState(item); - const onClose = () => { - setOpen(false); - }; + const ComponentType = ((): EditModalContentType => { + switch (item.type) { + case ItemType.DOCUMENT: + return DocumentForm; + case ItemType.LOCAL_FILE: + case ItemType.S3_FILE: + return FileForm; + case ItemType.SHORTCUT: + return NameForm; + case ItemType.FOLDER: + case ItemType.LINK: + case ItemType.APP: + case ItemType.ETHERPAD: + case ItemType.H5P: + default: + return BaseItemForm; + } + })(); const submit = () => { if ( @@ -103,7 +119,13 @@ const EditModalWrapper = ({ }; return ( - <> + {translateBuilder(BUILDER.EDIT_ITEM_MODAL_TITLE)} @@ -138,7 +160,7 @@ const EditModalWrapper = ({ {translateCommon(COMMON.SAVE_BUTTON)} - + ); }; -export default EditModalWrapper; +export default EditModal; diff --git a/src/components/item/form/BaseItemForm.tsx b/src/components/item/form/BaseItemForm.tsx index 05b34f072..1f0cf5fbe 100644 --- a/src/components/item/form/BaseItemForm.tsx +++ b/src/components/item/form/BaseItemForm.tsx @@ -1,8 +1,8 @@ import { Box } from '@mui/material'; import { FOLDER_FORM_DESCRIPTION_ID } from '../../../config/selectors'; +import type { EditModalContentPropType } from '../edit/EditModal'; import DescriptionForm from './DescriptionForm'; -import { EditModalContentPropType } from './EditModalWrapper'; import NameForm from './NameForm'; const BaseItemForm = ({ diff --git a/src/components/item/form/DocumentForm.tsx b/src/components/item/form/DocumentForm.tsx index fb1d92fc0..1b884fc6e 100644 --- a/src/components/item/form/DocumentForm.tsx +++ b/src/components/item/form/DocumentForm.tsx @@ -36,7 +36,7 @@ import { ITEM_FORM_DOCUMENT_TEXT_ID, } from '../../../config/selectors'; import { BUILDER } from '../../../langs/constants'; -import type { EditModalContentPropType } from './EditModalWrapper'; +import type { EditModalContentPropType } from '../edit/EditModal'; import NameForm from './NameForm'; enum EditorMode { diff --git a/src/components/item/form/FileForm.tsx b/src/components/item/form/FileForm.tsx index 60cb8559e..6dc860957 100644 --- a/src/components/item/form/FileForm.tsx +++ b/src/components/item/form/FileForm.tsx @@ -7,8 +7,8 @@ import { ITEM_FORM_IMAGE_ALT_TEXT_EDIT_FIELD_ID } from '@/config/selectors'; import { getExtraFromPartial } from '@/utils/itemExtra'; import { BUILDER } from '../../../langs/constants'; +import type { EditModalContentPropType } from '../edit/EditModal'; import DescriptionForm from './DescriptionForm'; -import { EditModalContentPropType } from './EditModalWrapper'; import NameForm from './NameForm'; const FileForm = (props: EditModalContentPropType): JSX.Element | null => { diff --git a/src/components/item/form/NameForm.tsx b/src/components/item/form/NameForm.tsx index 7191bcbb1..b63e67679 100644 --- a/src/components/item/form/NameForm.tsx +++ b/src/components/item/form/NameForm.tsx @@ -6,7 +6,7 @@ import { IconButton, TextField } from '@mui/material'; import { useBuilderTranslation } from '../../../config/i18n'; import { ITEM_FORM_NAME_INPUT_ID } from '../../../config/selectors'; import { BUILDER } from '../../../langs/constants'; -import type { EditModalContentPropType } from './EditModalWrapper'; +import type { EditModalContentPropType } from '../edit/EditModal'; export type NameFormProps = EditModalContentPropType & { required?: boolean; diff --git a/src/components/item/header/Actions.tsx b/src/components/item/header/Actions.tsx new file mode 100644 index 000000000..27a0e821e --- /dev/null +++ b/src/components/item/header/Actions.tsx @@ -0,0 +1,126 @@ +import { MouseEvent, useState } from 'react'; + +import { MoreVert } from '@mui/icons-material'; +import { Divider, IconButton, Menu } from '@mui/material'; + +import { + ItemType, + PackedItem, + PermissionLevel, + PermissionLevelCompare, +} from '@graasp/sdk'; +import { ActionButton } from '@graasp/ui'; + +import useModalStatus from '@/components/hooks/useModalStatus'; +import { hooks } from '@/config/queryClient'; + +import BookmarkButton from '../../common/BookmarkButton'; +import CollapseButton from '../../common/CollapseButton'; +import FlagButton from '../../common/FlagButton'; +import HideButton from '../../common/HideButton'; +import PinButton from '../../common/PinButton'; +import RecycleButton from '../../common/RecycleButton'; +import CreateShortcutButton from '../shortcut/CreateShortcutButton'; +import CreateShortcutModal from '../shortcut/CreateShortcutModal'; + +type Props = { + item: PackedItem; +}; + +const internalId = 'menu'; + +/** + * Menu of actions for item header + * contains less actions since some of them are outside + * or does not make sense in the context of the item + */ +const Actions = ({ item }: Props): JSX.Element | null => { + const { data: member } = hooks.useCurrentMember(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: MouseEvent): void => { + setAnchorEl(event.currentTarget); + }; + const closeMenu = (): void => { + setAnchorEl(null); + }; + const { + isOpen: isCreateShortcutOpen, + openModal: openCreateShortcutModal, + closeModal: closeCreateShortcutModal, + } = useModalStatus(); + + const canWrite = + item.permission && + PermissionLevelCompare.gte(item.permission, PermissionLevel.Write); + const canAdmin = + item.permission && + PermissionLevelCompare.gte(item.permission, PermissionLevel.Admin); + + if (!member?.id) { + return null; + } + + return ( + <> + + + + + + { + openCreateShortcutModal(); + closeMenu(); + }} + /> + + {canWrite && ( + <> + + + + {item.type !== ItemType.FOLDER && ( + + )} + + )} + {canAdmin ? ( + <> + + + + ) : ( + + )} + + + + ); +}; + +export default Actions; diff --git a/src/components/item/header/ItemHeader.tsx b/src/components/item/header/ItemHeader.tsx index 19b942ecd..3565a3c4e 100644 --- a/src/components/item/header/ItemHeader.tsx +++ b/src/components/item/header/ItemHeader.tsx @@ -1,4 +1,9 @@ -import { Stack } from '@mui/material'; +import { useParams } from 'react-router'; + +import { Alert, Stack } from '@mui/material'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { BUILDER } from '@/langs/constants'; import { ITEM_HEADER_ID } from '../../../config/selectors'; import Navigation from '../../layout/Navigation'; @@ -8,18 +13,28 @@ type Props = { showNavigation?: boolean; }; -const ItemHeader = ({ showNavigation = true }: Props): JSX.Element | null => ( - - {/* display empty div to render actions on the right */} - {showNavigation ? :
} - - -); +const ItemHeader = ({ showNavigation = true }: Props): JSX.Element | null => { + const { itemId } = useParams(); + const { t: translateBuilder } = useBuilderTranslation(); + return ( + + {/* display empty div to render actions on the right */} + {showNavigation ? :
} + {itemId ? ( + + ) : ( + + {translateBuilder(BUILDER.ERROR_MESSAGE)} + + )} + + ); +}; export default ItemHeader; diff --git a/src/components/item/header/ItemHeaderActions.tsx b/src/components/item/header/ItemHeaderActions.tsx index 33261f5e9..b1039e2e9 100644 --- a/src/components/item/header/ItemHeaderActions.tsx +++ b/src/components/item/header/ItemHeaderActions.tsx @@ -1,11 +1,13 @@ -import { useParams } from 'react-router-dom'; - import { Stack } from '@mui/material'; -import { PermissionLevel, PermissionLevelCompare } from '@graasp/sdk'; +import { + DiscriminatedItem, + PermissionLevel, + PermissionLevelCompare, +} from '@graasp/sdk'; import { ChatboxButton } from '@graasp/ui'; -import EditButton from '@/components/common/EditButton'; +import useModalStatus from '@/components/hooks/useModalStatus'; import DownloadButton from '@/components/main/DownloadButton'; import { ITEM_TYPES_WITH_CAPTIONS } from '../../../config/constants'; @@ -16,19 +18,27 @@ import { BUILDER } from '../../../langs/constants'; import PublishButton from '../../common/PublishButton'; import ShareButton from '../../common/ShareButton'; import { useLayoutContext } from '../../context/LayoutContext'; -import ItemMenu from '../../main/ItemMenu'; +import EditButton from '../edit/EditButton'; +import EditModal from '../edit/EditModal'; import ItemSettingsButton from '../settings/ItemSettingsButton'; -import ModeButton from './ModeButton'; +import Actions from './Actions'; const { useItem } = hooks; -const ItemHeaderActions = (): JSX.Element => { - const { itemId } = useParams(); +type Props = { + itemId: DiscriminatedItem['id']; +}; + +const ItemHeaderActions = ({ itemId }: Props): JSX.Element | null => { const { t: translateBuilder } = useBuilderTranslation(); const { editingItemId, isChatboxMenuOpen, setIsChatboxMenuOpen } = useLayoutContext(); - const { data: item } = useItem(itemId); + const { + isOpen: isEditModalOpen, + openModal: openEditModal, + closeModal: closeEditModal, + } = useModalStatus(); const canWrite = item?.permission ? PermissionLevelCompare.gte(item.permission, PermissionLevel.Write) @@ -41,50 +51,46 @@ const ItemHeaderActions = (): JSX.Element => { setIsChatboxMenuOpen(!isChatboxMenuOpen); }; - const renderItemActions = () => { - // if id is defined, we are looking at an item - if (item && item?.id) { - // show edition only for allowed types - const showEditButton = - !editingItemId && - ITEM_TYPES_WITH_CAPTIONS.includes(item.type) && - canWrite; + // if id is defined, we are looking at an item + if (item && item?.id) { + // show edition only for allowed types + const showEditButton = + !editingItemId && + ITEM_TYPES_WITH_CAPTIONS.includes(item.type) && + canWrite; - return ( - <> - {showEditButton && } - - {/* prevent moving from top header to avoid confusion */} - + return ( + + {showEditButton && ( + <> + + + + )} + - - - {canAdmin && } - {canWrite && } - - ); - } - return null; - }; + + + {canAdmin && } + {canWrite && } + {/* prevent moving from top header to avoid confusion */} + + + ); + } - return ( - - {renderItemActions()} - - - ); + return null; }; export default ItemHeaderActions; diff --git a/src/components/item/header/ModeButton.tsx b/src/components/item/header/ModeButton.tsx index 5690a8f21..22872021a 100644 --- a/src/components/item/header/ModeButton.tsx +++ b/src/components/item/header/ModeButton.tsx @@ -1,4 +1,5 @@ import { MouseEvent, useState } from 'react'; +import { useMatch } from 'react-router'; import { List as ListIcon, @@ -9,6 +10,7 @@ import { IconButton } from '@mui/material'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; +import { HOME_PATH, buildItemPath } from '@/config/paths'; import { LAYOUT_MODE_BUTTON_ID } from '@/config/selectors'; import { ItemLayoutMode } from '../../../enums'; @@ -33,6 +35,9 @@ const ModeButton = (): JSX.Element | null => { const handleClick = (event: MouseEvent) => { setAnchorEl(event.currentTarget); }; + const isHomePath = useMatch(HOME_PATH); + const isItemPath = useMatch(buildItemPath()); + const handleClose = () => { setAnchorEl(null); }; @@ -42,13 +47,19 @@ const ModeButton = (): JSX.Element | null => { handleClose(); }; + // show map only for home and path + let options = Object.values(ItemLayoutMode); + if (!isHomePath && !isItemPath) { + options = options.filter((o) => o !== ItemLayoutMode.Map); + } + return ( <> {modeToIcon(mode)} - {Object.values(ItemLayoutMode).map((value) => ( + {options.map((value) => ( handleChange(value)} diff --git a/src/components/item/move/MoveButton.tsx b/src/components/item/move/MoveButton.tsx new file mode 100644 index 000000000..e473ce3c7 --- /dev/null +++ b/src/components/item/move/MoveButton.tsx @@ -0,0 +1,43 @@ +import { + ActionButton, + ActionButtonVariant, + ColorVariants, + MoveButton as GraaspMoveButton, +} from '@graasp/ui'; + +import { useBuilderTranslation } from '../../../config/i18n'; +import { + ITEM_MENU_MOVE_BUTTON_CLASS, + ITEM_MOVE_BUTTON_CLASS, +} from '../../../config/selectors'; +import { BUILDER } from '../../../langs/constants'; + +type MoveButtonProps = { + color?: ColorVariants; + id?: string; + type?: ActionButtonVariant; + onClick?: () => void; +}; + +const MoveButton = ({ + color = 'primary', + id, + type = ActionButton.ICON_BUTTON, + onClick, +}: MoveButtonProps): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + return ( + + ); +}; + +export default MoveButton; diff --git a/src/components/item/move/MoveModal.tsx b/src/components/item/move/MoveModal.tsx new file mode 100644 index 000000000..2f89a1b46 --- /dev/null +++ b/src/components/item/move/MoveModal.tsx @@ -0,0 +1,79 @@ +import { DiscriminatedItem, PackedItem } from '@graasp/sdk'; +import { type NavigationElement } from '@graasp/ui'; + +import { mutations } from '@/config/queryClient'; +import { getDirectParentId } from '@/utils/item'; +import { computeButtonText } from '@/utils/itemSelection'; + +import { useBuilderTranslation } from '../../../config/i18n'; +import { BUILDER } from '../../../langs/constants'; +import ItemSelectionModal, { + ItemSelectionModalProps, +} from '../../main/itemSelectionModal/ItemSelectionModal'; + +type MoveButtonProps = { + items?: PackedItem[]; + open: boolean; + onClose: () => void; +}; + +export const MoveModal = ({ + onClose, + items, + open, +}: MoveButtonProps): JSX.Element | null => { + const { t: translateBuilder } = useBuilderTranslation(); + const { mutate: moveItems } = mutations.useMoveItems(); + + const onConfirm: ItemSelectionModalProps['onConfirm'] = (destination) => { + if (items) { + moveItems({ + items, + to: destination, + }); + } + }; + + const isDisabled = ( + itemsArray: DiscriminatedItem[], + item: NavigationElement, + homeId: string, + ) => { + if (itemsArray?.length) { + // cannot move inside self and below + const moveInSelf = itemsArray.some((i) => item.path.includes(i.path)); + + // cannot move in same direct parent + // todo: not opti because we only have the ids from the table + const directParentIds = itemsArray.map((i) => getDirectParentId(i.path)); + const moveInDirectParent = directParentIds.includes(item.id); + + // cannot move to home if was already on home + let moveToHome = false; + + moveToHome = item.id === homeId && !getDirectParentId(itemsArray[0].path); + + return moveInSelf || moveInDirectParent || moveToHome; + } + return false; + }; + + const buttonText = (name?: string) => + computeButtonText({ + translateBuilder, + translateKey: BUILDER.MOVE_BUTTON, + name, + }); + + return items ? ( + i.id)} + /> + ) : null; +}; diff --git a/src/components/item/shortcut/CreateShortcutButton.tsx b/src/components/item/shortcut/CreateShortcutButton.tsx new file mode 100644 index 000000000..9435eb88c --- /dev/null +++ b/src/components/item/shortcut/CreateShortcutButton.tsx @@ -0,0 +1,25 @@ +import LabelImportantIcon from '@mui/icons-material/LabelImportant'; +import { ListItemIcon, MenuItem } from '@mui/material'; + +import { useBuilderTranslation } from '../../../config/i18n'; +import { ITEM_MENU_SHORTCUT_BUTTON_CLASS } from '../../../config/selectors'; +import { BUILDER } from '../../../langs/constants'; + +export type Props = { + onClick?: () => void; +}; + +const CreateShortcutButton = ({ onClick }: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + return ( + + + + + {translateBuilder(BUILDER.ITEM_MENU_CREATE_SHORTCUT_MENU_ITEM)} + + ); +}; + +export default CreateShortcutButton; diff --git a/src/components/item/shortcut/CreateShortcutModal.tsx b/src/components/item/shortcut/CreateShortcutModal.tsx new file mode 100644 index 000000000..ec454964f --- /dev/null +++ b/src/components/item/shortcut/CreateShortcutModal.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react'; + +import { + DiscriminatedItem, + ItemType, + ShortcutItemType, + buildShortcutExtra, +} from '@graasp/sdk'; + +import { mutations } from '@/config/queryClient'; +import { computeButtonText } from '@/utils/itemSelection'; + +import { useBuilderTranslation } from '../../../config/i18n'; +import { BUILDER } from '../../../langs/constants'; +import ItemSelectionModal, { + ItemSelectionModalProps, +} from '../../main/itemSelectionModal/ItemSelectionModal'; + +export type Props = { + item: DiscriminatedItem; + onClose: () => void; + open: boolean; +}; + +const CreateShortcutModal = ({ + item: defaultItem, + onClose, + open, +}: Props): JSX.Element | null => { + const { t: translateBuilder } = useBuilderTranslation(); + const { mutate: createShortcut } = mutations.usePostItem(); + const [item] = useState(defaultItem); + + const onConfirm: ItemSelectionModalProps['onConfirm'] = (destination) => { + const target = item.id; // id of the item where the shortcut is pointing + + const shortcut: Partial & + Pick & { + parentId?: string; + } = { + name: translateBuilder(BUILDER.CREATE_SHORTCUT_DEFAULT_NAME, { + name: item?.name, + }), + extra: buildShortcutExtra(target), + type: ItemType.SHORTCUT, + parentId: destination, + }; + + createShortcut(shortcut); + onClose(); + }; + + const buttonText = (name?: string) => + computeButtonText({ + translateBuilder, + translateKey: BUILDER.CREATE_SHORTCUT_BUTTON, + name, + }); + + if (item && open) { + return ( + + ); + } + + return null; +}; + +export default CreateShortcutModal; diff --git a/src/components/main/CopyButton.tsx b/src/components/main/CopyButton.tsx deleted file mode 100644 index 1774d65fb..000000000 --- a/src/components/main/CopyButton.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useState } from 'react'; - -import { - ActionButtonVariant, - ColorVariants, - CopyButton as GraaspCopyButton, -} from '@graasp/ui'; - -import { mutations } from '@/config/queryClient'; -import { computeButtonText } from '@/utils/itemSelection'; - -import { useBuilderTranslation } from '../../config/i18n'; -import { - ITEM_COPY_BUTTON_CLASS, - ITEM_MENU_COPY_BUTTON_CLASS, -} from '../../config/selectors'; -import { BUILDER } from '../../langs/constants'; -import ItemSelectionModal, { - ItemSelectionModalProps, -} from './itemSelectionModal/ItemSelectionModal'; - -export type Props = { - color?: ColorVariants; - id?: string; - onClick?: () => void; - type?: ActionButtonVariant; - itemIds: string[]; -}; - -const CopyButton = ({ - itemIds, - color, - id, - type, - onClick, -}: Props): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - const { mutate: copyItems } = mutations.useCopyItems(); - const [open, setOpen] = useState(false); - - const openCopyModal = () => { - setOpen(true); - }; - - const onClose = () => { - setOpen(false); - }; - - const onConfirm: ItemSelectionModalProps['onConfirm'] = (destination) => { - copyItems({ - ids: itemIds, - to: destination, - }); - onClose(); - }; - - const handleCopy = () => { - openCopyModal(); - onClick?.(); - }; - - const buttonText = (name?: string) => - computeButtonText({ - translateBuilder, - translateKey: BUILDER.COPY_BUTTON, - name, - }); - - return ( - <> - - - {itemIds && open && ( - - )} - - ); -}; - -export default CopyButton; diff --git a/src/components/main/CreateShortcutButton.tsx b/src/components/main/CreateShortcutButton.tsx deleted file mode 100644 index 106366f05..000000000 --- a/src/components/main/CreateShortcutButton.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useState } from 'react'; - -import LabelImportantIcon from '@mui/icons-material/LabelImportant'; -import { ListItemIcon, MenuItem } from '@mui/material'; - -import { - DiscriminatedItem, - ItemType, - ShortcutItemType, - buildShortcutExtra, -} from '@graasp/sdk'; - -import { mutations } from '@/config/queryClient'; -import { computeButtonText } from '@/utils/itemSelection'; - -import { useBuilderTranslation } from '../../config/i18n'; -import { ITEM_MENU_SHORTCUT_BUTTON_CLASS } from '../../config/selectors'; -import { BUILDER } from '../../langs/constants'; -import ItemSelectionModal, { - ItemSelectionModalProps, -} from './itemSelectionModal/ItemSelectionModal'; - -export type Props = { - item: DiscriminatedItem; - onClick?: () => void; -}; - -const CreateShortcutButton = ({ - item: defaultItem, - onClick, -}: Props): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - const { mutate: createShortcut } = mutations.usePostItem(); - const [open, setOpen] = useState(false); - const [item, setItem] = useState(defaultItem); - - const openShortcutModal = (newItem: DiscriminatedItem) => { - setOpen(true); - setItem(newItem); - }; - - const onClose = () => { - setOpen(false); - }; - - const onConfirm: ItemSelectionModalProps['onConfirm'] = (destination) => { - const target = item.id; // id of the item where the shortcut is pointing - - const shortcut: Partial & - Pick & { - parentId?: string; - } = { - name: translateBuilder(BUILDER.CREATE_SHORTCUT_DEFAULT_NAME, { - name: item?.name, - }), - extra: buildShortcutExtra(target), - type: ItemType.SHORTCUT, - parentId: destination, - }; - - createShortcut(shortcut); - onClose(); - }; - - const handleShortcut = () => { - openShortcutModal(item); - onClick?.(); - }; - - const buttonText = (name?: string) => - computeButtonText({ - translateBuilder, - translateKey: BUILDER.CREATE_SHORTCUT_BUTTON, - name, - }); - - return ( - <> - - - - - {translateBuilder(BUILDER.ITEM_MENU_CREATE_SHORTCUT_MENU_ITEM)} - - - {item && open && ( - - )} - - ); -}; - -export default CreateShortcutButton; diff --git a/src/components/main/EmptyItem.tsx b/src/components/main/EmptyItem.tsx deleted file mode 100644 index 28f0ce078..000000000 --- a/src/components/main/EmptyItem.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Typography } from '@mui/material'; - -import { useBuilderTranslation } from '../../config/i18n'; -import { ITEMS_GRID_NO_ITEM_ID } from '../../config/selectors'; -import { BUILDER } from '../../langs/constants'; - -const EmptyItem = (): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - - return ( - - {translateBuilder(BUILDER.EMPTY_ITEM_MESSAGE)} - - ); -}; - -export default EmptyItem; diff --git a/src/components/main/ImportH5P.tsx b/src/components/main/ImportH5P.tsx index d824d7963..911f9c2aa 100644 --- a/src/components/main/ImportH5P.tsx +++ b/src/components/main/ImportH5P.tsx @@ -2,7 +2,11 @@ import { useParams } from 'react-router-dom'; import { Box, Typography } from '@mui/material'; -import { MAX_ZIP_FILE_SIZE, formatFileSize } from '@graasp/sdk'; +import { + DiscriminatedItem, + MAX_ZIP_FILE_SIZE, + formatFileSize, +} from '@graasp/sdk'; import { UploadFileButton } from '@graasp/ui'; import { mutations } from '@/config/queryClient'; @@ -14,16 +18,14 @@ import { useUploadWithProgress } from '../hooks/uploadWithProgress'; const ImportH5P = ({ onComplete, + previousItemId, }: { onComplete?: () => void; + previousItemId?: DiscriminatedItem['id']; }): JSX.Element => { const { itemId } = useParams(); const { mutateAsync: importH5P, isLoading } = mutations.useImportH5P(); - const { - update, - close: closeNotification, - closeAndShowError, - } = useUploadWithProgress(); + const { update, close: closeNotification } = useUploadWithProgress(); const { t: translateBuilder } = useBuilderTranslation(); return ( @@ -47,6 +49,7 @@ const ImportH5P = ({ importH5P({ onUploadProgress: update, id: itemId, + previousItemId, file: e.target.files[0], }) .then(() => { @@ -54,7 +57,7 @@ const ImportH5P = ({ onComplete?.(); }) .catch((error) => { - closeAndShowError(error); + closeNotification(error); }); } }} diff --git a/src/components/main/ImportZip.tsx b/src/components/main/ImportZip.tsx index ab841f589..a685a12c0 100644 --- a/src/components/main/ImportZip.tsx +++ b/src/components/main/ImportZip.tsx @@ -15,11 +15,7 @@ import { useUploadWithProgress } from '../hooks/uploadWithProgress'; const ImportZip = (): JSX.Element => { const { itemId } = useParams(); const { mutateAsync: importZip } = mutations.useImportZip(); - const { - update, - close: closeNotification, - closeAndShowError, - } = useUploadWithProgress(); + const { update, close: closeNotification } = useUploadWithProgress(); const { t: translateBuilder } = useBuilderTranslation(); @@ -49,7 +45,7 @@ const ImportZip = (): JSX.Element => { closeNotification(); }) .catch((error) => { - closeAndShowError(error); + closeNotification(error); }); } }} diff --git a/src/components/main/ItemActions.tsx b/src/components/main/ItemActions.tsx deleted file mode 100644 index 6b4fa497c..000000000 --- a/src/components/main/ItemActions.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { - ITEMS_TABLE_COPY_SELECTED_ITEMS_ID, - ITEMS_TABLE_MOVE_SELECTED_ITEMS_ID, - ITEMS_TABLE_RECYCLE_SELECTED_ITEMS_ID, -} from '../../config/selectors'; -import MoveButton from '../common/MoveButton'; -import RecycleButton from '../common/RecycleButton'; -import CopyButton from './CopyButton'; - -type Props = { - selectedIds: string[]; -}; -// todo: not used anymore ? -const ItemActionsRenderer = ({ selectedIds }: Props): JSX.Element => ( - <> - - - - -); - -export default ItemActionsRenderer; diff --git a/src/components/main/ItemCard.tsx b/src/components/main/ItemCard.tsx deleted file mode 100644 index ab579b511..000000000 --- a/src/components/main/ItemCard.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { CSSProperties, PropsWithChildren } from 'react'; -import { Link, useSearchParams } from 'react-router-dom'; - -import { Box } from '@mui/material'; - -import { - ItemType, - PackedItem, - PermissionLevel, - PermissionLevelCompare, - ThumbnailSize, -} from '@graasp/sdk'; -import { Card as GraaspCard, ItemIcon, Thumbnail } from '@graasp/ui'; - -import truncate from 'lodash.truncate'; - -import { DESCRIPTION_MAX_LENGTH } from '../../config/constants'; -import { buildItemPath } from '../../config/paths'; -import { hooks } from '../../config/queryClient'; -import { buildItemCard, buildItemLink } from '../../config/selectors'; -import { stripHtml } from '../../utils/item'; -import BookmarkButton from '../common/BookmarkButton'; -import EditButton from '../common/EditButton'; -import { useCurrentUserContext } from '../context/CurrentUserContext'; -import BadgesCellRenderer, { ItemsStatuses } from '../table/BadgesCellRenderer'; -import DownloadButton from './DownloadButton'; -import ItemMenu from './ItemMenu'; - -const NameWrapper = ({ id, style }: { id: string; style: CSSProperties }) => { - const [searchParams] = useSearchParams(); - const NameComponent = ({ - children, - }: PropsWithChildren): JSX.Element => ( - - {children} - - ); - return NameComponent; -}; - -type Props = { - item: PackedItem; - itemsStatuses?: ItemsStatuses; - canMove?: boolean; -}; - -const ItemComponent = ({ - item, - itemsStatuses, - canMove = true, -}: Props): JSX.Element => { - const { id, name } = item; - const { data: thumbnailUrl, isLoading } = hooks.useItemThumbnailUrl({ - id, - size: ThumbnailSize.Medium, - }); - - const alt = name; - const defaultValueComponent = ( - - - - ); - - const linkUrl = - item.type === ItemType.LINK - ? item?.extra?.[ItemType.LINK]?.thumbnails?.[0] - : undefined; - - const ThumbnailComponent = ( - - ); - - const { data: member } = useCurrentUserContext(); - - const canWrite = item.permission - ? PermissionLevelCompare.gte(item.permission, PermissionLevel.Write) - : false; - const canAdmin = item.permission - ? PermissionLevelCompare.gte(item.permission, PermissionLevel.Admin) - : false; - - const Actions = ( - <> - {canWrite && } - {((member && member.id) || itemsStatuses?.[item.id]?.isPublic) && ( - - )} - {member && member.id && } - - ); - // here we use the same component as the table this is why it is instantiated a bit weirdly - const Badges = BadgesCellRenderer({ itemsStatuses }); - - return ( - } - name={item.name} - creator={item.creator?.name} - ItemMenu={ - - } - Thumbnail={ThumbnailComponent} - cardId={buildItemCard(item.id)} - NameWrapper={NameWrapper({ - id: item.id, - style: { - textDecoration: 'none', - color: 'inherit', - }, - })} - /> - ); -}; - -export default ItemComponent; diff --git a/src/components/main/ItemMenu.tsx b/src/components/main/ItemMenu.tsx deleted file mode 100644 index fe5545509..000000000 --- a/src/components/main/ItemMenu.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { useContext, useState } from 'react'; - -import FileCopyIcon from '@mui/icons-material/FileCopy'; -import FlagIcon from '@mui/icons-material/Flag'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; -import { - IconButton, - IconButtonProps, - ListItemIcon, - Menu, - MenuItem, -} from '@mui/material'; - -import { PackedItem } from '@graasp/sdk'; -import { ActionButton } from '@graasp/ui'; - -import { mutations } from '@/config/queryClient'; -import { getParentsIdsFromPath } from '@/utils/item'; - -import { useBuilderTranslation } from '../../config/i18n'; -import { - ITEM_MENU_BUTTON_CLASS, - ITEM_MENU_DUPLICATE_BUTTON_CLASS, - ITEM_MENU_FLAG_BUTTON_CLASS, - buildItemMenu, - buildItemMenuButtonId, -} from '../../config/selectors'; -import { BUILDER } from '../../langs/constants'; -import BookmarkButton from '../common/BookmarkButton'; -import CollapseButton from '../common/CollapseButton'; -import HideButton from '../common/HideButton'; -import MoveButton from '../common/MoveButton'; -import PinButton from '../common/PinButton'; -import RecycleButton from '../common/RecycleButton'; -import { useCurrentUserContext } from '../context/CurrentUserContext'; -import { FlagItemModalContext } from '../context/FlagItemModalContext'; -import ItemSettingsButton from '../item/settings/ItemSettingsButton'; -import CopyButton from './CopyButton'; -import CreateShortcutButton from './CreateShortcutButton'; - -type Props = { - item: PackedItem; - canWrite?: boolean; - canAdmin?: boolean; - canMove?: boolean; -}; - -const ItemMenu = ({ - item, - canWrite = false, - canAdmin = false, - canMove = true, -}: Props): JSX.Element | null => { - const { data: member } = useCurrentUserContext(); - const [anchorEl, setAnchorEl] = useState(null); - const { t: translateBuilder } = useBuilderTranslation(); - const { openModal: openFlagModal } = useContext(FlagItemModalContext); - const { mutate: copyItems } = mutations.useCopyItems(); - - const handleClick: IconButtonProps['onClick'] = (event) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - const handleFlag = () => { - openFlagModal?.(item.id); - handleClose(); - }; - - const handleDuplicate = () => { - const parentsIds = getParentsIdsFromPath(item.path); - // get the close parent if not then undefined - const to = - parentsIds.length > 1 ? parentsIds[parentsIds.length - 2] : undefined; - - copyItems({ - ids: [item.id], - to, - }); - }; - const renderEditorActions = () => { - if (canWrite) { - return [ - , - canMove ? ( - - ) : undefined, - , - , - , - canAdmin ? ( - - ) : undefined, - ].filter(Boolean); - } - return null; - }; - - if (member?.id) { - return ( - <> - - - - - - - - - - - {translateBuilder(BUILDER.ITEM_MENU_DUPLICATE_MENU_ITEM)} - - - - {renderEditorActions()} - - - - - {translateBuilder(BUILDER.ITEM_MENU_FLAG_MENU_ITEM)} - - - - ); - } - return null; -}; - -export default ItemMenu; diff --git a/src/components/main/ItemMenuContent.tsx b/src/components/main/ItemMenuContent.tsx new file mode 100644 index 000000000..0e25beb81 --- /dev/null +++ b/src/components/main/ItemMenuContent.tsx @@ -0,0 +1,205 @@ +import { MouseEvent, useState } from 'react'; + +import { MoreVert } from '@mui/icons-material'; +import { Divider, IconButton, Menu } from '@mui/material'; + +import { + ItemType, + PackedItem, + PermissionLevel, + PermissionLevelCompare, +} from '@graasp/sdk'; +import { ActionButton } from '@graasp/ui'; + +import { hooks } from '@/config/queryClient'; +import { buildItemMenuId } from '@/config/selectors'; + +import BookmarkButton from '../common/BookmarkButton'; +import CollapseButton from '../common/CollapseButton'; +import DuplicateButton from '../common/DuplicateButton'; +import FlagButton from '../common/FlagButton'; +import HideButton from '../common/HideButton'; +import PinButton from '../common/PinButton'; +import RecycleButton from '../common/RecycleButton'; +import useModalStatus from '../hooks/useModalStatus'; +import CopyButton from '../item/copy/CopyButton'; +import { CopyModal } from '../item/copy/CopyModal'; +import EditButton from '../item/edit/EditButton'; +import EditModal from '../item/edit/EditModal'; +import MoveButton from '../item/move/MoveButton'; +import { MoveModal } from '../item/move/MoveModal'; +import ItemSettingsButton from '../item/settings/ItemSettingsButton'; +import CreateShortcutButton from '../item/shortcut/CreateShortcutButton'; +import CreateShortcutModal from '../item/shortcut/CreateShortcutModal'; + +type Props = { + item: PackedItem; +}; + +/** + * Menu of actions for item card + */ +const ItemMenuContent = ({ item }: Props): JSX.Element => { + const { data: member } = hooks.useCurrentMember(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: MouseEvent): void => { + setAnchorEl(event.currentTarget); + }; + const closeMenu = (): void => { + setAnchorEl(null); + }; + const internalId = buildItemMenuId(item.id); + + const { + isOpen: isCopyModalOpen, + openModal: openCopyModal, + closeModal: closeCopyModal, + } = useModalStatus(); + const { + isOpen: isMoveModalOpen, + openModal: openMoveModal, + closeModal: closeMoveModal, + } = useModalStatus(); + const { + isOpen: isEditModalOpen, + openModal: openEditModal, + closeModal: closeEditModal, + } = useModalStatus(); + const { + isOpen: isCreateShortcutOpen, + openModal: openCreateShortcutModal, + closeModal: closeCreateShortcutModal, + } = useModalStatus(); + + const canWrite = + item.permission && + PermissionLevelCompare.gte(item.permission, PermissionLevel.Write); + const canAdmin = + item.permission && + PermissionLevelCompare.gte(item.permission, PermissionLevel.Admin); + + return ( + <> + + + + + + + + + {canWrite && ( + { + openEditModal(); + closeMenu(); + }} + key="edit" + item={item} + type={ActionButton.MENU_ITEM} + /> + )} + {member?.id && ( + <> + { + openCopyModal(); + closeMenu(); + }} + /> + + + )} + {canAdmin && ( + { + openMoveModal(); + closeMenu(); + }} + /> + )} + + + {canWrite && ( + <> + + + {item.type !== ItemType.FOLDER && ( + + )} + + )} + + + + {member?.id && ( + <> + { + openCreateShortcutModal(); + closeMenu(); + }} + /> + + + )} + {canWrite && ( + + )} + + {canAdmin ? ( + <> + + + + ) : ( + + )} + {member?.id && } + + + ); +}; + +export default ItemMenuContent; diff --git a/src/components/main/Items.tsx b/src/components/main/Items.tsx deleted file mode 100644 index 146dab769..000000000 --- a/src/components/main/Items.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { useNavigate } from 'react-router'; -import { useSearchParams } from 'react-router-dom'; - -import { DiscriminatedItem, PackedItem, redirect } from '@graasp/sdk'; - -import { buildGraaspPlayerView } from '@/config/externalPaths'; -import { buildItemPath } from '@/config/paths'; -import { buildPlayerTabName } from '@/config/selectors'; -import { ShowOnlyMeChangeType } from '@/config/types'; - -import { ItemLayoutMode } from '../../enums'; -import { useLayoutContext } from '../context/LayoutContext'; -import FileUploaderOverlay from '../file/FileUploaderOverlay'; -import MapView from '../item/MapView'; -import { useItemsStatuses } from '../table/BadgesCellRenderer'; -import ItemsGrid from './ItemsGrid'; -import ItemsTable from './ItemsTable'; - -type Props = { - id?: string; - items?: PackedItem[]; - title: string; - headerElements?: JSX.Element[]; - actions?: ({ data }: { data: DiscriminatedItem }) => JSX.Element; - ToolbarActions?: ({ selectedIds }: { selectedIds: string[] }) => JSX.Element; - clickable?: boolean; - defaultSortedColumn?: { - updatedAt?: 'desc' | 'asc'; - createdAt?: 'desc' | 'asc'; - type?: 'desc' | 'asc'; - name?: 'desc' | 'asc'; - }; - parentId?: string; - showThumbnails?: boolean; - canMove?: boolean; - canEdit?: boolean; - onShowOnlyMeChange?: ShowOnlyMeChangeType; - showOnlyMe?: boolean; - itemSearch?: { text: string }; - page?: number; - setPage?: (p: number) => void; - // how many items exist, which can be more than the displayed items - totalCount?: number; - onSortChanged?: (e: any) => void; - pageSize?: number; - showDropzoneHelper?: boolean; -}; - -const Items = ({ - id, - items, - title, - headerElements = [], - actions, - ToolbarActions, - clickable = true, - parentId, - defaultSortedColumn, - showThumbnails = true, - canMove = true, - canEdit = true, - showOnlyMe = false, - itemSearch, - page, - setPage, - onShowOnlyMeChange, - totalCount = 0, - onSortChanged, - pageSize, - showDropzoneHelper = false, -}: Props): JSX.Element | null => { - const { mode } = useLayoutContext(); - const [searchParams] = useSearchParams(); - const navigate = useNavigate(); - const itemsStatuses = useItemsStatuses({ - items, - }); - switch (mode) { - case ItemLayoutMode.Map: { - const viewItem = (item: DiscriminatedItem) => { - redirect(window, buildGraaspPlayerView(item.id), { - name: buildPlayerTabName(item.id), - openInNewTab: false, - }); - }; - const viewItemInBuilder = (item: DiscriminatedItem) => { - // navigate to item in map - navigate({ - pathname: buildItemPath(item.id), - search: searchParams.toString(), - }); - }; - - return ( - - ); - } - case ItemLayoutMode.Grid: - return ( - <> - {totalCount ? : undefined} - - - ); - case ItemLayoutMode.List: - default: - return ( - <> - {totalCount ? : undefined} - - - ); - } -}; - -export default Items; diff --git a/src/components/main/ItemsGrid.tsx b/src/components/main/ItemsGrid.tsx deleted file mode 100644 index 6c95d4edc..000000000 --- a/src/components/main/ItemsGrid.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { Box, Grid, Pagination } from '@mui/material'; - -import { PackedItem } from '@graasp/sdk'; - -import { ShowOnlyMeChangeType } from '@/config/types'; - -import { ITEM_PAGE_SIZE } from '../../config/constants'; -import { - DROPZONE_HELPER_ID, - ITEMS_GRID_PAGINATION_ID, -} from '../../config/selectors'; -import FileUploader from '../file/FileUploader'; -import { useUploadWithProgress } from '../hooks/uploadWithProgress'; -import FolderDescription from '../item/FolderDescription'; -import { NoItemSearchResult } from '../item/ItemSearch'; -import { ItemsStatuses } from '../table/BadgesCellRenderer'; -import EmptyItem from './EmptyItem'; -import ItemCard from './ItemCard'; -import ItemsToolbar from './ItemsToolbar'; -import NewItemButton from './NewItemButton'; - -type Props = { - id?: string; - items?: PackedItem[]; - itemsStatuses?: ItemsStatuses; - title: string; - itemSearch?: { - text: string; - }; - headerElements?: JSX.Element[]; - parentId?: string; - canMove?: boolean; - showOnlyMe?: boolean; - onShowOnlyMeChange?: ShowOnlyMeChangeType; - totalCount?: number; - onPageChange: any; - page?: number; - canEdit?: boolean; -}; - -const ItemsGrid = ({ - id: gridId = '', - items = [], - title, - itemSearch, - headerElements = [], - itemsStatuses, - parentId, - onShowOnlyMeChange, - canMove = true, - canEdit = true, - showOnlyMe, - totalCount = 0, - onPageChange, - page = 1, -}: Props): JSX.Element => { - const { - update, - close: closeNotification, - closeAndShowError, - show, - } = useUploadWithProgress(); - const pagesCount = Math.ceil(Math.max(1, totalCount / ITEM_PAGE_SIZE)); - const renderItems = () => { - if (!items?.length) { - // we need to show toast notifications since the websockets reset the view as soon as one file is uploaded - if (itemSearch?.text) { - return ( - - - - ); - } - if (canEdit) { - return ( - - } - /> - - ); - } - return ; - } - - return items.map((item) => ( - - - - )); - }; - - return ( -
- } - headerElements={headerElements} - onShowOnlyMeChange={onShowOnlyMeChange} - showOnlyMe={showOnlyMe} - /> - - - {renderItems()} - - - onPageChange(v)} - /> - -
- ); -}; - -export default ItemsGrid; diff --git a/src/components/main/ItemsTable.tsx b/src/components/main/ItemsTable.tsx index de6ce1011..5eb2e270e 100644 --- a/src/components/main/ItemsTable.tsx +++ b/src/components/main/ItemsTable.tsx @@ -1,340 +1,200 @@ -import { useCallback } from 'react'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; +import { useState } from 'react'; +import { Trans } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; -import { - DiscriminatedItem, - ItemType, - PermissionLevel, - PermissionLevelCompare, - formatDate, - getFolderExtra, - getShortcutExtra, -} from '@graasp/sdk'; +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 { COMMON } from '@graasp/translations'; -import { Table as GraaspTable } from '@graasp/ui/table'; +import { Button, DraggingWrapper } from '@graasp/ui'; import { - CellClickedEvent, - ColDef, - IRowDragItem, - SortChangedEvent, -} from '@ag-grid-community/core'; - -import { ShowOnlyMeChangeType } from '@/config/types'; - -import { ITEMS_TABLE_CONTAINER_HEIGHT } from '../../config/constants'; -import i18n, { useBuilderTranslation, useCommonTranslation, useEnumsTranslation, -} from '../../config/i18n'; -import { buildItemPath } from '../../config/paths'; +} from '@/config/i18n'; +import { BUILDER } from '@/langs/constants'; + import { hooks, mutations } from '../../config/queryClient'; -import { - DROPZONE_HELPER_ID, - buildItemsTableRowId, -} from '../../config/selectors'; -import { BUILDER } from '../../langs/constants'; -import FileUploader from '../file/FileUploader'; import { useUploadWithProgress } from '../hooks/uploadWithProgress'; -import FolderDescription from '../item/FolderDescription'; -import ActionsCellRenderer from '../table/ActionsCellRenderer'; -import BadgesCellRenderer, { ItemsStatuses } from '../table/BadgesCellRenderer'; -import NameCellRenderer from '../table/ItemNameCellRenderer'; -import MemberNameCellRenderer from '../table/MemberNameCellRenderer'; -import ItemsToolbar from './ItemsToolbar'; -import NewItemButton from './NewItemButton'; +import { useItemsStatuses } from '../table/Badges'; +import ItemsTableCard from './ItemsTableCard'; const { useItem } = hooks; export type ItemsTableProps = { id?: string; - items?: DiscriminatedItem[]; - itemsStatuses?: ItemsStatuses; - tableTitle: string; - headerElements?: JSX.Element[]; - isSearching?: boolean; - actions?: ({ data }: { data: DiscriminatedItem }) => JSX.Element; - ToolbarActions?: ({ selectedIds }: { selectedIds: string[] }) => JSX.Element; - clickable?: boolean; - defaultSortedColumn?: { - updatedAt?: 'desc' | 'asc' | null; - createdAt?: 'desc' | 'asc' | null; - type?: 'desc' | 'asc' | null; - name?: 'desc' | 'asc' | null; - }; + items?: PackedItem[]; showThumbnails?: boolean; canMove?: boolean; - onShowOnlyMeChange?: ShowOnlyMeChangeType; - showOnlyMe?: boolean; - page?: number; - setPage?: (p: number) => void; - totalCount?: number; - onSortChanged?: (e: SortChangedEvent) => void; - pageSize?: number; - showDropzoneHelper?: boolean; + enableMoveInBetween?: boolean; }; const ItemsTable = ({ - tableTitle, id: tableId = '', items: rows = [], - itemsStatuses, - headerElements = [], - isSearching = false, - actions, - ToolbarActions, - clickable = true, - defaultSortedColumn, showThumbnails = true, canMove = true, - showOnlyMe, - onShowOnlyMeChange, - page = 1, - setPage, - totalCount, - onSortChanged, - pageSize, - showDropzoneHelper = false, + enableMoveInBetween = true, }: ItemsTableProps): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); + const [open, setOpen] = useState(false); const { t: translateCommon } = useCommonTranslation(); + const { t: translateBuilder } = useBuilderTranslation(); const { t: translateEnums } = useEnumsTranslation(); - const [searchParams] = useSearchParams(); - const { - update, - close: closeNotification, - closeAndShowError, - show, - } = useUploadWithProgress(); - const navigate = useNavigate(); const { itemId } = useParams(); const { data: parentItem } = useItem(itemId); - const { mutate: editItem } = mutations.useEditItem(); - - const noStatusesToShow = - !itemsStatuses || - !Object.values(itemsStatuses) - .map((obj) => Object.values(obj).some((e) => e === true)) - .some((e) => e === true); - - const isFolder = useCallback(() => Boolean(itemId), [itemId]); - const canDrag = useCallback( - () => isFolder() && !isSearching, - [isFolder, isSearching], - ); - - const getRowNodeId = ({ data }: { data: DiscriminatedItem }) => - buildItemsTableRowId(data.id); + const { update, close } = useUploadWithProgress(); + const { mutateAsync: reorder } = mutations.useReorderItem(); + const [movingId, setMovingId] = useState(); + const { mutate: moveItems } = mutations.useMoveItems(); + const { mutateAsync: uploadItems } = mutations.useUploadFiles(); + const [moveData, setMoveData] = useState<{ + movedItem: PackedItem; + to: PackedItem; + }>(); + + const handleClose = () => { + setOpen(false); + }; - const onCellClicked = ({ - column, - data, - }: CellClickedEvent) => { - if (column.getColId() !== 'actions') { - let targetId = data?.id; + const itemsStatuses = useItemsStatuses({ + items: rows, + }); - // redirect to target if shortcut - if (data && data.type === ItemType.SHORTCUT) { - targetId = getShortcutExtra(data.extra)?.target; - } - navigate({ - pathname: buildItemPath(targetId), - search: searchParams.toString(), - }); + const onDropInRow = (movedItem: PackedItem | any, targetItem: PackedItem) => { + // prevent drop in non-folder item + if (targetItem.type !== ItemType.FOLDER) { + toast.error( + translateBuilder(BUILDER.MOVE_IN_NON_FOLDER_ERROR_MESSAGE, { + type: translateEnums(targetItem.type), + }), + ); + return; } - }; - const hasOrderChanged = (rowIds: string[]) => { - if (parentItem && parentItem.type === ItemType.FOLDER) { - const { childrenOrder = [] } = getFolderExtra(parentItem.extra) || {}; - return ( - rowIds.length !== childrenOrder.length || - !childrenOrder.every((id, i) => id === rowIds[i]) - ); + // upload files in item + if (movedItem.files) { + uploadItems({ + files: movedItem.files, + id: targetItem.id, + onUploadProgress: update, + }) + .then(() => { + close(); + }) + .catch((e) => { + close(e); + }); + } else if (movedItem.id !== targetItem.id) { + setOpen(true); + setMoveData({ movedItem, to: targetItem }); } - return true; }; - const onDragEnd = (displayRows: { data: DiscriminatedItem }[]) => { - if (!itemId) { - console.error('no item id defined'); - } else { - const rowIds = displayRows.map((r) => r.data.id); - if (canDrag() && hasOrderChanged(rowIds)) { - editItem({ - id: itemId, - extra: { - folder: { - childrenOrder: rowIds, - }, - }, + // warning: this won't work anymore with pagination! + const onDropBetweenRow = ( + { files, id }: PackedItem | any, + previousItem?: PackedItem, + ) => { + // upload files at row + if (files) { + uploadItems({ + files, + id: parentItem?.id, + previousItemId: previousItem?.id, + onUploadProgress: update, + }) + .then(() => { + close(); + }) + .catch((e) => { + close(e); }); - } + } else if (!itemId || !parentItem) { + console.error('cannot move in root'); + toast.error(BUILDER.ERROR_MESSAGE); + } else { + setMovingId(id); + reorder({ + id, + previousItemId: previousItem?.id, + parentItemId: parentItem.id, + }).finally(() => { + setMovingId(undefined); + }); } }; - const dateColumnFormatter = ({ value }: { value: string }) => - formatDate(value, { - locale: i18n.language, - defaultValue: translateCommon(COMMON.UNKNOWN_DATE), - }); - - const itemRowDragText = (params: IRowDragItem) => - params?.rowNode?.data?.name ?? - translateBuilder(BUILDER.ITEMS_TABLE_DRAG_DEFAULT_MESSAGE); - - const ActionComponent = ActionsCellRenderer({ - canMove, - }); - - const BadgesComponent = BadgesCellRenderer({ - itemsStatuses, - }); - - const columnDefs: ColDef[] = [ - { - field: 'name', - headerName: translateBuilder(BUILDER.ITEMS_TABLE_NAME_HEADER), - headerCheckboxSelection: true, - checkboxSelection: true, - cellRenderer: NameCellRenderer(showThumbnails), - flex: 4, - comparator: GraaspTable.textComparator, - sort: defaultSortedColumn?.name, - tooltipField: 'name', - }, - { - field: 'creator', - headerName: translateBuilder(BUILDER.ITEMS_TABLE_CREATOR_HEADER), - colId: 'creator', - type: 'rightAligned', - cellRenderer: MemberNameCellRenderer({ - defaultValue: translateCommon(COMMON.MEMBER_DEFAULT_NAME), - }), - cellStyle: { - display: 'flex', - justifyContent: 'end', - }, - sortable: false, - }, - { - field: 'status', - headerName: translateBuilder(BUILDER.ITEMS_TABLE_STATUS_HEADER), - cellRenderer: BadgesComponent, - hide: noStatusesToShow, - type: 'rightAligned', - flex: 1, - suppressAutoSize: true, - maxWidth: 100, - cellStyle: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - alignItems: 'center', - }, - }, - { - field: 'type', - headerName: translateBuilder(BUILDER.ITEMS_TABLE_TYPE_HEADER), - type: 'rightAligned', - cellRenderer: ({ data }: { data: DiscriminatedItem }) => - translateEnums(data.type), - minWidth: 90, - maxWidth: 120, - comparator: GraaspTable.textComparator, - sort: defaultSortedColumn?.type, - }, - { - field: 'updatedAt', - headerName: translateBuilder(BUILDER.ITEMS_TABLE_UPDATED_AT_HEADER), - maxWidth: 160, - minWidth: 80, - type: 'rightAligned', - valueFormatter: dateColumnFormatter, - comparator: GraaspTable.dateComparator, - sort: defaultSortedColumn?.updatedAt, - }, - { - field: 'actions', - cellRenderer: actions ?? ActionComponent, - suppressKeyboardEvent: GraaspTable.suppressKeyboardEventForParentCell, - headerName: translateBuilder(BUILDER.ITEMS_TABLE_ACTIONS_HEADER), - colId: 'actions', - type: 'rightAligned', - cellStyle: { - paddingLeft: '0!important', - paddingRight: '0!important', - textAlign: 'right', - }, - sortable: false, - suppressAutoSize: true, - // prevent ellipsis for small screens - minWidth: 140, - }, - ]; - - const countTextFunction = (selected: string[]) => - translateBuilder(BUILDER.ITEMS_TABLE_SELECTION_TEXT, { - count: selected.length, - }); - - const shouldShowDropzoneHelper = showDropzoneHelper && rows?.length === 0; - const canEditItem = parentItem?.permission - ? PermissionLevelCompare.gte(parentItem.permission, PermissionLevel.Write) - : false; + const handleMoveItems = () => { + if (moveData) { + moveItems({ items: [moveData.movedItem], to: moveData.to.id }); + setMoveData(undefined); + handleClose(); + } + }; return ( <> - : null} - headerElements={headerElements} - onShowOnlyMeChange={onShowOnlyMeChange} - showOnlyMe={showOnlyMe} + movingId !== item.id && canMove} + getRowId={(row) => row.id} + renderComponent={(droppedEl, y) => ( + + )} + rows={rows} + onDropBetweenRow={onDropBetweenRow} + enableMoveInBetween={enableMoveInBetween} + onDropInRow={onDropInRow} /> - {/* we need to show toast notifications since the websockets reset the view as soon as one file is uploaded */} - {shouldShowDropzoneHelper && (!parentItem || canEditItem) ? ( - } - /> - ) : ( - { - setPage?.(newPage + 1); - }} - countTextFunction={countTextFunction} - totalCount={totalCount} - // has to be fixed, otherwise the pagination is false on the last page - // rows can contain less for the last page - pageSize={pageSize ?? rows.length} - /> - )} + + {moveData ? ( + <> + + }} + /> + + + + {translateBuilder(BUILDER.MOVE_WARNING, { + name: moveData.movedItem.name, + })} + + + ) : ( + + )} + + + + + + ); }; diff --git a/src/components/main/ItemsTableCard.tsx b/src/components/main/ItemsTableCard.tsx new file mode 100644 index 000000000..3dd5c830c --- /dev/null +++ b/src/components/main/ItemsTableCard.tsx @@ -0,0 +1,83 @@ +import { Box, Stack } from '@mui/material'; + +import { PackedItem } from '@graasp/sdk'; +import type { DroppedFile } from '@graasp/ui'; + +import { Upload } from 'lucide-react'; + +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'; + +type Props = { + item: PackedItem | DroppedFile; + isDragging: boolean; + isOver: boolean; + isMovable: boolean; + showThumbnails: boolean; + itemsStatuses: ItemsStatuses; + enableMoveInBetween: boolean; +}; + +const ItemsTableCard = ({ + item, + isDragging, + isOver, + isMovable, + showThumbnails, + itemsStatuses, + enableMoveInBetween, +}: Props): JSX.Element => { + const { mode } = useLayoutContext(); + + const { t: translateBuilder } = useBuilderTranslation(); + + const dense = mode === ItemLayoutMode.List; + + if ('files' in item) { + return ( + + {translateBuilder(BUILDER.UPLOAD_BETWEEN_FILES)} + + ); + } + + return ( + + } + footer={ + + + + + } + /> + + ); +}; + +export default ItemsTableCard; diff --git a/src/components/main/ItemsToolbar.tsx b/src/components/main/ItemsToolbar.tsx deleted file mode 100644 index a4a5a4939..000000000 --- a/src/components/main/ItemsToolbar.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { FormControlLabel, Stack, Switch, Typography } from '@mui/material'; - -import { useBuilderTranslation } from '@/config/i18n'; -import { ACCESSIBLE_ITEMS_ONLY_ME_ID } from '@/config/selectors'; -import { ShowOnlyMeChangeType } from '@/config/types'; -import { BUILDER } from '@/langs/constants'; - -import SelectTypes from '../common/SelectTypes'; - -type Props = { - title: string; - subTitleElement?: JSX.Element | null; - headerElements?: JSX.Element[]; - onShowOnlyMeChange?: ShowOnlyMeChangeType; - showOnlyMe?: boolean; -}; - -const ItemsToolbar = ({ - title, - subTitleElement, - headerElements, - onShowOnlyMeChange, - showOnlyMe, -}: Props): JSX.Element => { - const { t } = useBuilderTranslation(); - return ( - <> - - - {title} - - - {headerElements} - - - {subTitleElement} - - {onShowOnlyMeChange && ( - onShowOnlyMeChange(checked)} - /> - } - label={t(BUILDER.HOME_SHOW_ONLY_CREATED_BY_ME)} - /> - )} - - - - ); -}; -export default ItemsToolbar; diff --git a/src/components/main/NewItemButton.tsx b/src/components/main/NewItemButton.tsx index 9753cfc4e..7db5556a6 100644 --- a/src/components/main/NewItemButton.tsx +++ b/src/components/main/NewItemButton.tsx @@ -1,8 +1,9 @@ import { useState } from 'react'; -import { Add } from '@mui/icons-material'; -import { ButtonProps } from '@mui/material'; +import { Add as AddIcon } from '@mui/icons-material'; +import { ButtonProps, IconButton, useTheme } from '@mui/material'; +import { DiscriminatedItem } from '@graasp/sdk'; import { Button } from '@graasp/ui'; import { useBuilderTranslation } from '../../config/i18n'; @@ -11,12 +12,19 @@ import { BUILDER } from '../../langs/constants'; import NewItemModal from './NewItemModal'; type Props = { + previousItemId?: DiscriminatedItem['id']; size?: ButtonProps['size']; + type?: 'button' | 'icon'; }; -const NewItemButton = ({ size }: Props): JSX.Element => { +const NewItemButton = ({ + previousItemId, + size = 'small', + type = 'button', +}: Props): JSX.Element => { const [open, setOpen] = useState(false); const { t: translateBuilder } = useBuilderTranslation(); + const theme = useTheme(); const handleClickOpen = () => { setOpen(true); @@ -28,11 +36,34 @@ const NewItemButton = ({ size }: Props): JSX.Element => { return ( <> - - + {type === 'icon' ? ( + + + + ) : ( + + )} + ); }; diff --git a/src/components/main/NewItemModal.tsx b/src/components/main/NewItemModal.tsx index eb9f16e29..98cddc0f5 100644 --- a/src/components/main/NewItemModal.tsx +++ b/src/components/main/NewItemModal.tsx @@ -33,7 +33,7 @@ import { InternalItemType, NewItemTabType } from '../../config/types'; import { BUILDER } from '../../langs/constants'; import { isItemValid } from '../../utils/item'; import CancelButton from '../common/CancelButton'; -import UploadFiles from '../file/UploadFiles'; +import FileUploader from '../file/FileUploader'; import AppForm from '../item/form/AppForm'; import DocumentForm from '../item/form/DocumentForm'; import useEtherpadForm from '../item/form/EtherpadForm'; @@ -62,6 +62,7 @@ type Props = { open: boolean; handleClose: () => void; geolocation?: Partial; + previousItemId?: DiscriminatedItem['id']; }; const DEFAULT_PROPERTIES: PropertiesPerType = { @@ -75,6 +76,7 @@ const NewItemModal = ({ open, handleClose, geolocation, + previousItemId, }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { t: translateCommon } = useCommonTranslation(); @@ -129,6 +131,7 @@ const NewItemModal = ({ postItem({ geolocation, parentId, + previousItemId, ...(updatedPropertiesPerType[type] as any), }), DOUBLE_CLICK_DELAY_MS, @@ -181,7 +184,10 @@ const NewItemModal = ({ {translateBuilder(BUILDER.UPLOAD_FILE_TITLE)} - + ); case InternalItemType.ZIP: @@ -199,7 +205,10 @@ const NewItemModal = ({ {translateBuilder(BUILDER.IMPORT_H5P_TITLE)} - + ); case ItemType.ETHERPAD: diff --git a/src/components/map/DesktopMap.tsx b/src/components/map/DesktopMap.tsx new file mode 100644 index 000000000..891b4796a --- /dev/null +++ b/src/components/map/DesktopMap.tsx @@ -0,0 +1,34 @@ +import { useNavigate } from 'react-router'; + +import { DiscriminatedItem } from '@graasp/sdk'; + +import { buildGraaspPlayerView } from '@/config/externalPaths'; +import { buildItemPath } from '@/config/paths'; + +import MapView from '../item/MapView'; + +type Props = { + parentId?: DiscriminatedItem['id']; +}; + +export const DesktopMap = ({ parentId }: Props): JSX.Element => { + const navigate = useNavigate(); + + const viewItem = (item: DiscriminatedItem) => { + navigate(buildGraaspPlayerView(item.id)); + }; + + const viewItemInBuilder = (item: DiscriminatedItem) => { + navigate(buildItemPath(item.id)); + }; + + // todo: improve height + return ( + + ); +}; diff --git a/src/components/map/useCurrentLocation.tsx b/src/components/map/useCurrentLocation.tsx new file mode 100644 index 000000000..009eaf38e --- /dev/null +++ b/src/components/map/useCurrentLocation.tsx @@ -0,0 +1,76 @@ +import { useEffect, useState } from 'react'; + +const options = { + enableHighAccuracy: true, + timeout: 5000, + maximumAge: 0, +}; + +type CurrentPosition = { + lat: number; + lng: number; +}; + +export const useCurrentLocation = ( + enableGeolocation = true, +): { + hasFetchedCurrentLocation: boolean; + currentPosition?: CurrentPosition; +} => { + const [hasFetchedCurrentLocation, setHasFetchedCurrentLocation] = + useState(false); + + const [currentPosition, setCurrentPosition] = useState(); + + const getCurrentPosition = () => { + const success = (pos: { + coords: { latitude: number; longitude: number }; + }) => { + const crd = pos.coords; + setCurrentPosition({ lat: crd.latitude, lng: crd.longitude }); + setHasFetchedCurrentLocation(true); + }; + + navigator.geolocation.getCurrentPosition( + success, + (err: { code: number; message: string }) => { + // eslint-disable-next-line no-console + console.warn(`ERROR(${err.code}): ${err.message}`); + setHasFetchedCurrentLocation(true); + }, + options, + ); + }; + + // get current location + useEffect(() => { + if (enableGeolocation) { + if (navigator.permissions) { + // check permissions + // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/permissions#examples + navigator.permissions + .query({ name: 'geolocation' }) + .then(({ state }) => { + if (state === 'denied') { + console.error('geolocation denied:', state); + setHasFetchedCurrentLocation(true); + } + // allows granted and prompt values (safari) + else { + getCurrentPosition(); + } + }) + .catch((e) => { + console.error('geolocation denied:', e); + setHasFetchedCurrentLocation(true); + }); + } else { + // navigator.permissions does not exist in safari + // still try to get position for webview's ios + getCurrentPosition(); + } + } + }, [enableGeolocation]); + + return { hasFetchedCurrentLocation, currentPosition }; +}; diff --git a/src/components/pages/BookmarkedItemsScreen.tsx b/src/components/pages/BookmarkedItemsScreen.tsx index b30876ae4..b5c639514 100644 --- a/src/components/pages/BookmarkedItemsScreen.tsx +++ b/src/components/pages/BookmarkedItemsScreen.tsx @@ -1,22 +1,28 @@ -import { Helmet } from 'react-helmet-async'; - -import { Box } from '@mui/material'; +import { Alert, Box, Stack } from '@mui/material'; import { Loader } from '@graasp/ui'; +import { Ordering } from '@/enums'; + import { useBuilderTranslation } from '../../config/i18n'; import { hooks } from '../../config/queryClient'; import { + BOOKMARKED_ITEMS_ERROR_ALERT_ID, BOOKMARKED_ITEMS_ID, - FAVORITE_ITEMS_ERROR_ALERT_ID, } from '../../config/selectors'; import { BUILDER } from '../../langs/constants'; import ErrorAlert from '../common/ErrorAlert'; +import SelectTypes from '../common/SelectTypes'; import { useFilterItemsContext } from '../context/FilterItemsContext'; -import ItemHeader from '../item/header/ItemHeader'; -import Items from '../main/Items'; +import { useItemSearch } from '../item/ItemSearch'; +import ModeButton from '../item/header/ModeButton'; +import ItemsTable from '../main/ItemsTable'; +import SortingSelect from '../table/SortingSelect'; +import { SortingOptions } from '../table/types'; +import { useSorting, useTranslatedSortingOptions } from '../table/useSorting'; +import PageWrapper from './PageWrapper'; -const BookmarkedItemsLoadableContent = (): JSX.Element | null => { +const BookmarkedItems = (): JSX.Element | null => { const { t: translateBuilder } = useBuilderTranslation(); const { data, @@ -24,38 +30,117 @@ const BookmarkedItemsLoadableContent = (): JSX.Element | null => { isError, } = hooks.useBookmarkedItems(); const { shouldDisplayItem } = useFilterItemsContext(); - // TODO: implement filter in the hooks directly ? - const filteredData = data?.filter((d) => shouldDisplayItem(d.item.type)); + const { input, text } = useItemSearch(); - if (filteredData) { - return ( - <> - - {translateBuilder(BUILDER.BOOKMARKED_ITEMS_TITLE)} - - - - d.item)} - /> + const { sortBy, setSortBy, ordering, setOrdering, sortFn } = + useSorting({ + sortBy: SortingOptions.ItemUpdatedAt, + ordering: Ordering.DESC, + }); + const options = useTranslatedSortingOptions(); + + const filteredData = data + ?.map((d) => d.item) + ?.filter( + (item) => shouldDisplayItem(item.type) && item.name.includes(text), + ); + + filteredData?.sort(sortFn); + + const renderContent = () => { + if (isError) { + return ( + + - + ); + } + + if (!data?.length) { + return ( + + {translateBuilder(BUILDER.BOOKMARKS_NO_ITEM)} + + ); + } + + if (filteredData) { + return ( + + + + + + {sortBy && setSortBy && ( + + )} + + + + + {filteredData.length ? ( + + ) : ( + + {translateBuilder(BUILDER.BOOKMARKS_NO_ITEM_SEARCH, { + search: text, + })} + + )} + + ); + } + + if (isItemsLoading) { + return ; + } + + return ( + + + ); - } - - if (isItemsLoading) { - return ; - } - if (isError) { - return ; - } - return null; -}; + }; -const BookmarkedItemsScreen = (): JSX.Element => ( - -); + return ( + + {input} + + } + > + {renderContent()} + + ); +}; -export default BookmarkedItemsScreen; +export default BookmarkedItems; diff --git a/src/components/pages/HomeScreen.tsx b/src/components/pages/HomeScreen.tsx deleted file mode 100644 index 25c4f8b9b..000000000 --- a/src/components/pages/HomeScreen.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useState } from 'react'; -import { Helmet } from 'react-helmet-async'; - -import { Box, LinearProgress } from '@mui/material'; - -import { Loader } from '@graasp/ui'; - -import { ITEM_PAGE_SIZE } from '@/config/constants'; -import { ShowOnlyMeChangeType } from '@/config/types'; - -import { useBuilderTranslation } from '../../config/i18n'; -import { hooks } from '../../config/queryClient'; -import { - ACCESSIBLE_ITEMS_TABLE_ID, - HOME_ERROR_ALERT_ID, -} from '../../config/selectors'; -import { BUILDER } from '../../langs/constants'; -import ErrorAlert from '../common/ErrorAlert'; -import { useCurrentUserContext } from '../context/CurrentUserContext'; -import { useFilterItemsContext } from '../context/FilterItemsContext'; -import { useItemSearch } from '../item/ItemSearch'; -import ItemHeader from '../item/header/ItemHeader'; -import ItemActions from '../main/ItemActions'; -import Items from '../main/Items'; -import { ItemsTableProps } from '../main/ItemsTable'; -import NewItemButton from '../main/NewItemButton'; - -type HomeItemSortableColumn = - | 'item.name' - | 'item.type' - | 'item.created_at' - | 'item.updated_at'; - -const HomeLoadableContent = (): JSX.Element => { - const { t: translateBuilder } = useBuilderTranslation(); - const { data: currentMember } = useCurrentUserContext(); - const { itemTypes } = useFilterItemsContext(); - const [showOnlyMe, setShowOnlyMe] = useState(false); - - const [page, setPage] = useState(1); - const [sortColumn, setSortColumn] = - useState('item.updated_at'); - const [ordering, setOrdering] = useState<'asc' | 'desc'>('desc'); - const itemSearch = useItemSearch({ onSearch: () => setPage(1) }); - const { - data: accessibleItems, - isLoading, - isFetching, - } = hooks.useAccessibleItems( - { - // todo: in the future this can be any member from creators - creatorId: showOnlyMe ? currentMember?.id : undefined, - name: itemSearch.text, - sortBy: sortColumn, - ordering, - types: itemTypes, - }, - // todo: adapt page size given the user window height - { page, pageSize: ITEM_PAGE_SIZE }, - ); - - const onShowOnlyMeChange: ShowOnlyMeChangeType = (checked) => { - setShowOnlyMe(checked); - setPage(1); - }; - - // todo: this should be a global function but this is not applicable to other tables - // since they don't use a pagination - // with a custom table we won't need this anymore - const onSortChanged: ItemsTableProps['onSortChanged'] = (e) => { - const sortedColumn = e.columnApi - .getColumnState() - .find(({ sort }) => Boolean(sort)); - - // todo: remove this code when table is custom - if (sortedColumn) { - const { colId, sort } = sortedColumn; - if (sort) { - setOrdering(sort); - } - - // we don't sort by creator because table definition is global - // we should wait till the table is refactored - - let prop = colId; - if (colId === 'createdAt') { - prop = 'created_at'; - } - if (colId === 'updatedAt') { - prop = 'updated_at'; - } - if (['name', 'type', 'created_at', 'updated_at'].includes(prop)) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - setSortColumn(`item.${prop}`); - } - } else { - setSortColumn('item.updated_at'); - setOrdering('desc'); - } - }; - - if (accessibleItems) { - return ( - <> - - {translateBuilder(BUILDER.MY_ITEMS_TITLE)} - - - - , - ]} - ToolbarActions={ItemActions} - onShowOnlyMeChange={onShowOnlyMeChange} - showOnlyMe={showOnlyMe} - page={page} - setPage={setPage} - totalCount={accessibleItems.totalCount} - onSortChanged={onSortChanged} - pageSize={ITEM_PAGE_SIZE} - showDropzoneHelper - /> - {isFetching && ( - - - - )} - - - ); - } - - if (isLoading) { - return ; - } - - return ; -}; - -const HomeScreen = (): JSX.Element => ; - -export default HomeScreen; diff --git a/src/components/pages/NoItemFilters.tsx b/src/components/pages/NoItemFilters.tsx new file mode 100644 index 000000000..2cc56ba14 --- /dev/null +++ b/src/components/pages/NoItemFilters.tsx @@ -0,0 +1,36 @@ +import { Box, Typography } from '@mui/material'; + +import { useFilterItemsContext } from '@/components/context/FilterItemsContext'; +import { useBuilderTranslation } from '@/config/i18n'; +import { BUILDER } from '@/langs/constants'; + +const NoItemFilters = ({ searchText }: { searchText: string }): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + const { itemTypes } = useFilterItemsContext(); + + return ( + + + {translateBuilder(BUILDER.ITEM_SEARCH_NOTHING_FOUND)} + + {searchText && ( + + + {translateBuilder(BUILDER.ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE)} + + : {searchText} + + )} + {itemTypes.length ? ( + + + {translateBuilder(BUILDER.ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE)}:{' '} + + {itemTypes.join(', ')} + + ) : null} + + ); +}; + +export default NoItemFilters; diff --git a/src/components/pages/PageWrapper.tsx b/src/components/pages/PageWrapper.tsx new file mode 100644 index 000000000..39d0e1bb4 --- /dev/null +++ b/src/components/pages/PageWrapper.tsx @@ -0,0 +1,34 @@ +import { Helmet } from 'react-helmet-async'; + +import { Container, Stack, Typography } from '@mui/material'; + +type Props = { + id?: string; + children: JSX.Element; + options?: JSX.Element; + title: string; +}; + +const PageWrapper = ({ + id, + children, + options, + title, +}: Props): JSX.Element | null => ( + <> + + {title} + + + + + {title} + + {options} + + {children} + + +); + +export default PageWrapper; diff --git a/src/components/pages/PublishedItemsScreen.tsx b/src/components/pages/PublishedItemsScreen.tsx index 674afb0e1..6438cc87d 100644 --- a/src/components/pages/PublishedItemsScreen.tsx +++ b/src/components/pages/PublishedItemsScreen.tsx @@ -1,9 +1,9 @@ -import { Helmet } from 'react-helmet-async'; - -import { Box } from '@mui/material'; +import { Alert, Box, Stack } from '@mui/material'; import { Loader } from '@graasp/ui'; +import { Ordering } from '@/enums'; + import { useBuilderTranslation } from '../../config/i18n'; import { hooks } from '../../config/queryClient'; import { @@ -12,12 +12,22 @@ import { } from '../../config/selectors'; import { BUILDER } from '../../langs/constants'; import ErrorAlert from '../common/ErrorAlert'; +import SelectTypes from '../common/SelectTypes'; import { useCurrentUserContext } from '../context/CurrentUserContext'; import { useFilterItemsContext } from '../context/FilterItemsContext'; -import ItemHeader from '../item/header/ItemHeader'; -import Items from '../main/Items'; +import { useItemSearch } from '../item/ItemSearch'; +import ModeButton from '../item/header/ModeButton'; +import ItemsTable from '../main/ItemsTable'; +import SortingSelect from '../table/SortingSelect'; +import { SortingOptions } from '../table/types'; +import { useSorting, useTranslatedSortingOptions } from '../table/useSorting'; +import PageWrapper from './PageWrapper'; -const PublishedItemsLoadableContent = (): JSX.Element | null => { +const PublishedItemsScreenContent = ({ + searchText, +}: { + searchText: string; +}) => { const { t: translateBuilder } = useBuilderTranslation(); const { data: member } = useCurrentUserContext(); const { @@ -25,40 +35,117 @@ const PublishedItemsLoadableContent = (): JSX.Element | null => { isLoading, isError, } = hooks.usePublishedItemsForMember(member?.id); + const options = useTranslatedSortingOptions(); const { shouldDisplayItem } = useFilterItemsContext(); - // TODO: implement filter in the hooks directly ? - const filteredData = publishedItems?.filter((d) => shouldDisplayItem(d.type)); + const { sortBy, setSortBy, ordering, setOrdering, sortFn } = + useSorting({ + sortBy: SortingOptions.ItemUpdatedAt, + ordering: Ordering.DESC, + }); + const filteredData = publishedItems?.filter( + (d) => shouldDisplayItem(d.type) && d.name.includes(searchText), + ); + filteredData?.sort(sortFn); + + if (isError) { + return ( + + + + ); + } + + if (!publishedItems?.length) { + return ( + + {translateBuilder(BUILDER.PUBLISHED_ITEMS_EMPTY)} + + ); + } if (filteredData) { return ( <> - - {translateBuilder(BUILDER.PUBLISHED_ITEMS_TITLE)} - - - - + + + + {sortBy && setSortBy && ( + + )} + + + + + {filteredData.length ? ( + - + ) : ( + + + {translateBuilder(BUILDER.PUBLISHED_ITEMS_NOT_FOUND_SEARCH, { + search: searchText, + })} + + + )} ); } - if (isLoading) { return ; } - if (isError) { - return ; - } - return null; + return ( + + ; + + ); }; -const PublishedItemsScreen = (): JSX.Element => ( - -); +const PublishedItemsScreen = (): JSX.Element | null => { + const { t: translateBuilder } = useBuilderTranslation(); + const { input, text } = useItemSearch(); + + return ( + + {input} + + } + > + + + ); +}; export default PublishedItemsScreen; diff --git a/src/components/pages/RecycledItemsScreen.tsx b/src/components/pages/RecycledItemsScreen.tsx index b94685591..83a8bee87 100644 --- a/src/components/pages/RecycledItemsScreen.tsx +++ b/src/components/pages/RecycledItemsScreen.tsx @@ -1,83 +1,113 @@ -import { Helmet } from 'react-helmet-async'; - -import { Box } from '@mui/material'; +import { Alert, Box, Stack, Typography } from '@mui/material'; import { Loader } from '@graasp/ui'; +import { Ordering } from '@/enums'; + import { useBuilderTranslation } from '../../config/i18n'; import { hooks } from '../../config/queryClient'; import { - ITEMS_TABLE_DELETE_SELECTED_ITEMS_ID, - ITEMS_TABLE_RESTORE_SELECTED_ITEMS_ID, RECYCLED_ITEMS_ERROR_ALERT_ID, - RECYCLED_ITEMS_ID, RECYCLED_ITEMS_ROOT_CONTAINER, } from '../../config/selectors'; import { BUILDER } from '../../langs/constants'; import DeleteButton from '../common/DeleteButton'; import ErrorAlert from '../common/ErrorAlert'; import RestoreButton from '../common/RestoreButton'; +import SelectTypes from '../common/SelectTypes'; import { useFilterItemsContext } from '../context/FilterItemsContext'; -import ItemHeader from '../item/header/ItemHeader'; -import Items from '../main/Items'; - -type RowActionsProps = { - data: { id: string }; -}; - -const RowActions = ({ data: item }: RowActionsProps): JSX.Element => ( - <> - - - -); +import { useItemSearch } from '../item/ItemSearch'; +import ModeButton from '../item/header/ModeButton'; +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'; -type ToolbarActionsProps = { - selectedIds: string[]; -}; - -const ToolbarActions = ({ selectedIds }: ToolbarActionsProps): JSX.Element => ( - <> - - - -); - -const RecycleBinLoadableContent = (): JSX.Element | null => { +const RecycledItemsScreenContent = ({ + searchText, +}: { + searchText: string; +}): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const { data: recycledItems, isLoading, isError } = hooks.useRecycledItems(); + const options = useTranslatedSortingOptions(); const { shouldDisplayItem } = useFilterItemsContext(); - // TODO: implement filter in the hooks directly ? - const filteredData = recycledItems?.filter((d) => shouldDisplayItem(d.type)); + const filteredData = recycledItems?.filter( + (d) => shouldDisplayItem(d.type) && d.name.includes(searchText), + ); + const { sortBy, setSortBy, ordering, setOrdering, sortFn } = + useSorting({ + sortBy: SortingOptions.ItemUpdatedAt, + ordering: Ordering.DESC, + }); + filteredData?.sort(sortFn); + + if (isError) { + return ( + + ; + + ); + } + if (!recycledItems?.length) { + return ( + {translateBuilder(BUILDER.TRASH_NO_ITEM)} + ); + } if (filteredData) { return ( - <> - - {translateBuilder(BUILDER.RECYCLE_BIN_TITLE)} - - - - - - + + + + + + {sortBy && setSortBy && ( + + )} + + + + + {filteredData.length ? ( + filteredData.map((item) => ( + + + + + } + /> + )) + ) : ( + + {translateBuilder(BUILDER.TRASH_NO_ITEM_SEARCH, { + search: searchText, + })} + + )} + ); } @@ -85,13 +115,35 @@ const RecycleBinLoadableContent = (): JSX.Element | null => { return ; } - if (isError) { - return ; - } - - return null; + return ( + + ; + + ); }; -const RecycledItemsScreen = (): JSX.Element => ; +const RecycledItemsScreen = (): JSX.Element | null => { + const { t: translateBuilder } = useBuilderTranslation(); + const itemSearch = useItemSearch(); + + return ( + + {itemSearch.input} + + } + > + + + ); +}; export default RecycledItemsScreen; diff --git a/src/components/pages/home/HomeScreen.tsx b/src/components/pages/home/HomeScreen.tsx new file mode 100644 index 000000000..520c6facc --- /dev/null +++ b/src/components/pages/home/HomeScreen.tsx @@ -0,0 +1,200 @@ +import { useState } from 'react'; + +import { Alert, Box, LinearProgress, Stack } from '@mui/material'; + +import { Button } from '@graasp/ui'; + +import { ITEM_PAGE_SIZE } from '@/config/constants'; +import { ShowOnlyMeChangeType } from '@/config/types'; +import { ItemLayoutMode, Ordering } from '@/enums'; + +import { + useBuilderTranslation, + useEnumsTranslation, +} from '../../../config/i18n'; +import { hooks } from '../../../config/queryClient'; +import { ACCESSIBLE_ITEMS_TABLE_ID } from '../../../config/selectors'; +import { BUILDER } from '../../../langs/constants'; +import SelectTypes from '../../common/SelectTypes'; +import { useCurrentUserContext } from '../../context/CurrentUserContext'; +import { useFilterItemsContext } from '../../context/FilterItemsContext'; +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 { DesktopMap } from '../../map/DesktopMap'; +import ShowOnlyMeButton from '../../table/ShowOnlyMeButton'; +import SortingSelect from '../../table/SortingSelect'; +import { SortingOptions } from '../../table/types'; +import { useSorting } from '../../table/useSorting'; +import NoItemFilters from '../NoItemFilters'; +import PageWrapper from '../PageWrapper'; +import HomeScreenLoading from './HomeScreenLoading'; + +const HomeScreenContent = ({ searchText }: { searchText: string }) => { + const { t: translateBuilder } = useBuilderTranslation(); + const { t: translateEnums } = useEnumsTranslation(); + const { data: currentMember } = useCurrentUserContext(); + const { itemTypes } = useFilterItemsContext(); + const [showOnlyMe, setShowOnlyMe] = useState(false); + + const { mode } = useLayoutContext(); + const { sortBy, setSortBy, ordering, setOrdering } = + useSorting({ + sortBy: SortingOptions.ItemUpdatedAt, + ordering: Ordering.DESC, + }); + const { data, fetchNextPage, isLoading, isFetching } = + hooks.useInfiniteAccessibleItems( + { + // todo: in the future this can be any member from creators + creatorId: showOnlyMe ? currentMember?.id : undefined, + name: searchText, + sortBy, + ordering, + types: itemTypes, + }, + // todo: adapt page size given the user window height + { pageSize: ITEM_PAGE_SIZE }, + ); + + const onShowOnlyMeChange: ShowOnlyMeChangeType = (checked) => { + setShowOnlyMe(checked); + }; + + if (mode === ItemLayoutMode.Map) { + return ( + <> + + + + + + ); + } + + if (data && data.pages.length) { + // default show upload zone + let content = ( + + } /> + + ); + if (data.pages[0].data.length) { + const totalFetchedItems = data + ? data.pages.map(({ data: d }) => d.length).reduce((a, b) => a + b, 0) + : 0; + content = ( + <> + i)} + enableMoveInBetween={false} + /> + {!isFetching && data.pages[0].totalCount > totalFetchedItems && ( + + + + )} + {!isFetching && data.pages[0].totalCount === totalFetchedItems && ( + // avoids button fullwidth + + + + )} + + ); + } else if (itemTypes.length || searchText) { + content = ; + } + + return ( + <> + + + + + + + + {sortBy && setSortBy && ( + + sortBy={sortBy} + setSortBy={setSortBy} + ordering={ordering} + setOrdering={setOrdering} + options={Object.values(SortingOptions).sort((t1, t2) => + translateEnums(t1).localeCompare(translateEnums(t2)), + )} + /> + )} + + + + + + {content} + {data && isFetching && ( + + + + )} + + + ); + } + + if (isLoading) { + return ; + } + + return ( + {translateBuilder(BUILDER.ERROR_MESSAGE)} + ); +}; + +const HomeScreen = (): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + const itemSearch = useItemSearch(); + + return ( + + {itemSearch.input} + + + } + > + + + ); +}; + +export default HomeScreen; diff --git a/src/components/pages/home/HomeScreenLoading.tsx b/src/components/pages/home/HomeScreenLoading.tsx new file mode 100644 index 000000000..b8eedb6a8 --- /dev/null +++ b/src/components/pages/home/HomeScreenLoading.tsx @@ -0,0 +1,19 @@ +import { Skeleton, Stack } from '@mui/material'; + +const HomeScreenLoading = (): JSX.Element => ( + <> + + + + + + + + + + + + +); + +export default HomeScreenLoading; diff --git a/src/components/pages/item/ItemLoginWrapper.tsx b/src/components/pages/item/ItemLoginWrapper.tsx index 47cd81f02..e0ec69502 100644 --- a/src/components/pages/item/ItemLoginWrapper.tsx +++ b/src/components/pages/item/ItemLoginWrapper.tsx @@ -6,7 +6,6 @@ import { hooks, mutations } from '@/config/queryClient'; import { ITEM_LOGIN_SIGN_IN_BUTTON_ID, ITEM_LOGIN_SIGN_IN_MEMBER_ID_ID, - ITEM_LOGIN_SIGN_IN_MODE_ID, ITEM_LOGIN_SIGN_IN_PASSWORD_ID, ITEM_LOGIN_SIGN_IN_USERNAME_ID, } from '@/config/selectors'; @@ -36,7 +35,6 @@ const ItemLoginWrapper = (WrappedComponent: () => JSX.Element): JSX.Element => { usernameInputId: ITEM_LOGIN_SIGN_IN_USERNAME_ID, signInButtonId: ITEM_LOGIN_SIGN_IN_BUTTON_ID, passwordInputId: ITEM_LOGIN_SIGN_IN_PASSWORD_ID, - modeSelectId: ITEM_LOGIN_SIGN_IN_MODE_ID, })(WrappedComponent); return ; }; diff --git a/src/components/table/ActionsCellRenderer.tsx b/src/components/table/ActionsCellRenderer.tsx deleted file mode 100644 index 3350b4112..000000000 --- a/src/components/table/ActionsCellRenderer.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { - PackedItem, - PermissionLevel, - PermissionLevelCompare, -} from '@graasp/sdk'; - -import EditButton from '../common/EditButton'; -import DownloadButton from '../main/DownloadButton'; -import ItemMenu from '../main/ItemMenu'; - -type Props = { - canMove?: boolean; -}; - -type ChildCompProps = { - data: PackedItem; -}; - -// items and memberships match by index -const ActionsCellRenderer = ({ - canMove, -}: Props): ((arg: ChildCompProps) => JSX.Element) => { - const ChildComponent = ({ data: item }: ChildCompProps) => { - const canWrite = item.permission - ? PermissionLevelCompare.gte(item.permission, PermissionLevel.Write) - : false; - const canAdmin = item.permission - ? PermissionLevelCompare.gte(item.permission, PermissionLevel.Admin) - : false; - - const renderAnyoneActions = () => ( - <> - - - - ); - - const renderEditorActions = () => { - if (canWrite) { - return ; - } - return null; - }; - - return ( - <> - {renderEditorActions()} - {renderAnyoneActions()} - - ); - }; - return ChildComponent; -}; - -export default ActionsCellRenderer; diff --git a/src/components/table/BadgesCellRenderer.tsx b/src/components/table/Badges.tsx similarity index 55% rename from src/components/table/BadgesCellRenderer.tsx rename to src/components/table/Badges.tsx index 5a4d9ed67..a33a43713 100644 --- a/src/components/table/BadgesCellRenderer.tsx +++ b/src/components/table/Badges.tsx @@ -27,12 +27,9 @@ const DEFAULT_ITEM_STATUSES: ItemStatuses = { export type ItemsStatuses = { [key: DiscriminatedItem['id']]: ItemStatuses }; -type Props = { - itemsStatuses?: ItemsStatuses; -}; - type ChildCompProps = { data: DiscriminatedItem; + itemsStatuses?: ItemsStatuses; }; export const useItemsStatuses = ({ @@ -67,39 +64,34 @@ export const useItemsStatuses = ({ }, {} as ItemsStatuses); }; -const BadgesCellRenderer = ({ - itemsStatuses, -}: Props): ((arg: ChildCompProps) => JSX.Element) => { - const ChildComponent = ({ data: item }: ChildCompProps) => { - const { t } = useBuilderTranslation(); - // this is useful because the item.id we are looking for may not be present and the itemStatuses will be undefined - const itemStatuses = itemsStatuses?.[item.id] || DEFAULT_ITEM_STATUSES; - const { - showChatbox, - isPinned, - isHidden, - isPublic, - isPublished, - isCollapsible, - } = itemStatuses; - return ( - - ); - }; - return ChildComponent; +const Badges = ({ itemsStatuses, data: item }: ChildCompProps): JSX.Element => { + const { t } = useBuilderTranslation(); + // this is useful because the item.id we are looking for may not be present and the itemStatuses will be undefined + const itemStatuses = itemsStatuses?.[item.id] || DEFAULT_ITEM_STATUSES; + const { + showChatbox, + isPinned, + isHidden, + isPublic, + isPublished, + isCollapsible, + } = itemStatuses; + return ( + + ); }; -export default BadgesCellRenderer; +export default Badges; diff --git a/src/components/table/ItemActions.tsx b/src/components/table/ItemActions.tsx new file mode 100644 index 000000000..13cba3180 --- /dev/null +++ b/src/components/table/ItemActions.tsx @@ -0,0 +1,20 @@ +import { Stack } from '@mui/material'; + +import { DiscriminatedItem } from '@graasp/sdk'; + +import BookmarkButton from '../common/BookmarkButton'; +import DownloadButton from '../main/DownloadButton'; + +type Props = { + data: DiscriminatedItem; +}; + +// items and memberships match by index +const ItemActions = ({ data: item }: Props): JSX.Element => ( + + + + +); + +export default ItemActions; diff --git a/src/components/table/ItemCard.tsx b/src/components/table/ItemCard.tsx new file mode 100644 index 000000000..09e82634c --- /dev/null +++ b/src/components/table/ItemCard.tsx @@ -0,0 +1,101 @@ +import { Typography } from '@mui/material'; +import Grid2 from '@mui/material/Unstable_Grid2/Grid2'; + +import { ItemType, PackedItem, ThumbnailSize, formatDate } from '@graasp/sdk'; +import { COMMON } from '@graasp/translations'; +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'; + +type Props = { + item: PackedItem; + dense?: boolean; + showThumbnail?: boolean; + footer: JSX.Element; + isOver?: boolean; + isDragging?: boolean; + disabled?: boolean; + menu?: JSX.Element; +}; + +const ItemCard = ({ + item, + footer, + dense = true, + isDragging = false, + isOver = false, + showThumbnail = true, + disabled, + menu, +}: Props): JSX.Element => { + const { t: translateCommon } = useCommonTranslation(); + const { data: thumbnailUrl } = hooks.useItemThumbnailUrl({ + id: showThumbnail ? item.id : undefined, + size: ThumbnailSize.Medium, + }); + + const dateColumnFormatter = (value: string) => + formatDate(value, { + locale: i18n.language, + defaultValue: translateCommon(COMMON.UNKNOWN_DATE), + }); + + const to = + item.type === ItemType.SHORTCUT + ? buildItemPath(item.extra.shortcut.target) + : buildItemPath(item.id); + + const content = ( + + {dense ? ( + <> + + {item.type} + + + + {dateColumnFormatter(item.updatedAt)} + + + + ) : ( + + + + + + )} + + ); + + return ( + + ); +}; +export default ItemCard; diff --git a/src/components/table/ItemNameCellRenderer.tsx b/src/components/table/ItemNameCellRenderer.tsx deleted file mode 100644 index d0b6d1312..000000000 --- a/src/components/table/ItemNameCellRenderer.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Box, Typography } from '@mui/material'; - -import { - DiscriminatedItem, - ItemType, - getLinkExtra, - getMimetype, -} from '@graasp/sdk'; -import { ItemIcon, Thumbnail } from '@graasp/ui'; - -import { hooks } from '../../config/queryClient'; -import { buildNameCellRendererId } from '../../config/selectors'; - -type ChildProps = { data: DiscriminatedItem }; - -const ItemNameCellRenderer = ( - showThumbnails: boolean, -): ((props: ChildProps) => JSX.Element) => { - const Component = ({ data: item }: ChildProps): JSX.Element => { - const linkExtra = - item.type === ItemType.LINK ? getLinkExtra(item.extra) : undefined; - - const alt = item.name; - const iconSrc = linkExtra?.icons?.[0]; - const thumbnailSrc = linkExtra?.thumbnails?.[0]; - const defaultValueComponent = ( - - ); - - const { data: thumbnailUrl, isLoading } = hooks.useItemThumbnailUrl({ - id: item.id, - }); - - return ( - - {showThumbnails && ( - - )} - - {item.name} - - - ); - }; - - return Component; -}; - -export default ItemNameCellRenderer; diff --git a/src/components/table/ItemThumbnail.tsx b/src/components/table/ItemThumbnail.tsx new file mode 100644 index 000000000..d02365d89 --- /dev/null +++ b/src/components/table/ItemThumbnail.tsx @@ -0,0 +1,47 @@ +import { + DiscriminatedItem, + ItemType, + getLinkExtra, + getMimetype, +} from '@graasp/sdk'; +import { ItemIcon, Thumbnail } from '@graasp/ui'; + +import { hooks } from '../../config/queryClient'; + +const ItemThumbnail = ({ + data: item, +}: { + data: DiscriminatedItem; +}): JSX.Element => { + const linkExtra = + item.type === ItemType.LINK ? getLinkExtra(item.extra) : undefined; + + const alt = item.name; + const iconSrc = linkExtra?.icons?.[0]; + const thumbnailSrc = linkExtra?.thumbnails?.[0]; + const defaultValueComponent = ( + + ); + + const { data: thumbnailUrl, isLoading } = hooks.useItemThumbnailUrl({ + id: item.id, + }); + + return ( + + ); +}; + +export default ItemThumbnail; diff --git a/src/components/table/ShowOnlyMeButton.tsx b/src/components/table/ShowOnlyMeButton.tsx new file mode 100644 index 000000000..21c78181b --- /dev/null +++ b/src/components/table/ShowOnlyMeButton.tsx @@ -0,0 +1,30 @@ +import { Chip } from '@mui/material'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { ACCESSIBLE_ITEMS_ONLY_ME_ID } from '@/config/selectors'; + +import { BUILDER } from '../../langs/constants'; + +const ShowOnlyMeButton = ({ + onClick, + enabled = false, +}: { + onClick?: (value: boolean) => void; + enabled?: boolean; +}): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + return ( + { + onClick?.(!enabled); + }} + variant={enabled ? 'filled' : 'outlined'} + sx={{ fontSize: '1rem', maxWidth: 'max-content' }} + id={ACCESSIBLE_ITEMS_ONLY_ME_ID} + label={translateBuilder(BUILDER.HOME_SHOW_ONLY_CREATED_BY_ME)} + /> + ); +}; + +export default ShowOnlyMeButton; diff --git a/src/components/table/SortingSelect.tsx b/src/components/table/SortingSelect.tsx new file mode 100644 index 000000000..8eea4b661 --- /dev/null +++ b/src/components/table/SortingSelect.tsx @@ -0,0 +1,106 @@ +import { Dispatch } from 'react'; + +import { + FormControl, + FormGroup, + IconButton, + InputLabel, + MenuItem, + OutlinedInput, + Select, + SelectChangeEvent, +} from '@mui/material'; + +import { ArrowDownNarrowWide, ArrowUpWideNarrow } from 'lucide-react'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { SORTING_SELECT_SELECTOR_TEST_ID } from '@/config/selectors'; +import { Ordering } from '@/enums'; +import { BUILDER } from '@/langs/constants'; + +import { AllSortingOptions, SortingOptions } from './types'; + +const ITEM_HEIGHT = 48; +const ITEM_PADDING_TOP = 8; +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + }, + }, +}; + +const LABEL_ID = 'sort-by-filter-label'; + +export type SortingSelectProps = { + sortBy: T; + setSortBy: Dispatch; + ordering: Ordering; + setOrdering: Dispatch; + options: T[]; +}; + +export const SortingSelect = ({ + sortBy, + setSortBy, + ordering, + setOrdering, + options, +}: SortingSelectProps): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + + const handleChange = (event: SelectChangeEvent) => { + const { + target: { value: v }, + } = event; + setSortBy(v as T); + }; + + const label = translateBuilder(BUILDER.SORT_BY_LABEL); + + const sortedOptions = options + .map((o) => [o, translateBuilder(o)]) + .sort((a, b) => (a[1] > b[1] ? 1 : -1)); + + return ( + + + {label} + + + { + setOrdering( + ordering === Ordering.ASC ? Ordering.DESC : Ordering.ASC, + ); + }} + > + {ordering === Ordering.ASC ? ( + + ) : ( + + )} + + + + ); +}; + +export default SortingSelect; diff --git a/src/components/table/types.ts b/src/components/table/types.ts new file mode 100644 index 000000000..929b13f65 --- /dev/null +++ b/src/components/table/types.ts @@ -0,0 +1,19 @@ +// corresponds to the value that should be sent in the request +export enum SortingOptions { + ItemName = 'item.name', + ItemType = 'item.type', + ItemCreator = 'item.creator.name', + ItemUpdatedAt = 'item.updated_at', +} + +// special sorting value for inside folders +// corresponds to the value that should be sent in the request +export enum SortingOptionsForFolder { + ItemName = 'item.name', + ItemType = 'item.type', + ItemCreator = 'item.creator.name', + ItemUpdatedAt = 'item.updated_at', + Order = 'item.order', +} + +export type AllSortingOptions = SortingOptions | SortingOptionsForFolder; diff --git a/src/components/table/useSorting.tsx b/src/components/table/useSorting.tsx new file mode 100644 index 000000000..cffb1e711 --- /dev/null +++ b/src/components/table/useSorting.tsx @@ -0,0 +1,74 @@ +import { Dispatch, useState } from 'react'; + +import { PackedItem } from '@graasp/sdk'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { Ordering } from '@/enums'; + +import { + AllSortingOptions, + SortingOptions, + SortingOptionsForFolder, +} from './types'; + +export const useSorting = ({ + sortBy: s, + ordering: o = Ordering.DESC, +}: { + sortBy?: T; + ordering: Ordering; +}): { + sortBy: T; + ordering: Ordering; + setSortBy: Dispatch; + setOrdering: Dispatch; + sortFn: (a: PackedItem, b: PackedItem) => number; +} => { + const [sortBy, setSortBy] = useState( + s ?? (SortingOptions.ItemUpdatedAt as T), + ); + const [ordering, setOrdering] = useState(o); + + const sortFn = (a: PackedItem, b: PackedItem) => { + const f = ordering === Ordering.ASC ? 1 : -1; + let value = 0; + switch (sortBy) { + case SortingOptions.ItemName: + value = a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1; + break; + case SortingOptions.ItemCreator: + if (!a.creator) { + value = -1; + } else if (!b.creator) { + value = 1; + } else { + value = + a.creator?.name?.toLowerCase() > b.creator?.name?.toLowerCase() + ? 1 + : -1; + } + break; + case SortingOptions.ItemType: + value = a.type > b.type ? 1 : -1; + break; + case SortingOptions.ItemUpdatedAt: + value = a.updatedAt > b.updatedAt ? 1 : -1; + break; + case SortingOptionsForFolder.Order: + default: + value = 0; + } + + return value * f; + }; + + return { sortBy, ordering, setSortBy, setOrdering, sortFn }; +}; + +export const useTranslatedSortingOptions = (): SortingOptions[] => { + const { t } = useBuilderTranslation(); + + return Object.values(SortingOptions).sort((t1, t2) => + t(t1).localeCompare(t(t2)), + ); +}; diff --git a/src/components/thumbnails/ThumbnailUploader.hook.tsx b/src/components/thumbnails/ThumbnailUploader.hook.tsx index 84dc4e76d..a4801bde4 100644 --- a/src/components/thumbnails/ThumbnailUploader.hook.tsx +++ b/src/components/thumbnails/ThumbnailUploader.hook.tsx @@ -40,11 +40,7 @@ export const useThumbnailUploader = ({ const { mutate: deleteThumbnail } = useDeleteItemThumbnail(); const { mutateAsync: uploadItemThumbnail } = mutations.useUploadItemThumbnail(); - const { - update, - close: closeNotification, - closeAndShowError, - } = useUploadWithProgress(); + const { update, close: closeNotification } = useUploadWithProgress(); const { id: itemId, settings } = item; const { @@ -103,7 +99,7 @@ export const useThumbnailUploader = ({ } catch (error) { console.error(error); setIsUploadingError(true); - closeAndShowError(error as Error); + closeNotification(error as Error); } finally { setIsThumbnailUploading(false); setUploadingProgress(0); diff --git a/src/config/selectors.ts b/src/config/selectors.ts index 6b87aaa23..108891101 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -1,6 +1,6 @@ // todo: this makes tests slow because it compiles ui everytime // import { Platform } from '@graasp/ui'; -import { DescriptionPlacementType } from '@graasp/sdk'; +import { DescriptionPlacementType, ShortLink } from '@graasp/sdk'; import { PublicationStatus } from '@/types/publication'; @@ -17,7 +17,6 @@ export const ITEM_FORM_NAME_INPUT_ID = 'newItemNameInput'; export const ITEM_FORM_DISPLAY_NAME_INPUT_ID = 'newItemDisplayNameInput'; export const ITEM_FORM_CONFIRM_BUTTON_ID = 'newItemConfirmButton'; export const ITEM_SCREEN_ERROR_ALERT_ID = 'itemScreenErrorAlert'; -export const buildItemLink = (id: string): string => `itemLink-${id}`; export const NAVIGATION_HOME_LINK_ID = 'navigationHomeLink'; export const buildNavigationLink = (id: string): string => `navigationLink-${id}`; @@ -38,7 +37,6 @@ export const buildItemRowArrowId = (id: string): string => export const TREE_MODAL_CONFIRM_BUTTON_ID = 'treeModalConfirmButton'; export const ITEMS_GRID_NO_ITEM_ID = 'itemsGridNoItem'; export const EDIT_ITEM_BUTTON_CLASS = 'editButton'; -export const BOOKMARKED_ITEM_BUTTON_CLASS = 'bookmarkedButton'; export const PIN_ITEM_BUTTON_CLASS = 'pinButton'; export const COLLAPSE_ITEM_BUTTON_CLASS = 'collapseButton'; export const HIDDEN_ITEM_BUTTON_CLASS = 'hideButton'; @@ -52,28 +50,8 @@ export const buildPermissionOptionId = (id: string): string => `permission-${id}`; export const SHARE_ITEM_SHARE_BUTTON_ID = 'shareItemModalShareButton'; -export const SHARED_ITEMS_ID = 'sharedItems'; export const PUBLISHED_ITEMS_ID = 'publishedItems'; export const BOOKMARKED_ITEMS_ID = 'bookmarkedItems'; -export const RECYCLED_ITEMS_ID = 'recycledItems'; -export const OWNED_ITEMS_ID = 'ownedItems'; -export const ITEMS_TABLE_BODY = 'itemsTableBody'; -export const ITEMS_TABLE_ROW = '.ag-row'; -export const buildItemsTableRowId = (id: string): string => - `itemsTableRow-${id}`; -export const buildItemsTableRowSelector = (id: string): string => - `[row-id="${buildItemsTableRowId(id)}"]`; -export const buildItemsTableRowIdAttribute = (id: string): string => - `.ag-center-cols-container [row-id="${buildItemsTableRowId(id)}"]`; -export const ITEMS_TABLE_EMPTY_ROW_ID = 'itemsTableEmptyRow'; -export const ITEMS_TABLE_DELETE_SELECTED_ITEMS_ID = - 'itemsTableDeleteSelectedItems'; -export const ITEMS_TABLE_RECYCLE_SELECTED_ITEMS_ID = - 'itemsTableDeleteSelectedItems'; -export const ITEMS_TABLE_COPY_SELECTED_ITEMS_ID = 'itemsTableCopySelectedItems'; -export const ITEMS_TABLE_MOVE_SELECTED_ITEMS_ID = 'itemsTableMoveSelectedItems'; -export const ITEMS_TABLE_ROW_CHECKBOX_CLASS = 'itemsTableRowCheckbox'; -export const UPLOADER_ID = 'uploader'; export const buildFileItemId = (id: string): string => `file-${id}`; export const ITEM_PANEL_ID = 'itemPanelMetadata'; export const ITEM_PANEL_NAME_ID = 'itemPanelName'; @@ -96,7 +74,7 @@ export const ITEM_LOGIN_SIGN_IN_MODE_ID = 'itemLoginSignInMode'; export const ITEM_MAIN_CLASS = 'itemMain'; export const HOME_ERROR_ALERT_ID = 'homeErrorAlert'; export const SHARED_ITEMS_ERROR_ALERT_ID = 'sharedItemsErrorAlert'; -export const FAVORITE_ITEMS_ERROR_ALERT_ID = 'bookmarkedItemsErrorAlert'; +export const BOOKMARKED_ITEMS_ERROR_ALERT_ID = 'bookmarkedItemsErrorAlert'; export const PUBLISHED_ITEMS_ERROR_ALERT_ID = 'publishedItemsErrorAlert'; export const RECYCLED_ITEMS_ERROR_ALERT_ID = 'recycledItemsErrorAlert'; export const ITEM_MENU_SHORTCUT_BUTTON_CLASS = 'itemMenuShortcutButton'; @@ -149,13 +127,10 @@ export const buildItemsGridPaginationButton = (page: number): string => export const buildItemsGridPaginationButtonSelected = (page: number): string => `${buildItemsGridPaginationButton(page)}.Mui-selected`; export const ITEM_HEADER_ID = 'itemHeader'; -export const ROW_DRAGGER_CLASS = `drag-cell-class-name`; export const buildShareButtonId = (id: string): string => `shareButton-${id}`; export const buildPublishButtonId = (id: string): string => `publishButton-${id}`; export const buildDeleteButtonId = (id: string): string => `deleteButton-${id}`; -export const buildItemMenuButtonId = (id: string): string => - `itemMenuButton-${id}`; export const buildPlayerButtonId = (id: string): string => `playerButton-${id}`; export const buildEditButtonId = (id: string): string => `editButton-${id}`; export const buildSettingsButtonId = (id: string): string => @@ -364,12 +339,14 @@ export const buildShortLinkEditBtnId = (alias: string): string => `shortLinkEditBtn-${alias}`; export const buildShortLinkShortenBtnId = ( itemId: string, - platform: string, + platform: ShortLink['platform'], ): string => `${SHORT_LINK_SHORTEN_START_ID}-${platform}-${itemId}`; -export const buildShortLinkPlatformTextId = (platform: string): string => - `shortLinkPlatformText-${platform}`; -export const buildShortLinkUrlTextId = (platform: string): string => - `shortLinkUrlText-${platform}`; +export const buildShortLinkPlatformTextId = ( + platform: ShortLink['platform'], +): string => `shortLinkPlatformText-${platform}`; +export const buildShortLinkUrlTextId = ( + platform: ShortLink['platform'], +): string => `shortLinkUrlText-${platform}`; export const ACCESSIBLE_ITEMS_ONLY_ME_ID = 'accessibleItemsOnlyMe'; export const ACCESSIBLE_ITEMS_TABLE_ID = 'accessibleItemsTable'; export const ACCESSIBLE_ITEMS_NEXT_PAGE_BUTTON_SELECTOR = `#${ACCESSIBLE_ITEMS_TABLE_ID} [data-testid="KeyboardArrowRightIcon"]`; @@ -386,7 +363,7 @@ export const buildDescriptionPlacementId = ( export const ITEM_THUMBNAIL_CONTAINER_ID = 'itemThumbnailContainer'; export const ITEM_THUMBNAIL_DELETE_BTN_ID = 'itemThumbnailDeleteBtn'; -export const DROPZONE_HELPER_ID = 'dropzoneHelper'; +export const DROPZONE_SELECTOR = '[role="dropzone"]'; export const buildMapViewId = (parentId?: string): string => `map-view-${parentId}`; @@ -426,3 +403,14 @@ export const IMAGE_THUMBNAIL_UPLOADER = 'imageThumbnailUploader'; export const REMOVE_THUMBNAIL_BUTTON = 'removeThumbnailButton'; export const MUI_CHIP_REMOVE_BTN = 'CancelIcon'; +export const HOME_LOAD_MORE_BUTTON_SELECTOR = '[role="feed"]'; +export const buildItemsGridMoreButtonSelector = (id: string): string => + `#${buildItemCard(id)} [data-testid="MoreVertIcon"]`; +export const buildItemMenuId = (id: string): string => `item-menu-id-${id}`; +export const SORTING_SELECT_SELECTOR_TEST_ID = 'sortingSelect'; +export const SORTING_SELECT_SELECTOR = `[data-testid="${SORTING_SELECT_SELECTOR_TEST_ID}"]`; +export const SORTING_ORDERING_SELECTOR_DESC = '.lucide-arrow-up-wide-narrow'; +export const SORTING_ORDERING_SELECTOR_ASC = '.lucide-arrow-down-narrow-wide'; +export const UNBOOKMARK_ICON_SELECTOR = '[data-testid="BookmarkIcon"]'; +export const BOOKMARK_ICON_SELECTOR = + '[data-testid="BookmarkBorderOutlinedIcon"]'; diff --git a/src/langs/ar.json b/src/langs/ar.json index e968aa70a..62b628867 100644 --- a/src/langs/ar.json +++ b/src/langs/ar.json @@ -151,7 +151,6 @@ "ITEMS_TABLE_ACTIONS_HEADER": "أجراءات", "ITEMS_TABLE_CREATOR_HEADER": "المُنشئ", "ITEMS_TABLE_DRAG_DEFAULT_MESSAGE": "نقل عنصر واحد", - "ITEMS_TABLE_EMPTY_MESSAGE": "لا توجد عناصر", "ITEMS_TABLE_NAME_HEADER": "الأُسم", "ITEMS_TABLE_STATUS_HEADER": "حالة", "ITEMS_TABLE_SELECTION_TEXT_one": "تم إختيار {{count}}", diff --git a/src/langs/constants.ts b/src/langs/constants.ts index fc0ac47b1..4a9c627f9 100644 --- a/src/langs/constants.ts +++ b/src/langs/constants.ts @@ -105,6 +105,10 @@ export const BUILDER = { LIBRARY_SETTINGS_BUTTON_TITLE: 'LIBRARY_SETTINGS_BUTTON_TITLE', RECYCLE_ITEM_BUTTON: 'RECYCLE_ITEM_BUTTON', + TRASH_NO_ITEM: 'TRASH_NO_ITEM', + TRASH_NO_ITEM_SEARCH: 'TRASH_NO_ITEM_SEARCH', + BOOKMARKS_NO_ITEM: 'BOOKMARKS_NO_ITEM', + BOOKMARKS_NO_ITEM_SEARCH: 'BOOKMARKS_NO_ITEM_SEARCH', RESTORE_ITEM_BUTTON: 'RESTORE_ITEM_BUTTON', SHARE_ITEM_BUTTON: 'SHARE_ITEM_BUTTON', ITEM_METADATA_CREATOR_TITLE: 'ITEM_METADATA_CREATOR_TITLE', @@ -474,6 +478,7 @@ export const BUILDER = { UPDATE_THUMBNAIL_AT_INFO_ALERT: 'UPDATE_THUMBNAIL_AT_INFO_ALERT', ITEM_ACTION_INFORMATION: 'ITEM_ACTION_INFORMATION', FILTER_BY_TYPES_LABEL: 'FILTER_BY_TYPES_LABEL', + SORT_BY_LABEL: 'SORT_BY_LABEL', ITEM_SETTINGS_GEOLOCATION_PLACEHOLDER: 'ITEM_SETTINGS_GEOLOCATION_PLACEHOLDER', ITEM_SETTINGS_GEOLOCATION_NO_ADDRESS: 'ITEM_SETTINGS_GEOLOCATION_NO_ADDRESS', @@ -549,4 +554,17 @@ export const BUILDER = { UPLOAD_H5P_BUTTON: 'UPLOAD_H5P_BUTTON', UPLOAD_NOTIFICATION_LOADING: 'UPLOAD_NOTIFICATION_LOADING', UPLOAD_NOTIFICATION_COMPLETE: 'UPLOAD_NOTIFICATION_COMPLETE', + + UPLOAD_BETWEEN_FILES: 'UPLOAD_BETWEEN_FILES', + MOVE_WARNING: 'MOVE_WARNING', + MOVE_IN_NON_FOLDER_ERROR_MESSAGE: 'MOVE_IN_NON_FOLDER_ERROR_MESSAGE', + MOVE_CONFIRM_TITLE: 'MOVE_CONFIRM_TITLE', + ITEM_SEARCH_NOTHING_FOUND: 'ITEM_SEARCH_NOTHING_FOUND', + ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE: + 'ITEM_SEARCH_NOTHING_FOUND_QUERY_TITLE', + ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE: + 'ITEM_SEARCH_NOTHING_FOUND_TYPES_TITLE', + HOME_SCREEN_LOAD_MORE_BUTTON: 'HOME_SCREEN_LOAD_MORE_BUTTON', + PUBLISHED_ITEMS_EMPTY: 'PUBLISHED_ITEMS_EMPTY', + PUBLISHED_ITEMS_NOT_FOUND_SEARCH: 'PUBLISHED_ITEMS_NOT_FOUND_SEARCH', }; diff --git a/src/langs/de.json b/src/langs/de.json index 28517ed51..12e59f0b9 100644 --- a/src/langs/de.json +++ b/src/langs/de.json @@ -169,7 +169,6 @@ "ITEMS_TABLE_ACTIONS_HEADER": "Aktionen", "ITEMS_TABLE_CREATOR_HEADER": "Urheber", "ITEMS_TABLE_DRAG_DEFAULT_MESSAGE": "Bewegen Sie ein Element", - "ITEMS_TABLE_EMPTY_MESSAGE": "Keine Elemente", "ITEMS_TABLE_NAME_HEADER": "Name", "ITEMS_TABLE_STATUS_HEADER": "Status", "ITEMS_TABLE_SELECTION_TEXT_one": "{{count}} ausgewählt", diff --git a/src/langs/en.json b/src/langs/en.json index 9c9d794bd..86792724d 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -87,7 +87,7 @@ "HIDE_ITEM_HIDE_TEXT": "Hide", "HIDE_ITEM_SHOW_TEXT": "Show", "HOME_TITLE": "Home", - "HOME_SHOW_ONLY_CREATED_BY_ME": "Show only created by me", + "HOME_SHOW_ONLY_CREATED_BY_ME": "Created by me", "IMPORT_H5P_INFORMATIONS": "You can upload H5P rich content by uploading exported .h5p files (e.g. from H5P.com, external Moodle services, etc).", "IMPORT_H5P_LIMITATIONS_TEXT": "You can upload up to one H5P of {{maxSize}} at a time.", "IMPORT_H5P_TITLE": "Import an H5P", @@ -170,7 +170,7 @@ "ITEMS_TABLE_ACTIONS_HEADER": "Actions", "ITEMS_TABLE_CREATOR_HEADER": "Creator", "ITEMS_TABLE_DRAG_DEFAULT_MESSAGE": "Move one item", - "ITEMS_TABLE_EMPTY_MESSAGE": "No items", + "ITEMS_TABLE_EMPTY_MESSAGE": "This folder does not contain any items", "ITEMS_TABLE_NAME_HEADER": "Name", "ITEMS_TABLE_STATUS_HEADER": "Status", "ITEMS_TABLE_SELECTION_TEXT_one": "{{count}} selected", @@ -447,5 +447,26 @@ "UPLOADING": "Uploading...", "IMPORT_ZIP_BUTTON": "Import a ZIP archive", "UPLOAD_NOTIFICATION_LOADING": "Uploading...", - "UPLOAD_NOTIFICATION_COMPLETE": "Upload complete!" + "UPLOAD_NOTIFICATION_COMPLETE": "Upload complete!", + "SORT_BY_LABEL": "Sort by", + "item.updated_at": "Updated At", + "item.creator.name": "Creator", + "item.type": "Type", + "item.name": "Name", + "order": "Order", + "TRASH_NO_ITEM": "You trash is empty.", + "TRASH_NO_ITEM_SEARCH": "No trashed item for {{search}}", + "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_IN_NON_FOLDER_ERROR_MESSAGE": "Cannot add items in {{type}}", + "MOVE_CONFIRM_TITLE": "Confirm moving <1>{{name}} 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", + "HOME_SCREEN_LOAD_MORE_BUTTON": "Load More", + "PUBLISHED_ITEMS_EMPTY": "You didn't publish any items.", + "PUBLISHED_ITEMS_NOT_FOUND_SEARCH": "No published item found for {{search}}", + "item.order": "Order" } diff --git a/src/langs/es.json b/src/langs/es.json index 103eac2a9..a65da5f82 100644 --- a/src/langs/es.json +++ b/src/langs/es.json @@ -151,7 +151,6 @@ "ITEMS_TABLE_ACTIONS_HEADER": "Comportamiento", "ITEMS_TABLE_CREATOR_HEADER": "Creador", "ITEMS_TABLE_DRAG_DEFAULT_MESSAGE": "Mover un elemento", - "ITEMS_TABLE_EMPTY_MESSAGE": "No hay artículos", "ITEMS_TABLE_NAME_HEADER": "Nombre", "ITEMS_TABLE_STATUS_HEADER": "Estado", "ITEMS_TABLE_SELECTION_TEXT_one": "{{count}} seleccionado", diff --git a/src/langs/fr.json b/src/langs/fr.json index d0ccce1f0..62647ceaf 100644 --- a/src/langs/fr.json +++ b/src/langs/fr.json @@ -156,7 +156,7 @@ "ITEMS_TABLE_ACTIONS_HEADER": "Actions", "ITEMS_TABLE_CREATOR_HEADER": "Créateur", "ITEMS_TABLE_DRAG_DEFAULT_MESSAGE": "Déplacer un élément", - "ITEMS_TABLE_EMPTY_MESSAGE": "Aucun élément", + "ITEMS_TABLE_EMPTY_MESSAGE": "Ce dossier ne contient aucun élément", "ITEMS_TABLE_NAME_HEADER": "Nom", "ITEMS_TABLE_STATUS_HEADER": "Statut", "ITEMS_TABLE_SELECTION_TEXT_one": "{{count}} sélectionné", @@ -381,5 +381,10 @@ "ITEM_GEOLOCATION_ADVANCED_MODAL_SECONDARY_ADDRESS_LABEL": "Informations complémentaires", "ITEM_GEOLOCATION_ADVANCED_MODAL_SECONDARY_ADDRESS_PLACEHOLDER": "porte rouge sur la droite, ...", "ITEM_GEOLOCATION_ADVANCED_MODAL_COUNTRY_LABEL": "Pays (ex: CH, FR...)", - "ITEM_GEOLOCATION_ADVANCED_MODAL_ERROR": "La latitude et longitude doivent être définies" + "ITEM_GEOLOCATION_ADVANCED_MODAL_ERROR": "La latitude et longitude doivent être définies", + "item.updated_at": "Date d'édition", + "item.creator.name": "Créateur", + "item.type": "Type", + "item.name": "Nom", + "item.order": "Ordre" } diff --git a/src/langs/it.json b/src/langs/it.json index 634406aa4..713345627 100644 --- a/src/langs/it.json +++ b/src/langs/it.json @@ -151,7 +151,6 @@ "ITEMS_TABLE_ACTIONS_HEADER": "Azioni", "ITEMS_TABLE_CREATOR_HEADER": "Creatore", "ITEMS_TABLE_DRAG_DEFAULT_MESSAGE": "Sposta un elemento", - "ITEMS_TABLE_EMPTY_MESSAGE": "Nessun oggetto", "ITEMS_TABLE_NAME_HEADER": "Nome", "ITEMS_TABLE_STATUS_HEADER": "Stato", "ITEMS_TABLE_SELECTION_TEXT_one": "{{count}} selezionato", diff --git a/yarn.lock b/yarn.lock index 9f135887a..aea11d503 100644 --- a/yarn.lock +++ b/yarn.lock @@ -51,13 +51,20 @@ __metadata: languageName: node linkType: hard -"@ag-grid-community/styles@npm:31.3.2, @ag-grid-community/styles@npm:^31.3.1, @ag-grid-community/styles@npm:^31.3.2": +"@ag-grid-community/styles@npm:31.3.2, @ag-grid-community/styles@npm:^31.3.2": version: 31.3.2 resolution: "@ag-grid-community/styles@npm:31.3.2" checksum: 10/b651750803577419e6bbbc45b64d717d64c500332364f4523f84d9d384351f69a7d8eb83bd8343a4ec1530b0db0f26288e91e69516c6ade6e5f7dcf9a7e3078c languageName: node linkType: hard +"@ag-grid-community/styles@npm:^31.3.1": + version: 31.3.1 + resolution: "@ag-grid-community/styles@npm:31.3.1" + checksum: 10/34c4e80f62891fe1d8881e233df38ace6ba03eb2934b3015bb8537937164dc2c261479d8d15d5c06e36f2bb84c3b27c7b65aaab773236190729bdfe32ec2c225 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -1522,9 +1529,9 @@ __metadata: languageName: node linkType: hard -"@graasp/query-client@npm:3.14.0": - version: 3.14.0 - resolution: "@graasp/query-client@npm:3.14.0" +"@graasp/query-client@npm:3.15.2": + version: 3.15.2 + resolution: "@graasp/query-client@npm:3.15.2" dependencies: "@tanstack/react-query": "npm:4.36.1" "@tanstack/react-query-devtools": "npm:4.36.1" @@ -1535,22 +1542,22 @@ __metadata: "@graasp/sdk": ^4.0.0 "@graasp/translations": "*" react: ^18.0.0 - checksum: 10/2d47d8de6092572ccc525e54b6d78a7d34095e1da6e44de60a126e13c48b604040e24a372f858583f1cc30a0387992474d3dbb917bead4423cd14df98b26e556 + checksum: 10/137cbbdc721b244ee85b3e3a658b0aef3ecfd816eddafaaa8408783d5a5d97680303b2bde61d1b4de9939562b556c6224a18f0d1891c8f27cd44d7983a923c2e languageName: node linkType: hard -"@graasp/sdk@npm:4.15.1": - version: 4.15.1 - resolution: "@graasp/sdk@npm:4.15.1" +"@graasp/sdk@npm:4.17.0": + version: 4.17.0 + resolution: "@graasp/sdk@npm:4.17.0" dependencies: "@faker-js/faker": "npm:8.4.1" - filesize: "npm:10.1.2" + filesize: "npm:10.1.4" js-cookie: "npm:3.0.5" validator: "npm:13.12.0" peerDependencies: date-fns: ^3 uuid: ^9 || ^10.0.0 - checksum: 10/17722029c75b0d43d0cbcbef24f7947434980ad26792c2fbb14ac3dc5b1a4a7dfc791b3d273c7a03b0650dbad7074598ac2bc04d307ea43d5d66bacaf3bc6edd + checksum: 10/5fa444fceb20494898956f921ee8a029b9cca2184c4ebf8a2a65fb581b0252ff76e59f0ee2d5646c53dab29240bc9862d2ed9ac7c3e7568fd2f44cda953f2a61 languageName: node linkType: hard @@ -1628,25 +1635,24 @@ __metadata: languageName: node linkType: hard -"@graasp/ui@npm:4.20.2": - version: 4.20.2 - resolution: "@graasp/ui@npm:4.20.2" +"@graasp/ui@npm:4.21.0": + version: 4.21.0 + resolution: "@graasp/ui@npm:4.21.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.10" + "@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.399.0" + lucide-react: "npm:0.402.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: @@ -1655,9 +1661,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 @@ -1665,7 +1671,7 @@ __metadata: react-router-dom: ^6.11.0 stylis: ^4.1.3 stylis-plugin-rtl: ^2.1.1 - checksum: 10/0ed092f9b0a90c832ee90bb9caf4b074f84defe6cd03314127943ccfad229cc3f06f3ce6489eb4e287d57c4e72dc0524e8822e264f49f07afd9764432e3d1a6d + checksum: 10/6a8778ae27ebb534204a44e1f97b08fdba6d7524fe3417fd7eaab8f4cf66e19800a38812f0b10c8796c885d6cabc5823bed137aebfbf36f29308ac7f04525314 languageName: node linkType: hard @@ -1850,9 +1856,9 @@ __metadata: linkType: hard "@mui/core-downloads-tracker@npm:^5.16.0": - version: 5.16.0 - resolution: "@mui/core-downloads-tracker@npm:5.16.0" - checksum: 10/cf625680553eb918f033a6dbeb78078e4a81d0939a3db9a97a7daaacd1449c894b05dab53864d03e8831a98cc32af6780c7c9db82829a2a3967c05b1ee33f222 + version: 5.16.1 + resolution: "@mui/core-downloads-tracker@npm:5.16.1" + checksum: 10/13321ec64a9c6387b12b3cc137a0e75b4eb2259ae312a527c03aee0ec1992351454df7de44f221463b61b706e0d0714f0ccf2f16bce4becac47d18f720704141 languageName: node linkType: hard @@ -2017,12 +2023,12 @@ __metadata: languageName: node linkType: hard -"@mui/private-theming@npm:^5.16.0": - version: 5.16.0 - resolution: "@mui/private-theming@npm:5.16.0" +"@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.0" + "@mui/utils": "npm:^5.16.1" prop-types: "npm:^15.8.1" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -2030,7 +2036,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/bb3321fff647d980ae14d02d55d6f5650578f3aff4a154630fb8a4bb23305445e7786d06ec762f7ad1383249f2cd364e0ed17f04cc84e74044ef8488476e2842 + checksum: 10/fdfbc5e55bc1c980a00faab952e37280f437826788d8b1908d3eed75d053a4848b6927ea650a976b23419997113509e7ae0f21f325df6ba3a394ccdb32aa4f84 languageName: node linkType: hard @@ -2055,6 +2061,27 @@ __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/system@npm:^5.15.15": version: 5.15.15 resolution: "@mui/system@npm:5.15.15" @@ -2112,14 +2139,14 @@ __metadata: linkType: hard "@mui/system@npm:^5.16.0": - version: 5.16.0 - resolution: "@mui/system@npm:5.16.0" + version: 5.16.1 + resolution: "@mui/system@npm:5.16.1" dependencies: "@babel/runtime": "npm:^7.23.9" - "@mui/private-theming": "npm:^5.16.0" - "@mui/styled-engine": "npm:^5.15.14" - "@mui/types": "npm:^7.2.14" - "@mui/utils": "npm:^5.16.0" + "@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" @@ -2135,7 +2162,7 @@ __metadata: optional: true "@types/react": optional: true - checksum: 10/dc047d1b54bd33acc4827ce86dcee18d4ee255e9a3843eeb524eeedcf0543171051d7b24e4a2a7b35067c1b05e72be7dfe501d1e299c88988cb503240ac495ed + checksum: 10/e49852b203b61bf4166ba4f1c1e56c846ea204219a6c9e725cbadcad264bff52cec5a10a0cd6516e1772958022b0d204bd8c7674e29d63ea212f5aef7e2814c5 languageName: node linkType: hard @@ -2151,25 +2178,19 @@ __metadata: languageName: node linkType: hard -"@mui/utils@npm:^5.15.14": - version: 5.15.14 - resolution: "@mui/utils@npm:5.15.14" - dependencies: - "@babel/runtime": "npm:^7.23.9" - "@types/prop-types": "npm:^15.7.11" - prop-types: "npm:^15.8.1" - react-is: "npm:^18.2.0" +"@mui/types@npm:^7.2.15": + version: 7.2.15 + resolution: "@mui/types@npm:7.2.15" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 - react: ^17.0.0 || ^18.0.0 peerDependenciesMeta: "@types/react": optional: true - checksum: 10/b3cbe2d0aa7ec65969752dababc39fc6e0b8bb1a9cf8b9bac42ca40e3dd3eaa59b79765bd259019318acc7421d64b9f421bc67e776a581d7c9da6a1c0c50bfbc + checksum: 10/235b4af48a76cbe121e4cf7c4c71c7f9e4eaa458eaff5df2ac8a8f2d4ae93eafa929aba7848a2dfbb3c97dd8d50f4e13828dc17ec136b777bcfdd7d654263996 languageName: node linkType: hard -"@mui/utils@npm:^5.15.20": +"@mui/utils@npm:^5.15.14, @mui/utils@npm:^5.15.20": version: 5.15.20 resolution: "@mui/utils@npm:5.15.20" dependencies: @@ -2187,21 +2208,21 @@ __metadata: languageName: node linkType: hard -"@mui/utils@npm:^5.16.0": - version: 5.16.0 - resolution: "@mui/utils@npm:5.16.0" +"@mui/utils@npm:^5.16.0, @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.11" + "@types/prop-types": "npm:^15.7.12" prop-types: "npm:^15.8.1" - react-is: "npm:^18.2.0" + 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/c3ef877285ac726c0c704315996f553a785406fb65476c88f5243aed9104e04cc5c6d0eb3a0d42abd54c51cadd634833115fdadc02b51c3396c80848eb943bf1 + checksum: 10/d3294dfc9953b8f1697c4837bf57a81a97b26fdfc6dd4d28747b3126a4ae8d9f4160e03326d42fbb2e1885ea4d9c56301516e13c8b50d81ea4db2455d9f18f3b languageName: node linkType: hard @@ -2601,6 +2622,43 @@ __metadata: languageName: node linkType: hard +"@storybook/builder-vite@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/builder-vite@npm:8.1.11" + dependencies: + "@storybook/channels": "npm:8.1.11" + "@storybook/client-logger": "npm:8.1.11" + "@storybook/core-common": "npm:8.1.11" + "@storybook/core-events": "npm:8.1.11" + "@storybook/csf-plugin": "npm:8.1.11" + "@storybook/node-logger": "npm:8.1.11" + "@storybook/preview": "npm:8.1.11" + "@storybook/preview-api": "npm:8.1.11" + "@storybook/types": "npm:8.1.11" + "@types/find-cache-dir": "npm:^3.2.1" + browser-assert: "npm:^1.2.1" + es-module-lexer: "npm:^1.5.0" + express: "npm:^4.17.3" + find-cache-dir: "npm:^3.0.0" + fs-extra: "npm:^11.1.0" + magic-string: "npm:^0.30.0" + ts-dedent: "npm:^2.0.0" + peerDependencies: + "@preact/preset-vite": "*" + typescript: ">= 4.3.x" + vite: ^4.0.0 || ^5.0.0 + vite-plugin-glimmerx: "*" + peerDependenciesMeta: + "@preact/preset-vite": + optional: true + typescript: + optional: true + vite-plugin-glimmerx: + optional: true + checksum: 10/6e49345ed60b1cdc83b48f931ed673dfeecda48cd1ba6f19f9126b7a35106ae50918fb3105b75226029d3ea40bea682820cb54158c20f73c85d9097c0754b07b + languageName: node + linkType: hard + "@storybook/channels@npm:8.1.10": version: 8.1.10 resolution: "@storybook/channels@npm:8.1.10" @@ -2614,6 +2672,19 @@ __metadata: languageName: node linkType: hard +"@storybook/channels@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/channels@npm:8.1.11" + dependencies: + "@storybook/client-logger": "npm:8.1.11" + "@storybook/core-events": "npm:8.1.11" + "@storybook/global": "npm:^5.0.0" + telejson: "npm:^7.2.0" + tiny-invariant: "npm:^1.3.1" + checksum: 10/7ca5c0c418d76ca151369b11d3bce0f514f1ca63f6bc66b94ebcbbaa908b4859358020de4aff67a9dec9a43aa0b7c57f9acec67189ec61df8f617403b63023ce + languageName: node + linkType: hard + "@storybook/client-logger@npm:8.1.10": version: 8.1.10 resolution: "@storybook/client-logger@npm:8.1.10" @@ -2623,6 +2694,15 @@ __metadata: languageName: node linkType: hard +"@storybook/client-logger@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/client-logger@npm:8.1.11" + dependencies: + "@storybook/global": "npm:^5.0.0" + checksum: 10/4d2b652133a7aa51387d4667e428db084b81eb67d215cb401a7bdd7e7021a8e698c8b4a203a3ca17d0bef0a8edaeac09bdd37ba770fd87a150d038435835e47a + languageName: node + linkType: hard + "@storybook/core-common@npm:8.1.10": version: 8.1.10 resolution: "@storybook/core-common@npm:8.1.10" @@ -2665,6 +2745,48 @@ __metadata: languageName: node linkType: hard +"@storybook/core-common@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/core-common@npm:8.1.11" + dependencies: + "@storybook/core-events": "npm:8.1.11" + "@storybook/csf-tools": "npm:8.1.11" + "@storybook/node-logger": "npm:8.1.11" + "@storybook/types": "npm:8.1.11" + "@yarnpkg/fslib": "npm:2.10.3" + "@yarnpkg/libzip": "npm:2.3.0" + chalk: "npm:^4.1.0" + cross-spawn: "npm:^7.0.3" + esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0" + esbuild-register: "npm:^3.5.0" + execa: "npm:^5.0.0" + file-system-cache: "npm:2.3.0" + find-cache-dir: "npm:^3.0.0" + find-up: "npm:^5.0.0" + fs-extra: "npm:^11.1.0" + glob: "npm:^10.0.0" + handlebars: "npm:^4.7.7" + lazy-universal-dotenv: "npm:^4.0.0" + node-fetch: "npm:^2.0.0" + picomatch: "npm:^2.3.0" + pkg-dir: "npm:^5.0.0" + prettier-fallback: "npm:prettier@^3" + pretty-hrtime: "npm:^1.0.3" + resolve-from: "npm:^5.0.0" + semver: "npm:^7.3.7" + tempy: "npm:^3.1.0" + tiny-invariant: "npm:^1.3.1" + ts-dedent: "npm:^2.0.0" + util: "npm:^0.12.4" + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + checksum: 10/f76cfba89418bc9c494bbd1f57cca308b1b1596c7a2f2a6452668e8db3b91e702c3a9d93a66b3b1c50271a7181b4fe0f19d0d77e7da327663a4daf8be7c10c38 + languageName: node + linkType: hard + "@storybook/core-events@npm:8.1.10": version: 8.1.10 resolution: "@storybook/core-events@npm:8.1.10" @@ -2675,6 +2797,16 @@ __metadata: languageName: node linkType: hard +"@storybook/core-events@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/core-events@npm:8.1.11" + dependencies: + "@storybook/csf": "npm:^0.1.7" + ts-dedent: "npm:^2.0.0" + checksum: 10/8628593b9604b189ef295532a1c2210a8e6fc5d8d7b9a6ff79553296985e77abc20e83a000a35c401bbfc39cc4734fdf346637d97508d6b6759603651ac29e8f + languageName: node + linkType: hard + "@storybook/csf-plugin@npm:8.1.10": version: 8.1.10 resolution: "@storybook/csf-plugin@npm:8.1.10" @@ -2685,6 +2817,16 @@ __metadata: languageName: node linkType: hard +"@storybook/csf-plugin@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/csf-plugin@npm:8.1.11" + dependencies: + "@storybook/csf-tools": "npm:8.1.11" + unplugin: "npm:^1.3.1" + checksum: 10/cb41f58b57f453ad3d896a98789852e0b9f0aa2b1e0bf10e95794136954a5c9badb044913a15634f562b27a24d35833da5378de2848e7c9f55a6eb487261b13f + languageName: node + linkType: hard + "@storybook/csf-tools@npm:8.1.10": version: 8.1.10 resolution: "@storybook/csf-tools@npm:8.1.10" @@ -2702,6 +2844,23 @@ __metadata: languageName: node linkType: hard +"@storybook/csf-tools@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/csf-tools@npm:8.1.11" + dependencies: + "@babel/generator": "npm:^7.24.4" + "@babel/parser": "npm:^7.24.4" + "@babel/traverse": "npm:^7.24.1" + "@babel/types": "npm:^7.24.0" + "@storybook/csf": "npm:^0.1.7" + "@storybook/types": "npm:8.1.11" + fs-extra: "npm:^11.1.0" + recast: "npm:^0.23.5" + ts-dedent: "npm:^2.0.0" + checksum: 10/eb9efa4f2b1ad6bcb6a8f420f3dfbcf9c709c7e5a4b3b08669bfa9e798a0058b4a73ebb50ce82b877c57f051c73a9d7ff63ce1faa054cc5ceb3a01788bf47ef2 + languageName: node + linkType: hard + "@storybook/csf@npm:^0.1.7": version: 0.1.7 resolution: "@storybook/csf@npm:0.1.7" @@ -2727,6 +2886,22 @@ __metadata: languageName: node linkType: hard +"@storybook/docs-tools@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/docs-tools@npm:8.1.11" + dependencies: + "@storybook/core-common": "npm:8.1.11" + "@storybook/core-events": "npm:8.1.11" + "@storybook/preview-api": "npm:8.1.11" + "@storybook/types": "npm:8.1.11" + "@types/doctrine": "npm:^0.0.3" + assert: "npm:^2.1.0" + doctrine: "npm:^3.0.0" + lodash: "npm:^4.17.21" + checksum: 10/baaba321d380c26918ce479bd4095d58efe63b14f7352c1f2577d535b80137f64d90583d98ccaf73780471c5f9132150a80038d0b16939a685cdf862dbcb462e + languageName: node + linkType: hard + "@storybook/global@npm:^5.0.0": version: 5.0.0 resolution: "@storybook/global@npm:5.0.0" @@ -2741,6 +2916,13 @@ __metadata: languageName: node linkType: hard +"@storybook/node-logger@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/node-logger@npm:8.1.11" + checksum: 10/9f96c30cacae8c138f5f17f11e243a3d506faad606e4146e57a5cafdfbcb0d1f3397d6f17c36b2422db4e927451cf9c6de785cc6f81e078b729f7b906af39650 + languageName: node + linkType: hard + "@storybook/preview-api@npm:8.1.10": version: 8.1.10 resolution: "@storybook/preview-api@npm:8.1.10" @@ -2763,6 +2945,28 @@ __metadata: languageName: node linkType: hard +"@storybook/preview-api@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/preview-api@npm:8.1.11" + dependencies: + "@storybook/channels": "npm:8.1.11" + "@storybook/client-logger": "npm:8.1.11" + "@storybook/core-events": "npm:8.1.11" + "@storybook/csf": "npm:^0.1.7" + "@storybook/global": "npm:^5.0.0" + "@storybook/types": "npm:8.1.11" + "@types/qs": "npm:^6.9.5" + dequal: "npm:^2.0.2" + lodash: "npm:^4.17.21" + memoizerific: "npm:^1.11.3" + qs: "npm:^6.10.0" + tiny-invariant: "npm:^1.3.1" + ts-dedent: "npm:^2.0.0" + util-deprecate: "npm:^1.0.2" + checksum: 10/228fc11266d4e33e4da2964b301d3d6f6ab40b7488d8e5dfcf556f7f5eb5db9270ddc1e4794995e3e4c30acf5ff91344c28306e648cfea7a6b22ca930ebc5826 + languageName: node + linkType: hard + "@storybook/preview@npm:8.1.10": version: 8.1.10 resolution: "@storybook/preview@npm:8.1.10" @@ -2770,6 +2974,13 @@ __metadata: languageName: node linkType: hard +"@storybook/preview@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/preview@npm:8.1.11" + checksum: 10/e47d65f283fe1ba4a5e92add77f8c27a9c049e3283f1a4248ec34952bcd7ea9883437131bbc5b2e7c86ae5075039136067565680743c719198107ffa8688ba3f + languageName: node + linkType: hard + "@storybook/react-dom-shim@npm:8.1.10": version: 8.1.10 resolution: "@storybook/react-dom-shim@npm:8.1.10" @@ -2780,6 +2991,16 @@ __metadata: languageName: node linkType: hard +"@storybook/react-dom-shim@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/react-dom-shim@npm:8.1.11" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + checksum: 10/dc4724f361c13d2d9d9056caa5cd564b88cfea010e055d1a7359f1e38440715fc523e94b45e9d2f79400ef955427b61eeb08b29810c13aef866e0da169eb5ba0 + languageName: node + linkType: hard + "@storybook/react-vite@npm:8.1.10": version: 8.1.10 resolution: "@storybook/react-vite@npm:8.1.10" @@ -2803,6 +3024,29 @@ __metadata: languageName: node linkType: hard +"@storybook/react-vite@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/react-vite@npm:8.1.11" + dependencies: + "@joshwooding/vite-plugin-react-docgen-typescript": "npm:0.3.1" + "@rollup/pluginutils": "npm:^5.0.2" + "@storybook/builder-vite": "npm:8.1.11" + "@storybook/node-logger": "npm:8.1.11" + "@storybook/react": "npm:8.1.11" + "@storybook/types": "npm:8.1.11" + find-up: "npm:^5.0.0" + magic-string: "npm:^0.30.0" + react-docgen: "npm:^7.0.0" + resolve: "npm:^1.22.8" + tsconfig-paths: "npm:^4.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + vite: ^4.0.0 || ^5.0.0 + checksum: 10/f4298efeaa6c639858859feaef05e02fd90f6e389d3f9cdb40fe5d2c94af2a5a137280b961056a84a48105b00e587800c01186763646eaf088c87f831b8c83d2 + languageName: node + linkType: hard + "@storybook/react@npm:8.1.10": version: 8.1.10 resolution: "@storybook/react@npm:8.1.10" @@ -2839,6 +3083,42 @@ __metadata: languageName: node linkType: hard +"@storybook/react@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/react@npm:8.1.11" + dependencies: + "@storybook/client-logger": "npm:8.1.11" + "@storybook/docs-tools": "npm:8.1.11" + "@storybook/global": "npm:^5.0.0" + "@storybook/preview-api": "npm:8.1.11" + "@storybook/react-dom-shim": "npm:8.1.11" + "@storybook/types": "npm:8.1.11" + "@types/escodegen": "npm:^0.0.6" + "@types/estree": "npm:^0.0.51" + "@types/node": "npm:^18.0.0" + acorn: "npm:^7.4.1" + acorn-jsx: "npm:^5.3.1" + acorn-walk: "npm:^7.2.0" + escodegen: "npm:^2.1.0" + html-tags: "npm:^3.1.0" + lodash: "npm:^4.17.21" + prop-types: "npm:^15.7.2" + react-element-to-jsx-string: "npm:^15.0.0" + semver: "npm:^7.3.7" + ts-dedent: "npm:^2.0.0" + type-fest: "npm:~2.19" + util-deprecate: "npm:^1.0.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + typescript: ">= 4.2.x" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/c38c105a8f4b37d3a9f054826186e2523dff65ee213ee0344800abd8f50468bfa68a302ef047113da3d74f58f3e03f676a0456f970fe9a29c9629210d226ef0d + languageName: node + linkType: hard + "@storybook/types@npm:8.1.10": version: 8.1.10 resolution: "@storybook/types@npm:8.1.10" @@ -2850,6 +3130,17 @@ __metadata: languageName: node linkType: hard +"@storybook/types@npm:8.1.11": + version: 8.1.11 + resolution: "@storybook/types@npm:8.1.11" + dependencies: + "@storybook/channels": "npm:8.1.11" + "@types/express": "npm:^4.7.0" + file-system-cache: "npm:2.3.0" + checksum: 10/80e2ba60cee54f4471025b9a986df78e12b6f6096769e6a2224549f2008235c2079965a71c9af05be26eafb8ddfaba8431ffcf4b8e39b229c08fb2d48670d851 + languageName: node + linkType: hard + "@tanstack/match-sorter-utils@npm:^8.7.0": version: 8.15.1 resolution: "@tanstack/match-sorter-utils@npm:8.15.1" @@ -3352,7 +3643,7 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:*, @types/prop-types@npm:^15.7.11": +"@types/prop-types@npm:*, @types/prop-types@npm:^15.7.11, @types/prop-types@npm:^15.7.12": version: 15.7.12 resolution: "@types/prop-types@npm:15.7.12" checksum: 10/ac16cc3d0a84431ffa5cfdf89579ad1e2269549f32ce0c769321fdd078f84db4fbe1b461ed5a1a496caf09e637c0e367d600c541435716a55b1d9713f5035dfe @@ -3543,15 +3834,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:7.15.0": - version: 7.15.0 - resolution: "@typescript-eslint/eslint-plugin@npm:7.15.0" +"@typescript-eslint/eslint-plugin@npm:7.16.0": + version: 7.16.0 + resolution: "@typescript-eslint/eslint-plugin@npm:7.16.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:7.15.0" - "@typescript-eslint/type-utils": "npm:7.15.0" - "@typescript-eslint/utils": "npm:7.15.0" - "@typescript-eslint/visitor-keys": "npm:7.15.0" + "@typescript-eslint/scope-manager": "npm:7.16.0" + "@typescript-eslint/type-utils": "npm:7.16.0" + "@typescript-eslint/utils": "npm:7.16.0" + "@typescript-eslint/visitor-keys": "npm:7.16.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -3562,44 +3853,44 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/e6b21687ab9e9dc38eb1b1d90a3ac483f3f5e5e9c49aa8a434a24de016822d65c82b926cda2ae79bac2225bd9495fb04f7aa6afcaad2b09f6129fd8014fbcedd + checksum: 10/beda6b586bfc953843877395b09acc0525d727dcb77e6ded5fbc645a9008b7e60360ddbaf6a9b7deaf23cd42c206412b7150d8df27f1fe2da3dc24dfab1c8d71 languageName: node linkType: hard -"@typescript-eslint/parser@npm:7.15.0": - version: 7.15.0 - resolution: "@typescript-eslint/parser@npm:7.15.0" +"@typescript-eslint/parser@npm:7.16.0": + version: 7.16.0 + resolution: "@typescript-eslint/parser@npm:7.16.0" dependencies: - "@typescript-eslint/scope-manager": "npm:7.15.0" - "@typescript-eslint/types": "npm:7.15.0" - "@typescript-eslint/typescript-estree": "npm:7.15.0" - "@typescript-eslint/visitor-keys": "npm:7.15.0" + "@typescript-eslint/scope-manager": "npm:7.16.0" + "@typescript-eslint/types": "npm:7.16.0" + "@typescript-eslint/typescript-estree": "npm:7.16.0" + "@typescript-eslint/visitor-keys": "npm:7.16.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: 10/0b5e7a14fa5d0680efb17e750a095729a7fb7c785d7a0fea2f9e6cbfef9e65caab2b751654b348b9ab813d222c1c3f8189ebf48561b81224d1821cee5c99d658 + checksum: 10/dc374e6c9e7dfcdd968828bb32ef59d3ebabd0a18671dee22d14dda2c713dade6eb493fd11b127df17035c7451898b42f4a88102da9a4bf3ca6a3baed8c20309 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:7.15.0": - version: 7.15.0 - resolution: "@typescript-eslint/scope-manager@npm:7.15.0" +"@typescript-eslint/scope-manager@npm:7.16.0": + version: 7.16.0 + resolution: "@typescript-eslint/scope-manager@npm:7.16.0" dependencies: - "@typescript-eslint/types": "npm:7.15.0" - "@typescript-eslint/visitor-keys": "npm:7.15.0" - checksum: 10/45bfdbae2d080691a34f5b37679b4a4067981baa3b82922268abdd21f6917a8dd1c4ccb12133f6c9cce81cfd640040913b223e8125235b92f42fdb57db358a3e + "@typescript-eslint/types": "npm:7.16.0" + "@typescript-eslint/visitor-keys": "npm:7.16.0" + checksum: 10/bf39a3ab803503c33e6c33568e7b93793d53d18100cb2f2ec1a540121aeba74d291d19c9ad3933198ff15e53a46d2f92db0c54309259dc99c1e3e297becd5677 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:7.15.0": - version: 7.15.0 - resolution: "@typescript-eslint/type-utils@npm:7.15.0" +"@typescript-eslint/type-utils@npm:7.16.0": + version: 7.16.0 + resolution: "@typescript-eslint/type-utils@npm:7.16.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:7.15.0" - "@typescript-eslint/utils": "npm:7.15.0" + "@typescript-eslint/typescript-estree": "npm:7.16.0" + "@typescript-eslint/utils": "npm:7.16.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.3.0" peerDependencies: @@ -3607,23 +3898,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/64fa589b413567df3689a19ef88f3dbaed66d965e39cc548a58626eb5bd8fc4e2338496eb632f3472de9ae9800cb14d0e48ef3508efe80bdb91af8f3f1e56ad7 + checksum: 10/84925c851a515768317573984dc855ac93bf787ebaa6382379dea6b356adb936ebd38bf7ab2f95124c68de7ab1fd5c849fe6717929343a80b839757fb5bf3af0 languageName: node linkType: hard -"@typescript-eslint/types@npm:7.15.0": - version: 7.15.0 - resolution: "@typescript-eslint/types@npm:7.15.0" - checksum: 10/b36c98344469f4bc54a5199733ea4f6d4d0f2da1070605e60d4031e2da2946b84b91a90108516c8e6e83a21030ba4e935053a0906041c920156de40683297d0b +"@typescript-eslint/types@npm:7.16.0": + version: 7.16.0 + resolution: "@typescript-eslint/types@npm:7.16.0" + checksum: 10/0813d9eb158f984b9d7e9e83961533ddc1e8c8815ca9059dab820df276b1e537b183f4c83cc4fe79ab3865cde1a64f2ec3f7fffe7209872d7d404636299f630b languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.15.0": - version: 7.15.0 - resolution: "@typescript-eslint/typescript-estree@npm:7.15.0" +"@typescript-eslint/typescript-estree@npm:7.16.0": + version: 7.16.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.16.0" dependencies: - "@typescript-eslint/types": "npm:7.15.0" - "@typescript-eslint/visitor-keys": "npm:7.15.0" + "@typescript-eslint/types": "npm:7.16.0" + "@typescript-eslint/visitor-keys": "npm:7.16.0" debug: "npm:^4.3.4" globby: "npm:^11.1.0" is-glob: "npm:^4.0.3" @@ -3633,31 +3924,31 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10/c5fb15108fbbc1bc976e827218ff7bfbc78930c5906292325ee42ba03514623e7b861497b3e3087f71ede9a757b16441286b4d234450450b0dd70ff753782736 + checksum: 10/5719c0cb649d627a073f1c8994a6073acc211ecfce0daef61d2de4315e42a23cf79e4dacb3b3596c4792eab062fdd22080c62345e2a58d38e7268eb6103a46d4 languageName: node linkType: hard -"@typescript-eslint/utils@npm:7.15.0": - version: 7.15.0 - resolution: "@typescript-eslint/utils@npm:7.15.0" +"@typescript-eslint/utils@npm:7.16.0": + version: 7.16.0 + resolution: "@typescript-eslint/utils@npm:7.16.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:7.15.0" - "@typescript-eslint/types": "npm:7.15.0" - "@typescript-eslint/typescript-estree": "npm:7.15.0" + "@typescript-eslint/scope-manager": "npm:7.16.0" + "@typescript-eslint/types": "npm:7.16.0" + "@typescript-eslint/typescript-estree": "npm:7.16.0" peerDependencies: eslint: ^8.56.0 - checksum: 10/f6de1849dee610a8110638be98ab2ec09e7cdf2f756b538b0544df2dfad86a8e66d5326a765302fe31553e8d9d3170938c0d5d38bd9c7d36e3ee0beb1bdc8172 + checksum: 10/325eab6705e70322d8df613cba4b018abc5d8ef857eb6c86f7a8376334eac789e6a585d30c041045c7eeede18083744faae66f48033e7811b2a23ebe8f6d3407 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:7.15.0": - version: 7.15.0 - resolution: "@typescript-eslint/visitor-keys@npm:7.15.0" +"@typescript-eslint/visitor-keys@npm:7.16.0": + version: 7.16.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.16.0" dependencies: - "@typescript-eslint/types": "npm:7.15.0" + "@typescript-eslint/types": "npm:7.16.0" eslint-visitor-keys: "npm:^3.4.3" - checksum: 10/0e17d7f5de767da7f98170c2efc905cdb0ceeaf04a667e12ca1a92eae64479a07f4f8e2a9b5023b055b01250916c3bcac86908cd06552610baff734fafae4464 + checksum: 10/aae065bdd6d5681d40df51af24933fc86c15f355f9d8f85c39a506f352ddc2a76fc72d4f8cf823ebb7550c84d543605a2fdd7d06979a0967cd48c1f542436714 languageName: node linkType: hard @@ -4396,7 +4687,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.2, braces@npm:~3.0.2": +"braces@npm:^3.0.2": version: 3.0.2 resolution: "braces@npm:3.0.2" dependencies: @@ -4405,6 +4696,15 @@ __metadata: languageName: node linkType: hard +"braces@npm:~3.0.2": + version: 3.0.3 + resolution: "braces@npm:3.0.3" + dependencies: + fill-range: "npm:^7.1.1" + checksum: 10/fad11a0d4697a27162840b02b1fad249c1683cbc510cd5bf1a471f2f8085c046d41094308c577a50a03a579dd99d5a6b3724c4b5e8b14df2c4443844cfcda2c6 + languageName: node + linkType: hard + "broadcast-channel@npm:^3.4.1": version: 3.7.0 resolution: "broadcast-channel@npm:3.7.0" @@ -5252,13 +5552,20 @@ __metadata: languageName: node linkType: hard -"dayjs@npm:1.11.10, dayjs@npm:^1.10.4": +"dayjs@npm:1.11.10": version: 1.11.10 resolution: "dayjs@npm:1.11.10" checksum: 10/27e8f5bc01c0a76f36c656e62ab7f08c2e7b040b09e613cd4844abf03fb258e0350f0a83b02c887b84d771c1f11e092deda0beef8c6df2a1afbc3f6c1fade279 languageName: node linkType: hard +"dayjs@npm:^1.10.4": + version: 1.11.11 + resolution: "dayjs@npm:1.11.11" + checksum: 10/f03948b172fbeed229837965988d1d5bac99c72a31c28731a457303259439f2f36289186489ae140adbeb10f591a926908c8de5d81eb449a2edbf5cbd6e9e30c + languageName: node + linkType: hard + "debug@npm:2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" @@ -5268,7 +5575,19 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": + version: 4.3.5 + resolution: "debug@npm:4.3.5" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/cb6eab424c410e07813ca1392888589972ce9a32b8829c6508f5e1f25f3c3e70a76731610ae55b4bbe58d1a2fffa1424b30e97fa8d394e49cd2656a9643aedd2 + languageName: node + linkType: hard + +"debug@npm:4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -6689,6 +7008,13 @@ __metadata: languageName: node linkType: hard +"filesize@npm:10.1.4": + version: 10.1.4 + resolution: "filesize@npm:10.1.4" + checksum: 10/ac2b95f4ee8d42ad4b12f8f918baeb1127065dcb319abca30c0d9ef115b602e31a06c8150953b13dc52e52ebb1238e18e6001ab5fca14a10957e788bd6012f1c + languageName: node + linkType: hard + "fill-range@npm:^7.0.1": version: 7.0.1 resolution: "fill-range@npm:7.0.1" @@ -6698,6 +7024,15 @@ __metadata: languageName: node linkType: hard +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 10/a7095cb39e5bc32fada2aa7c7249d3f6b01bd1ce461a61b0adabacccabd9198500c6fb1f68a7c851a657e273fce2233ba869638897f3d7ed2e87a2d89b4436ea + languageName: node + linkType: hard + "finalhandler@npm:1.2.0": version: 1.2.0 resolution: "finalhandler@npm:1.2.0" @@ -7226,10 +7561,10 @@ __metadata: "@emotion/styled": "npm:11.11.5" "@graasp/chatbox": "npm:3.1.0" "@graasp/map": "npm:1.16.0" - "@graasp/query-client": "npm:3.14.0" - "@graasp/sdk": "npm:4.15.1" + "@graasp/query-client": "npm:3.15.2" + "@graasp/sdk": "npm:4.17.0" "@graasp/translations": "npm:1.31.0" - "@graasp/ui": "npm:4.20.2" + "@graasp/ui": "npm:4.21.0" "@mui/icons-material": "npm:5.16.0" "@mui/lab": "npm:5.0.0-alpha.170" "@mui/material": "npm:5.16.0" @@ -7251,8 +7586,8 @@ __metadata: "@types/react-dom": "npm:18.3.0" "@types/uuid": "npm:10.0.0" "@types/validator": "npm:13.11.10" - "@typescript-eslint/eslint-plugin": "npm:7.15.0" - "@typescript-eslint/parser": "npm:7.15.0" + "@typescript-eslint/eslint-plugin": "npm:7.16.0" + "@typescript-eslint/parser": "npm:7.16.0" "@vitejs/plugin-react": "npm:4.3.1" axios: "npm:1.7.2" concurrently: "npm:8.2.2" @@ -8575,6 +8910,17 @@ __metadata: languageName: node linkType: hard +"katex@npm:0.16.11": + version: 0.16.11 + resolution: "katex@npm:0.16.11" + dependencies: + commander: "npm:^8.3.0" + bin: + katex: cli.js + checksum: 10/adfb95a70168f732c26f44a443d27df393ca641a3533aa9321f37b1b69134cf4b15142d533c187ec9a0b02c0bbfebab5ab26f15bd0cc08a57114e1f767f0d7ae + languageName: node + linkType: hard + "keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -8968,15 +9314,6 @@ __metadata: languageName: node linkType: hard -"lucide-react@npm:0.399.0": - version: 0.399.0 - resolution: "lucide-react@npm:0.399.0" - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10/89212e54dc58c99f6d7593fc4ac5020771ec57bf334a4f5c1280df131cfb99b2c2ffabcf44a7c26670bee29979d687e7b4318b5d8681d404714faa1e9cc2a6ce - languageName: node - linkType: hard - "lucide-react@npm:0.402.0": version: 0.402.0 resolution: "lucide-react@npm:0.402.0" @@ -9853,15 +10190,15 @@ __metadata: languageName: node linkType: hard -"mlly@npm:^1.4.2, mlly@npm:^1.6.1": - version: 1.6.1 - resolution: "mlly@npm:1.6.1" +"mlly@npm:^1.4.2, mlly@npm:^1.7.0": + version: 1.7.1 + resolution: "mlly@npm:1.7.1" dependencies: acorn: "npm:^8.11.3" pathe: "npm:^1.1.2" - pkg-types: "npm:^1.0.3" - ufo: "npm:^1.3.2" - checksum: 10/00b4c355236eb3d0294106f208718db486f6e34e28bbb7f6965bd9d6237db338e566f2e13489fbf8bfa9b1337c0f2568d4aeac1840f9963054c91881acc974a9 + pkg-types: "npm:^1.1.1" + ufo: "npm:^1.5.3" + checksum: 10/c1ef3989e95fb6c6c27a238330897b01f46507020501f45a681f2cae453f982e38dcb0e45aa65f672ea7280945d4a729d266f17a8acb187956f312b0cafddf61 languageName: node linkType: hard @@ -10546,14 +10883,14 @@ __metadata: languageName: node linkType: hard -"pkg-types@npm:^1.0.3": - version: 1.1.0 - resolution: "pkg-types@npm:1.1.0" +"pkg-types@npm:^1.0.3, pkg-types@npm:^1.1.1": + version: 1.1.1 + resolution: "pkg-types@npm:1.1.1" dependencies: confbox: "npm:^0.1.7" - mlly: "npm:^1.6.1" + mlly: "npm:^1.7.0" pathe: "npm:^1.1.2" - checksum: 10/c1e32a54a1ae00205eb769f6cdae1f0ed4389c785963875b2d53ce7445ac8f762d0e837a84b1ab802375f1f8f7fd0639ceaf81fc9bb9be84c360a3a9ddbddbae + checksum: 10/225eaf7c0339027e176dd0d34a6d9a1384c21e0aab295e57dfbef1f1b7fc132f008671da7e67553e352b80b17ba38c531c720c914061d277410eef1bdd9d9608 languageName: node linkType: hard @@ -11109,6 +11446,13 @@ __metadata: languageName: node linkType: hard +"react-is@npm:^18.3.1": + version: 18.3.1 + resolution: "react-is@npm:18.3.1" + checksum: 10/d5f60c87d285af24b1e1e7eaeb123ec256c3c8bdea7061ab3932e3e14685708221bf234ec50b21e10dd07f008f1b966a2730a0ce4ff67905b3872ff2042aec22 + languageName: node + linkType: hard + "react-leaflet-cluster@npm:2.1.0": version: 2.1.0 resolution: "react-leaflet-cluster@npm:2.1.0" @@ -11803,16 +12147,7 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.23.0": - version: 0.23.0 - resolution: "scheduler@npm:0.23.0" - dependencies: - loose-envify: "npm:^1.1.0" - checksum: 10/0c4557aa37bafca44ff21dc0ea7c92e2dbcb298bc62eae92b29a39b029134f02fb23917d6ebc8b1fa536b4184934314c20d8864d156a9f6357f3398aaf7bfda8 - languageName: node - linkType: hard - -"scheduler@npm:^0.23.2": +"scheduler@npm:^0.23.0, scheduler@npm:^0.23.2": version: 0.23.2 resolution: "scheduler@npm:0.23.2" dependencies: @@ -12703,7 +13038,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.6.2, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0": +"tslib@npm:2.6.2": version: 2.6.2 resolution: "tslib@npm:2.6.2" checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca @@ -12717,6 +13052,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.3.0": + version: 2.6.3 + resolution: "tslib@npm:2.6.3" + checksum: 10/52109bb681f8133a2e58142f11a50e05476de4f075ca906d13b596ae5f7f12d30c482feb0bff167ae01cfc84c5803e575a307d47938999246f5a49d174fc558c + languageName: node + linkType: hard + "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -12875,7 +13217,7 @@ __metadata: languageName: node linkType: hard -"ufo@npm:^1.3.2": +"ufo@npm:^1.5.3": version: 1.5.3 resolution: "ufo@npm:1.5.3" checksum: 10/2b30dddd873c643efecdb58cfe457183cd4d95937ccdacca6942c697b87a2c578232c25a5149fda85436696bf0fdbc213bf2b220874712bc3e58c0fb00a2c950