diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 7129e21fd..b47c0f2b4 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -9,7 +9,7 @@ on: concurrency: group: cypress-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: cypress: diff --git a/cypress/e2e/apps.cy.ts b/cypress/e2e/apps.cy.ts index 973a65904..7a793081a 100644 --- a/cypress/e2e/apps.cy.ts +++ b/cypress/e2e/apps.cy.ts @@ -1,6 +1,6 @@ import 'cypress-iframe'; -import { buildMainPath } from '@/config/paths'; +import { buildContentPagePath } from '@/config/paths'; import { APP_USING_CONTEXT_ITEM, @@ -25,7 +25,7 @@ describe('Apps', () => { it('App should request context', () => { const { id, name } = APP_USING_CONTEXT_ITEM; cy.setUpApi({ items: [APP_USING_CONTEXT_ITEM] }); - cy.visit(buildMainPath({ rootId: id })); + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); cy.wait(3000); const iframeSelector = `iframe[title="${name}"]`; @@ -70,7 +70,7 @@ describe('Public Apps', () => { currentMember: null, }); - cy.visit(buildMainPath({ rootId: id })); + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); }); it('Public App should request context', () => { diff --git a/cypress/e2e/chatbox.cy.ts b/cypress/e2e/chatbox.cy.ts index 567d98f66..7a691b77c 100644 --- a/cypress/e2e/chatbox.cy.ts +++ b/cypress/e2e/chatbox.cy.ts @@ -1,4 +1,5 @@ -import { buildMainPath } from '../../src/config/paths'; +import { buildContentPagePath } from '@/config/paths'; + import { ITEM_CHATBOX_BUTTON_ID, ITEM_CHATBOX_ID, @@ -20,24 +21,25 @@ describe('Chatbox', () => { }); it('Chatbox button should toggle chatbox visibility', () => { - cy.visit(buildMainPath({ rootId: ITEM_WITH_CHAT_BOX.id })); + const { id } = ITEM_WITH_CHAT_BOX; + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); cy.wait('@getCurrentMember'); - cy.get(`#${ITEM_CHATBOX_BUTTON_ID}`).should('exist'); - cy.get(`#${ITEM_CHATBOX_ID}`).should('not.exist'); - cy.get(`#${ITEM_CHATBOX_BUTTON_ID}`).click(); + // chatbox is closed by default + cy.get(`#${ITEM_CHATBOX_ID}`).should('not.be.visible'); + cy.get(`#${ITEM_CHATBOX_BUTTON_ID}`).should('be.visible').click(); cy.get(`#${ITEM_CHATBOX_ID}`).should('be.visible'); }); it('Side panel button should hide chatbox', () => { - cy.visit(buildMainPath({ rootId: ITEM_WITH_CHAT_BOX.id })); + const { id } = ITEM_WITH_CHAT_BOX; + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); cy.wait('@getCurrentMember'); - cy.get(`#${ITEM_CHATBOX_BUTTON_ID}`).should('exist'); - cy.get(`#${ITEM_CHATBOX_ID}`).should('not.be.exist'); - + cy.get(`#${ITEM_CHATBOX_ID}`).should('not.be.visible'); + cy.get(`#${ITEM_CHATBOX_BUTTON_ID}`).should('be.visible'); cy.get(`#${ITEM_CHATBOX_BUTTON_ID}`).click(); cy.get(`#${ITEM_CHATBOX_ID}`).should('be.visible'); @@ -47,7 +49,8 @@ describe('Chatbox', () => { }); it('Disabled chatbox should not have button', () => { - cy.visit(buildMainPath({ rootId: ITEM_WITHOUT_CHAT_BOX.id })); + const { id } = ITEM_WITH_CHAT_BOX; + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); cy.wait('@getCurrentMember'); cy.get(`#${ITEM_CHATBOX_BUTTON_ID}`).should('not.exist'); @@ -55,9 +58,11 @@ describe('Chatbox', () => { }); it('Chatbox button is clickable on document', () => { + const { id } = GRAASP_DOCUMENT_ITEM_WITH_CHAT_BOX; cy.visit( - buildMainPath({ - rootId: GRAASP_DOCUMENT_ITEM_WITH_CHAT_BOX.id, + buildContentPagePath({ + rootId: id, + itemId: id, }), ); cy.wait('@getCurrentMember'); diff --git a/cypress/e2e/collapsed.cy.ts b/cypress/e2e/collapsed.cy.ts index 44872e0c0..f3a1d6426 100644 --- a/cypress/e2e/collapsed.cy.ts +++ b/cypress/e2e/collapsed.cy.ts @@ -1,4 +1,4 @@ -import { buildMainPath } from '@/config/paths'; +import { buildContentPagePath } from '@/config/paths'; import { buildCollapsibleId } from '@/config/selectors'; import { FOLDER_WITH_COLLAPSIBLE_SHORTCUT_ITEMS } from '../fixtures/items'; @@ -12,7 +12,7 @@ describe('Collapsible', () => { it('Shows a collapsible wrapper around a collapsible shortcut', () => { const parent = FOLDER_WITH_COLLAPSIBLE_SHORTCUT_ITEMS.items[1]; - cy.visit(buildMainPath({ rootId: parent.id })); + cy.visit(buildContentPagePath({ rootId: parent.id, itemId: parent.id })); const collapsedShortcut = FOLDER_WITH_COLLAPSIBLE_SHORTCUT_ITEMS.items[2]; // collapsible document should show as collapsed cy.get(`#${buildCollapsibleId(collapsedShortcut.id)}`) diff --git a/cypress/e2e/hidden.cy.ts b/cypress/e2e/hidden.cy.ts index eccc5c236..952737084 100644 --- a/cypress/e2e/hidden.cy.ts +++ b/cypress/e2e/hidden.cy.ts @@ -1,4 +1,4 @@ -import { buildMainPath } from '../../src/config/paths'; +import { buildContentPagePath } from '../../src/config/paths'; import { buildDocumentId } from '../../src/config/selectors'; import { FOLDER_WITH_HIDDEN_ITEMS, @@ -13,7 +13,7 @@ describe('Hidden Items', () => { }); const parent = FOLDER_WITH_HIDDEN_ITEMS.items[0]; - cy.visit(buildMainPath({ rootId: parent.id })); + cy.visit(buildContentPagePath({ rootId: parent.id, itemId: parent.id })); // hidden document should not be displayed cy.get(`#${buildDocumentId(FOLDER_WITH_HIDDEN_ITEMS.items[1].id)}`).should( @@ -31,7 +31,7 @@ describe('Hidden Items', () => { }); const parent = FOLDER_WITH_HIDDEN_ITEMS.items[0]; - cy.visit(buildMainPath({ rootId: parent.id })); + cy.visit(buildContentPagePath({ rootId: parent.id, itemId: parent.id })); cy.get(`#${buildDocumentId(FOLDER_WITH_HIDDEN_ITEMS.items[1].id)}`).should( 'be.visible', @@ -49,7 +49,7 @@ describe('Hidden Items', () => { }); const parent = FOLDER_WITH_HIDDEN_ITEMS.items[0]; - cy.visit(buildMainPath({ rootId: parent.id })); + cy.visit(buildContentPagePath({ rootId: parent.id, itemId: parent.id })); cy.get(`#${buildDocumentId(FOLDER_WITH_HIDDEN_ITEMS.items[1].id)}`).should( 'be.visible', @@ -67,7 +67,7 @@ describe('Hidden Items', () => { }); const parent = PUBLIC_FOLDER_WITH_HIDDEN_ITEMS.items[0]; - cy.visit(buildMainPath({ rootId: parent.id })); + cy.visit(buildContentPagePath({ rootId: parent.id, itemId: parent.id })); cy.get( `#${buildDocumentId(PUBLIC_FOLDER_WITH_HIDDEN_ITEMS.items[1].id)}`, diff --git a/cypress/e2e/island.cy.ts b/cypress/e2e/island.cy.ts new file mode 100644 index 000000000..3a30f53b0 --- /dev/null +++ b/cypress/e2e/island.cy.ts @@ -0,0 +1,27 @@ +import { buildContentPagePath } from '@/config/paths'; +import { ITEM_CHATBOX_BUTTON_ID } from '@/config/selectors'; + +import { + DOCUMENT_WITHOUT_CHAT_BOX, + DOCUMENT_WITH_CHAT_BOX, +} from '../fixtures/items'; + +describe('Island', () => { + it('Show island with chat button on document with chat', () => { + const item = DOCUMENT_WITH_CHAT_BOX; + cy.setUpApi({ + items: [item], + }); + cy.visit(buildContentPagePath({ rootId: item.id, itemId: item.id })); + cy.get(`#${ITEM_CHATBOX_BUTTON_ID}`).should('be.visible'); + }); + + it('Hide island on document without chat', () => { + const item = DOCUMENT_WITHOUT_CHAT_BOX; + cy.setUpApi({ + items: [item], + }); + cy.visit(buildContentPagePath({ rootId: item.id, itemId: item.id })); + cy.get(`#${ITEM_CHATBOX_BUTTON_ID}`).should('not.exist'); + }); +}); diff --git a/cypress/e2e/main.cy.ts b/cypress/e2e/main.cy.ts index c187ff036..2960e04fb 100644 --- a/cypress/e2e/main.cy.ts +++ b/cypress/e2e/main.cy.ts @@ -1,4 +1,4 @@ -import { buildMainPath } from '../../src/config/paths'; +import { buildContentPagePath } from '../../src/config/paths'; import { FOLDER_NAME_TITLE_CLASS, MAIN_MENU_ID, @@ -55,19 +55,19 @@ describe('Main Screen', () => { describe('Links', () => { it('Website link', () => { const { id } = GRAASP_LINK_ITEM; - cy.visit(buildMainPath({ rootId: id })); + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); expectLinkViewScreenLayout(GRAASP_LINK_ITEM); }); it('Website link as iframe', () => { const { id } = GRAASP_LINK_ITEM_IFRAME_ONLY; - cy.visit(buildMainPath({ rootId: id })); + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); expectLinkViewScreenLayout(GRAASP_LINK_ITEM_IFRAME_ONLY); }); it('Youtube link', () => { const { id } = YOUTUBE_LINK_ITEM; - cy.visit(buildMainPath({ rootId: id })); + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); expectLinkViewScreenLayout(YOUTUBE_LINK_ITEM); }); @@ -76,19 +76,19 @@ describe('Main Screen', () => { describe('Files', () => { it('Image', () => { const { id } = IMAGE_ITEM_DEFAULT; - cy.visit(buildMainPath({ rootId: id })); + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); expectFileViewScreenLayout(IMAGE_ITEM_DEFAULT); }); it('Video', () => { const { id } = VIDEO_ITEM_DEFAULT; - cy.visit(buildMainPath({ rootId: id })); + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); expectFileViewScreenLayout(VIDEO_ITEM_DEFAULT); }); it('Pdf', () => { const { id } = PDF_ITEM_DEFAULT; - cy.visit(buildMainPath({ rootId: id })); + cy.visit(buildContentPagePath({ rootId: id, itemId: id })); expectFileViewScreenLayout(PDF_ITEM_DEFAULT); }); @@ -96,7 +96,12 @@ describe('Main Screen', () => { describe('Documents', () => { it('Graasp Document', () => { - cy.visit(buildMainPath({ rootId: GRAASP_DOCUMENT_ITEM.id })); + cy.visit( + buildContentPagePath({ + rootId: GRAASP_DOCUMENT_ITEM.id, + itemId: GRAASP_DOCUMENT_ITEM.id, + }), + ); expectDocumentViewScreenLayout(GRAASP_DOCUMENT_ITEM); }); @@ -104,7 +109,12 @@ describe('Main Screen', () => { describe('Apps', () => { it('App', () => { - cy.visit(buildMainPath({ rootId: GRAASP_APP_ITEM.id })); + cy.visit( + buildContentPagePath({ + rootId: GRAASP_APP_ITEM.id, + itemId: GRAASP_APP_ITEM.id, + }), + ); expectAppViewScreenLayout(GRAASP_APP_ITEM); }); @@ -113,7 +123,9 @@ describe('Main Screen', () => { describe('Folders', () => { it('Display sub Folder', () => { const parent = FOLDER_WITH_SUBFOLDER_ITEM.items[0]; - cy.visit(buildMainPath({ rootId: parent.id })); + cy.visit( + buildContentPagePath({ rootId: parent.id, itemId: parent.id }), + ); cy.get(`.${FOLDER_NAME_TITLE_CLASS}`).should('contain', parent.name); @@ -121,7 +133,9 @@ describe('Main Screen', () => { }); it('Display Folder without childrenOrder', () => { const parent = FOLDER_WITHOUT_CHILDREN_ORDER.items[0]; - cy.visit(buildMainPath({ rootId: parent.id })); + cy.visit( + buildContentPagePath({ rootId: parent.id, itemId: parent.id }), + ); cy.get(`.${FOLDER_NAME_TITLE_CLASS}`).should('contain', parent.name); }); @@ -133,7 +147,7 @@ describe('Main Screen', () => { cy.setUpApi(STATIC_ELECTRICITY); const parentFolder = STATIC_ELECTRICITY.items[0]; const rootId = parentFolder.id; - cy.visit(buildMainPath({ rootId })); + cy.visit(buildContentPagePath({ rootId, itemId: rootId })); expectFolderLayout({ rootId, items: STATIC_ELECTRICITY.items }); }); @@ -144,14 +158,14 @@ describe('Main Screen', () => { }); const parentFolder = STATIC_ELECTRICITY.items[0]; const rootId = parentFolder.id; - cy.visit(buildMainPath({ rootId })); + cy.visit(buildContentPagePath({ rootId, itemId: rootId })); cy.get(`#${MAIN_MENU_ID}`).should('not.exist'); }); it(`Display ${PUBLIC_STATIC_ELECTRICITY.items[0].name}`, () => { cy.setUpApi({ ...PUBLIC_STATIC_ELECTRICITY, currentMember: MEMBERS.BOB }); const parentFolder = PUBLIC_STATIC_ELECTRICITY.items[0]; const rootId = parentFolder.id; - cy.visit(buildMainPath({ rootId })); + cy.visit(buildContentPagePath({ rootId, itemId: rootId })); expectFolderLayout({ rootId, items: PUBLIC_STATIC_ELECTRICITY.items }); }); diff --git a/cypress/e2e/pinned.cy.ts b/cypress/e2e/pinned.cy.ts index 909a3a77a..e05fa49cc 100644 --- a/cypress/e2e/pinned.cy.ts +++ b/cypress/e2e/pinned.cy.ts @@ -1,4 +1,4 @@ -import { buildMainPath } from '../../src/config/paths'; +import { buildContentPagePath, buildMainPath } from '../../src/config/paths'; import { ITEM_PINNED_BUTTON_ID, ITEM_PINNED_ID, @@ -27,10 +27,10 @@ describe('Pinned Items', () => { it('Pinned button should toggle sidebar visibility', () => { const parent = FOLDER_WITH_SUBFOLDER_ITEM.items[0]; - cy.visit(buildMainPath({ rootId: parent.id })); + cy.visit(buildContentPagePath({ rootId: parent.id, itemId: parent.id })); cy.wait('@getChildren'); - cy.get(`#${ITEM_PINNED_BUTTON_ID}`).should('exist'); + cy.get(`#${ITEM_PINNED_BUTTON_ID}`).should('be.visible'); cy.get(`#${ITEM_PINNED_ID}`).should('be.visible'); cy.get(`#${ITEM_PINNED_BUTTON_ID}`).click(); @@ -40,7 +40,7 @@ describe('Pinned Items', () => { it('Parent folder should display pinned children', () => { const parent = FOLDER_WITH_PINNED_ITEMS.items[0]; const pinned = FOLDER_WITH_PINNED_ITEMS.items[2]; - cy.visit(buildMainPath({ rootId: parent.id })); + cy.visit(buildContentPagePath({ rootId: parent.id, itemId: parent.id })); cy.get(`#${ITEM_PINNED_ID} #${buildFolderButtonId(pinned.id)}`).should( 'contain', diff --git a/cypress/fixtures/items.ts b/cypress/fixtures/items.ts index 4ce8187c7..9c625b538 100644 --- a/cypress/fixtures/items.ts +++ b/cypress/fixtures/items.ts @@ -55,6 +55,32 @@ export const ITEM_WITH_CHAT_BOX: MockItem = { }, }; +export const DOCUMENT_WITH_CHAT_BOX: MockItem = { + ...DEFAULT_FOLDER_ITEM, + id: 'ecafbd2a-5688-11eb-ae93-0242ac130002', + name: 'parent folder', + path: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + type: 'document', + extra: { document: { content: 'hello this is a document' } }, + settings: { + isPinned: false, + showChatbox: true, + }, +}; + +export const DOCUMENT_WITHOUT_CHAT_BOX: MockItem = { + ...DEFAULT_FOLDER_ITEM, + id: 'ecafbd2a-5688-11eb-ae93-0242ac130002', + name: 'parent folder', + path: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + type: 'document', + extra: { document: { content: 'hello this is a document with no chatbox' } }, + settings: { + isPinned: false, + showChatbox: false, + }, +}; + export const ITEM_WITHOUT_CHAT_BOX: MockItem = { ...DEFAULT_FOLDER_ITEM, id: 'fdf09f5a-5688-11eb-ae93-0242ac130003', diff --git a/package.json b/package.json index 9b7060895..788b905f1 100644 --- a/package.json +++ b/package.json @@ -36,16 +36,16 @@ "@emotion/react": "11.11.4", "@emotion/styled": "11.11.0", "@graasp/chatbox": "3.1.0", - "@graasp/query-client": "2.7.1", + "@graasp/query-client": "3.0.1", "@graasp/sdk": "4.2.1", "@graasp/translations": "1.25.3", "@graasp/ui": "4.11.0", - "@mui/icons-material": "5.15.11", + "@mui/icons-material": "5.15.14", "@mui/lab": "5.0.0-alpha.151", - "@mui/material": "5.15.11", + "@mui/material": "5.15.14", "@sentry/react": "7.108.0", "classnames": "2.5.1", - "date-fns": "3.3.1", + "date-fns": "3.6.0", "i18next": "23.10.1", "katex": "0.16.9", "lodash.isarray": "4.0.0", @@ -55,31 +55,31 @@ "react-dom": "18.2.0", "react-fullscreen-crossbrowser": "1.1.3", "react-ga4": "2.1.0", - "react-i18next": "14.0.5", + "react-i18next": "14.1.0", "react-intersection-observer": "9.8.1", "react-quill": "2.0.0", "react-router": "6.22.3", "react-router-dom": "6.22.3", - "react-toastify": "10.0.4", + "react-toastify": "10.0.5", "stylis": "4.3.1", "stylis-plugin-rtl": "2.1.1", "uuid": "9.0.1" }, "devDependencies": { "@commitlint/config-conventional": "19.1.0", - "@cypress/code-coverage": "3.12.26", + "@cypress/code-coverage": "3.12.30", "@trivago/prettier-plugin-sort-imports": "4.3.0", "@types/katex": "^0.16.7", - "@types/lodash.isarray": "^4", + "@types/lodash.isarray": "^4.0.9", "@types/lodash.truncate": "^4.4.9", - "@types/node": "^20.11.20", - "@types/react": "18.2.60", - "@types/react-dom": "^18.2.19", + "@types/node": "^20.11.30", + "@types/react": "18.2.67", + "@types/react-dom": "^18.2.22", "@types/uuid": "9.0.8", "@typescript-eslint/eslint-plugin": "7.3.1", "@typescript-eslint/parser": "7.3.1", "@vitejs/plugin-react": "^4.2.1", - "commitlint": "19.2.0", + "commitlint": "19.2.1", "concurrently": "8.2.2", "cypress": "13.7.0", "cypress-iframe": "1.0.1", @@ -91,7 +91,7 @@ "eslint-import-resolver-typescript": "3.6.1", "eslint-plugin-import": "2.29.1", "eslint-plugin-jsx-a11y": "6.8.0", - "eslint-plugin-react": "7.34.0", + "eslint-plugin-react": "7.34.1", "eslint-plugin-react-hooks": "4.6.0", "http-status-codes": "2.3.0", "husky": "9.0.11", @@ -100,7 +100,7 @@ "typescript": "5.4.2", "vite": "5.2.3", "vite-plugin-checker": "^0.6.4", - "vite-plugin-istanbul": "5.0.0" + "vite-plugin-istanbul": "6.0.0" }, "packageManager": "yarn@4.1.1" } diff --git a/src/App.tsx b/src/App.tsx index 89ddedcf8..693586859 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,24 +1,50 @@ import { useEffect } from 'react'; import { + Link, Navigate, Route, Routes, useLocation, + useParams, useSearchParams, } from 'react-router-dom'; +import { Alert, Button, Stack, Typography } from '@mui/material'; + import { saveUrlForRedirection } from '@graasp/sdk'; import { CustomInitialLoader, withAuthorization } from '@graasp/ui'; import { SIGN_IN_PATH } from '@/config/constants'; import { DOMAIN } from '@/config/env'; -import { HOME_PATH, buildMainPath } from '@/config/paths'; +import { HOME_PATH, buildContentPagePath, buildMainPath } from '@/config/paths'; import { useCurrentMemberContext } from '@/contexts/CurrentMemberContext'; import HomePage from '@/modules/pages/HomePage'; import ItemPage from '@/modules/pages/ItemPage'; +import { usePlayerTranslation } from './config/i18n'; +import { PLAYER } from './langs/constants'; import PageWrapper from './modules/layout/PageWrapper'; +const RedirectToRootContentPage = () => { + const { rootId } = useParams(); + const { t } = usePlayerTranslation(); + if (rootId) { + return ( + + ); + } + return ( + + + {t(PLAYER.ITEM_ID_NOT_VALID)} + + + + ); +}; + export const App = (): JSX.Element => { const location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); @@ -55,7 +81,10 @@ export const App = (): JSX.Element => { return ( }> - } /> + + } /> + } /> + } /> } /> diff --git a/src/config/paths.ts b/src/config/paths.ts index 9e4e4ad57..6be14ee46 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -1,4 +1,9 @@ export const HOME_PATH = '/'; export const ROOT_ID_PATH = 'rootId'; +export const ITEM_PARAM = 'itemId'; export const buildMainPath = ({ rootId = `:${ROOT_ID_PATH}` } = {}): string => `/${rootId}`; +export const buildContentPagePath = ({ + rootId = `:${ROOT_ID_PATH}`, + itemId = `:${ITEM_PARAM}`, +} = {}): string => `/${rootId}/${itemId}`; diff --git a/src/contexts/LayoutContext.tsx b/src/contexts/LayoutContext.tsx index 8b89e697e..362935aa0 100644 --- a/src/contexts/LayoutContext.tsx +++ b/src/contexts/LayoutContext.tsx @@ -1,23 +1,45 @@ -import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { + Dispatch, + SetStateAction, + createContext, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import { useMobileView } from '@graasp/ui'; type LayoutContextType = { - isPinnedMenuOpen: boolean; - setIsPinnedMenuOpen: (state: boolean) => void; - isChatboxMenuOpen: boolean; - setIsChatboxMenuOpen: (state: boolean) => void; + isPinnedOpen: boolean; + setIsPinnedOpen: Dispatch>; + isChatboxOpen: boolean; + setIsChatboxOpen: Dispatch>; isFullscreen: boolean; - setIsFullscreen: (state: boolean) => void; + setIsFullscreen: Dispatch>; + toggleChatbox: () => void; + togglePinned: () => void; }; const LayoutContext = createContext({ - isPinnedMenuOpen: true, - setIsPinnedMenuOpen: () => null, - isChatboxMenuOpen: false, - setIsChatboxMenuOpen: () => null, + isPinnedOpen: true, + setIsPinnedOpen: () => { + throw new Error('No context'); + }, + isChatboxOpen: false, + setIsChatboxOpen: () => { + throw new Error('No context'); + }, isFullscreen: false, - setIsFullscreen: () => null, + setIsFullscreen: () => { + throw new Error('No context'); + }, + toggleChatbox: () => { + throw new Error('No context'); + }, + togglePinned: () => { + throw new Error('No context'); + }, }); type Props = { @@ -27,33 +49,46 @@ type Props = { export const LayoutContextProvider = ({ children }: Props): JSX.Element => { const { isMobile } = useMobileView(); - const [isPinnedMenuOpen, setIsPinnedMenuOpen] = useState(!isMobile); - const [isChatboxMenuOpen, setIsChatboxMenuOpen] = useState(false); + const [isPinnedOpen, setIsPinnedOpen] = useState(!isMobile); + const [isChatboxOpen, setIsChatboxOpen] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { - setIsPinnedMenuOpen(!isMobile); + setIsPinnedOpen(!isMobile); }, [isMobile]); const value = useMemo( () => ({ - isPinnedMenuOpen, - setIsPinnedMenuOpen, - isChatboxMenuOpen, - setIsChatboxMenuOpen, + isPinnedOpen, + setIsPinnedOpen, + isChatboxOpen, + setIsChatboxOpen, isFullscreen, setIsFullscreen, + toggleChatbox: () => { + setIsChatboxOpen((prev) => !prev); + if (isPinnedOpen) { + // close the pinned items + setIsPinnedOpen(false); + } + }, + togglePinned: () => { + setIsPinnedOpen((prev) => !prev); + if (isChatboxOpen) { + // close the chatbox items + setIsChatboxOpen(false); + } + }, }), [ - isPinnedMenuOpen, - setIsPinnedMenuOpen, - isChatboxMenuOpen, - setIsChatboxMenuOpen, + isPinnedOpen, + setIsPinnedOpen, + isChatboxOpen, + setIsChatboxOpen, isFullscreen, setIsFullscreen, ], ); - return ( {children} ); diff --git a/src/langs/constants.ts b/src/langs/constants.ts index f2b56c311..a2c08d812 100644 --- a/src/langs/constants.ts +++ b/src/langs/constants.ts @@ -20,4 +20,6 @@ export const PLAYER = { FALLBACK_TITLE: 'FALLBACK_TITLE', FALLBACK_TEXT: 'FALLBACK_TEXT', FALLBACK_RELOAD_PAGE: 'FALLBACK_RELOAD_PAGE', + ITEM_ID_NOT_VALID: 'ITEM_ID_NOT_VALID', + GO_TO_HOME: 'GO_TO_HOME', }; diff --git a/src/langs/en.json b/src/langs/en.json index efcb62121..9d83f6a8d 100644 --- a/src/langs/en.json +++ b/src/langs/en.json @@ -18,5 +18,7 @@ "SIGN_IN_BUTTON_TEXT": "Sign In", "FALLBACK_TITLE": "Oops", "FALLBACK_TEXT": "Something went wrong. Please try again. If the issue persists contact us.", - "FALLBACK_RELOAD_PAGE": "Reload page" + "FALLBACK_RELOAD_PAGE": "Reload page", + "ITEM_ID_NOT_VALID": "Item Id is not valid, please check it again. Click on the button below to go back to Home.", + "GO_TO_HOME": "Go to Home" } diff --git a/src/modules/common/ItemCard.tsx b/src/modules/common/ItemCard.tsx index a8760d5c1..0141afcf6 100644 --- a/src/modules/common/ItemCard.tsx +++ b/src/modules/common/ItemCard.tsx @@ -11,7 +11,7 @@ import { ActionTriggers, DiscriminatedItem, formatDate } from '@graasp/sdk'; import { usePlayerTranslation } from '@/config/i18n'; import { mutations } from '@/config/queryClient'; -import { buildMainPath } from '../../config/paths'; +import { buildContentPagePath } from '../../config/paths'; import { HIDDEN_STYLE } from './HiddenWrapper'; import ItemThumbnail from './ItemThumbnail'; @@ -22,7 +22,7 @@ type Props = { const SimpleCard = ({ item, isHidden = false }: Props): JSX.Element => { const { i18n } = usePlayerTranslation(); - const link = buildMainPath({ rootId: item.id }); + const link = buildContentPagePath({ rootId: item.id, itemId: item.id }); const { mutate: triggerAction } = mutations.usePostItemAction(); const handleCardClick = () => { diff --git a/src/modules/item/Item.tsx b/src/modules/item/Item.tsx index d618f63f9..df1eb635a 100644 --- a/src/modules/item/Item.tsx +++ b/src/modules/item/Item.tsx @@ -60,7 +60,7 @@ import { useCurrentMemberContext } from '@/contexts/CurrentMemberContext'; import { PLAYER } from '@/langs/constants'; import { isHidden, paginationContentFilter } from '@/utils/item'; -import NavigationButton from './NavigationButton'; +import NavigationIsland from '../navigationIsland/NavigationIsland'; import PinnedFolderItem from './PinnedFolderItem'; const { @@ -231,7 +231,7 @@ const AppContent = ({ item }: { item: AppItemType }): JSX.Element => { contextPayload={{ apiHost: API_HOST, settings: item.settings, - lang: item.settings?.lang || member?.extra?.lang || DEFAULT_LANG, + lang: item.lang || member?.extra?.lang || DEFAULT_LANG, permission: PermissionLevel.Read, context: Context.Player, memberId: member?.id, @@ -490,9 +490,7 @@ const Item = ({ ))} {showLoadMoreButton} - {!hasNextPage && !isFetchingNextPage && ( - - )} + )} @@ -509,7 +507,12 @@ const Item = ({ } // executed when item is a single child that is not a folder - return ; + return ( + <> + + + + ); }; export default Item; diff --git a/src/modules/item/MainScreen.tsx b/src/modules/item/MainScreen.tsx index bdda21442..fae4f6061 100644 --- a/src/modules/item/MainScreen.tsx +++ b/src/modules/item/MainScreen.tsx @@ -3,9 +3,8 @@ import { useParams } from 'react-router-dom'; import { Alert, Skeleton, Typography } from '@mui/material'; -import { ROOT_ID_PATH } from '@/config/paths'; +import { ITEM_PARAM } from '@/config/paths'; import { hooks } from '@/config/queryClient'; -import { useItemContext } from '@/contexts/ItemContext'; import { LayoutContextProvider } from '@/contexts/LayoutContext'; import { PLAYER } from '@/langs/constants'; import SideContent from '@/modules/rightPanel/SideContent'; @@ -13,14 +12,12 @@ import SideContent from '@/modules/rightPanel/SideContent'; import Item from './Item'; const MainScreen = (): JSX.Element | null => { - const rootId = useParams()[ROOT_ID_PATH]; - const { focusedItemId } = useItemContext(); - const mainId = focusedItemId || rootId; - const { data: item, isLoading, isError } = hooks.useItem(mainId); + const itemId = useParams()[ITEM_PARAM]; + const { data: item, isLoading, isError } = hooks.useItem(itemId); const { t } = useTranslation(); - const content = rootId ? ( - + const content = itemId ? ( + ) : ( {t('No item defined.')} diff --git a/src/modules/item/NavigationButton.tsx b/src/modules/item/NavigationButton.tsx deleted file mode 100644 index c65029511..000000000 --- a/src/modules/item/NavigationButton.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useParams } from 'react-router'; - -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import { AppBar, Box, Toolbar } from '@mui/material'; - -import { ActionTriggers, DiscriminatedItem, ItemType } from '@graasp/sdk'; -import { Button } from '@graasp/ui'; - -import isArray from 'lodash.isarray'; - -import { hooks, mutations } from '@/config/queryClient'; -import { useItemContext } from '@/contexts/ItemContext'; - -const NavigationButton = ({ - item, -}: { - item: DiscriminatedItem; -}): JSX.Element | null => { - const { rootId } = useParams(); - const { setFocusedItemId } = useItemContext(); - const { mutate: triggerAction } = mutations.usePostItemAction(); - const { data: rootItem } = hooks.useItem(rootId); - - const { data: descendants, isLoading } = hooks.useDescendants({ - // not correct but enabled - id: rootId ?? '', - enabled: Boolean(rootId), - }); - - const prevRoot: DiscriminatedItem | null = rootItem || null; - let prev: DiscriminatedItem | null = null; - let next: DiscriminatedItem | null = null; - - // if there are no descendants then there is no need to navigate - if (!isArray(descendants)) { - return null; - } - - if (isLoading) { - return null; - } - - // we only navigate through folders - const folderHierarchy: DiscriminatedItem[] = descendants.filter( - ({ type }) => type === ItemType.FOLDER, - ); - - // when focusing on the root item - if (item.id === rootId && folderHierarchy.length) { - // there is no previous and the nex in the first item in the hierarchy - [next] = folderHierarchy; - // when focusing on the descendants - } else { - const idx = folderHierarchy.findIndex(({ id }) => id === item.id) ?? -1; - - // if index is not found, then do not show navigation - if (idx < 0) { - return null; - } - - // if index is 0, previous is root - prev = idx === 0 ? prevRoot : folderHierarchy[idx - 1]; - // if you reach the end, next will be undefined and not show - next = folderHierarchy[idx + 1]; - } - - const handleClickNavigationButton = (itemId: string) => { - triggerAction({ itemId, payload: { type: ActionTriggers.ItemView } }); - setFocusedItemId(itemId); - }; - - return ( - // z-index is 998 because cookie banner is 999 - - - {prev ? ( - - ) : ( -

- )} - - {next ? ( - - ) : ( -

- )} - - - ); -}; - -export default NavigationButton; diff --git a/src/modules/layout/PageWrapper.tsx b/src/modules/layout/PageWrapper.tsx index 6a89955f4..8db236bba 100644 --- a/src/modules/layout/PageWrapper.tsx +++ b/src/modules/layout/PageWrapper.tsx @@ -28,7 +28,7 @@ import { ItemContextProvider } from '@/contexts/ItemContext'; import { PLAYER } from '@/langs/constants'; import HomeNavigation from '../navigation/HomeNavigation'; -import ItemNavigation from '../navigation/ItemNavigation'; +import ItemStructureNavigation from '../navigation/ItemNavigation'; import UserSwitchWrapper from '../userSwitch/UserSwitchWrapper'; // small converter for HOST_MAP into a usePlatformNavigation mapper @@ -95,7 +95,9 @@ const PageWrapper = ({ fullscreen }: PageWrapperProps): JSX.Element => {

: } + drawerContent={ + rootId ? : + } drawerOpenAriaLabel={t(PLAYER.DRAWER_ARIAL_LABEL)} LinkComponent={LinkComponent} PlatformComponent={ diff --git a/src/modules/navigation/ItemNavigation.tsx b/src/modules/navigation/ItemNavigation.tsx index 3bf23d392..30db8a755 100644 --- a/src/modules/navigation/ItemNavigation.tsx +++ b/src/modules/navigation/ItemNavigation.tsx @@ -1,4 +1,4 @@ -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { Alert, Skeleton } from '@mui/material'; @@ -6,10 +6,9 @@ import { FAILURE_MESSAGES } from '@graasp/translations'; import { MainMenu } from '@graasp/ui'; import { useMessagesTranslation } from '@/config/i18n'; -import { ROOT_ID_PATH } from '@/config/paths'; +import { ROOT_ID_PATH, buildContentPagePath } from '@/config/paths'; import { axios, hooks } from '@/config/queryClient'; import { MAIN_MENU_ID, TREE_VIEW_ID } from '@/config/selectors'; -import { useItemContext } from '@/contexts/ItemContext'; import TreeView from '@/modules/navigation/tree/TreeView'; import { isHidden } from '@/utils/item'; @@ -17,15 +16,19 @@ const { useItem, useDescendants, useItemsTags } = hooks; const DrawerNavigation = (): JSX.Element | null => { const rootId = useParams()[ROOT_ID_PATH]; + const navigate = useNavigate(); const { t: translateMessage } = useMessagesTranslation(); - const { setFocusedItemId } = useItemContext(); const { data: descendants } = useDescendants({ id: rootId ?? '' }); const { data: itemsTags } = useItemsTags(descendants?.map(({ id }) => id)); const { data: rootItem, isLoading, isError, error } = useItem(rootId); + const handleNavigationOnClick = (newItemId: string) => { + navigate(buildContentPagePath({ rootId, itemId: newItemId })); + }; + if (rootItem) { return ( @@ -36,7 +39,7 @@ const DrawerNavigation = (): JSX.Element | null => { (ele) => !isHidden(ele, itemsTags?.data?.[ele.id]), )} firstLevelStyle={{ fontWeight: 'bold' }} - onTreeItemSelect={setFocusedItemId} + onTreeItemSelect={handleNavigationOnClick} /> ); diff --git a/src/modules/navigation/tree/TreeView.tsx b/src/modules/navigation/tree/TreeView.tsx index dcbe19b6f..21975b1fb 100644 --- a/src/modules/navigation/tree/TreeView.tsx +++ b/src/modules/navigation/tree/TreeView.tsx @@ -3,6 +3,7 @@ import AccessibleTreeView, { INodeRendererProps, flattenTree, } from 'react-accessible-treeview'; +import { useParams } from 'react-router-dom'; import { Box, SxProps, Typography } from '@mui/material'; import Skeleton from '@mui/material/Skeleton'; @@ -17,7 +18,6 @@ import { import { GRAASP_MENU_ITEMS } from '@/config/constants'; import { hooks, mutations } from '@/config/queryClient'; -import { useItemContext } from '@/contexts/ItemContext'; import { ItemMetaData, getItemTree } from '@/utils/tree'; import Node from './Node'; @@ -46,14 +46,12 @@ const TreeView = ({ sx = {}, }: Props): JSX.Element => { const { mutate: triggerAction } = mutations.usePostItemAction(); - + const { itemId } = useParams(); const itemsToShow = items?.filter((item) => onlyShowContainerItems ? GRAASP_MENU_ITEMS.includes(item.type) : true, ); - const { focusedItemId } = useItemContext(); - - const { data: focusedItem } = hooks.useItem(focusedItemId); + const { data: focusedItem } = hooks.useItem(itemId); if (isLoading) { return ; @@ -95,14 +93,14 @@ const TreeView = ({ const defaultExpandedIds = rootItems[0]?.id ? [rootItems[0].id] : []; - const selectedIds = focusedItemId ? [focusedItemId] : []; + const selectedIds = itemId ? [itemId] : []; const expandedIds = focusedItem ? getIdsFromPath(focusedItem.path) : defaultExpandedIds; // need to filter the expandedIds to only include items that are present in the tree // we should not include parents that are above the current player root - const availableItemIds = itemsToShow?.map(({ id: itemId }) => itemId); + const availableItemIds = itemsToShow?.map(({ id: elemId }) => elemId); // filter the items to expand to only keep the ones that are present in the tree. // if there are no items in the tree we short circuit the filtering const accessibleExpandedItems = availableItemIds?.length diff --git a/src/modules/navigationIsland/ChatButton.tsx b/src/modules/navigationIsland/ChatButton.tsx new file mode 100644 index 000000000..0b3434ab6 --- /dev/null +++ b/src/modules/navigationIsland/ChatButton.tsx @@ -0,0 +1,42 @@ +import { useParams } from 'react-router-dom'; + +import ChatIcon from '@mui/icons-material/Forum'; +import ChatClosedIcon from '@mui/icons-material/ForumOutlined'; +import { IconButton } from '@mui/material'; + +import { usePlayerTranslation } from '@/config/i18n'; +import { hooks } from '@/config/queryClient'; +import { ITEM_CHATBOX_BUTTON_ID } from '@/config/selectors'; +import { useLayoutContext } from '@/contexts/LayoutContext'; +import { PLAYER } from '@/langs/constants'; + +const useChatButton = (): { chatButton: JSX.Element | false } => { + const { t } = usePlayerTranslation(); + const { itemId } = useParams(); + const { data: item } = hooks.useItem(itemId); + const { toggleChatbox, isChatboxOpen } = useLayoutContext(); + + // do not show chatbox button is chatbox setting is not enabled + if (!item?.settings?.showChatbox) { + return { chatButton: false }; + } + + return { + chatButton: ( + + {isChatboxOpen ? : } + + ), + }; +}; +export default useChatButton; diff --git a/src/modules/navigationIsland/NavigationIsland.tsx b/src/modules/navigationIsland/NavigationIsland.tsx new file mode 100644 index 000000000..39dd86afe --- /dev/null +++ b/src/modules/navigationIsland/NavigationIsland.tsx @@ -0,0 +1,50 @@ +import { Box, Stack } from '@mui/material'; + +import useChatButton from './ChatButton'; +import usePinnedItemsButton from './PinnedItemsButton'; +import usePreviousNextButtons from './PreviousNextButtons'; + +const NavigationIslandBox = (): JSX.Element | null => { + const { previousButton, nextButton } = usePreviousNextButtons(); + const { chatButton } = useChatButton(); + const { pinnedButton } = usePinnedItemsButton(); + const navigationItems = [ + previousButton, + nextButton, + chatButton, + pinnedButton, + ].filter(Boolean); + + if (navigationItems.length) { + return ( + + + {navigationItems} + + + ); + } + return null; +}; +export default NavigationIslandBox; diff --git a/src/modules/navigationIsland/PinnedItemsButton.tsx b/src/modules/navigationIsland/PinnedItemsButton.tsx new file mode 100644 index 000000000..ec533382f --- /dev/null +++ b/src/modules/navigationIsland/PinnedItemsButton.tsx @@ -0,0 +1,50 @@ +import { useParams } from 'react-router-dom'; + +import PinIcon from '@mui/icons-material/PushPin'; +import OutlinedPinIcon from '@mui/icons-material/PushPinOutlined'; +import { IconButton } from '@mui/material'; + +import { ItemTagType } from '@graasp/sdk'; + +import { usePlayerTranslation } from '@/config/i18n'; +import { hooks } from '@/config/queryClient'; +import { ITEM_PINNED_BUTTON_ID } from '@/config/selectors'; +import { useLayoutContext } from '@/contexts/LayoutContext'; +import { PLAYER } from '@/langs/constants'; + +const usePinnedItemsButton = (): { pinnedButton: JSX.Element | false } => { + const { t } = usePlayerTranslation(); + const { togglePinned, isPinnedOpen } = useLayoutContext(); + const { itemId } = useParams(); + const { data: children } = hooks.useChildren(itemId); + const { data: tags } = hooks.useItemsTags(children?.map(({ id }) => id)); + const pinnedCount = + children?.filter( + ({ id, settings: s }) => + s.isPinned && + // do not count hidden items as they are not displayed + !tags?.data?.[id].some(({ type }) => type === ItemTagType.Hidden), + )?.length || 0; + + if (pinnedCount > 0) { + return { + pinnedButton: ( + + {isPinnedOpen ? : } + + ), + }; + } + return { pinnedButton: false }; +}; +export default usePinnedItemsButton; diff --git a/src/modules/navigationIsland/PreviousNextButtons.tsx b/src/modules/navigationIsland/PreviousNextButtons.tsx new file mode 100644 index 000000000..78c245f4e --- /dev/null +++ b/src/modules/navigationIsland/PreviousNextButtons.tsx @@ -0,0 +1,112 @@ +import { useNavigate, useParams } from 'react-router-dom'; + +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; +import { Button } from '@mui/material'; + +import { ActionTriggers, DiscriminatedItem, ItemType } from '@graasp/sdk'; + +import isArray from 'lodash.isarray'; + +import { buildContentPagePath } from '@/config/paths'; +import { hooks, mutations } from '@/config/queryClient'; + +const usePreviousNextButtons = (): { + previousButton: JSX.Element | false; + nextButton: JSX.Element | false; +} => { + const { rootId, itemId } = useParams(); + const navigate = useNavigate(); + const { mutate: triggerAction } = mutations.usePostItemAction(); + const { data: rootItem } = hooks.useItem(rootId); + + const { data: descendants, isLoading } = hooks.useDescendants({ + // not correct but enabled + id: rootId ?? '', + enabled: Boolean(rootId), + }); + + const prevRoot: DiscriminatedItem | null = rootItem || null; + let prev: DiscriminatedItem | null = null; + let next: DiscriminatedItem | null = null; + + // if there are no descendants then there is no need to navigate + if (!isArray(descendants)) { + return { previousButton: false, nextButton: false }; + } + + if (isLoading) { + return { previousButton: false, nextButton: false }; + } + + // we only navigate through folders + const folderHierarchy: DiscriminatedItem[] = descendants.filter( + ({ type }) => type === ItemType.FOLDER, + ); + + // when focusing on the root item + if (itemId === rootId && folderHierarchy.length) { + // there is no previous and the nex in the first item in the hierarchy + [next] = folderHierarchy; + // when focusing on the descendants + } else { + const idx = folderHierarchy.findIndex(({ id }) => id === itemId) ?? -1; + + // if index is not found, then do not show navigation + if (idx < 0) { + return { previousButton: false, nextButton: false }; + } + + // if index is 0, previous is root + prev = idx === 0 ? prevRoot : folderHierarchy[idx - 1]; + // if you reach the end, next will be undefined and not show + next = folderHierarchy[idx + 1]; + } + + const handleClickNavigationButton = (newItemId: string) => { + triggerAction({ + itemId: newItemId, + payload: { type: ActionTriggers.ItemView }, + }); + navigate(buildContentPagePath({ rootId, itemId: newItemId })); + }; + + if (!prev && !next) { + return { previousButton: false, nextButton: false }; + } + + return { + previousButton: prev != null && ( + + ), + + nextButton: next != null && ( + + ), + }; +}; +export default usePreviousNextButtons; diff --git a/src/modules/pages/ItemPage.tsx b/src/modules/pages/ItemPage.tsx index d47728568..9245a948e 100644 --- a/src/modules/pages/ItemPage.tsx +++ b/src/modules/pages/ItemPage.tsx @@ -2,7 +2,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { ItemLoginAuthorization } from '@graasp/ui'; -import { HOME_PATH, ROOT_ID_PATH } from '@/config/paths'; +import { HOME_PATH, ITEM_PARAM } from '@/config/paths'; import { hooks, mutations } from '@/config/queryClient'; import PlayerCookiesBanner from '@/modules/cookies/PlayerCookiesBanner'; @@ -23,13 +23,13 @@ const { usePostItemLogin } = mutations; const ItemPage = (): JSX.Element | null => { const { mutate: itemLoginSignIn } = usePostItemLogin(); - const rootId = useParams()[ROOT_ID_PATH]; + const itemId = useParams()[ITEM_PARAM]; const navigate = useNavigate(); const ForbiddenContent = ; - if (!rootId) { + if (!itemId) { navigate(HOME_PATH); return null; } @@ -37,7 +37,7 @@ const ItemPage = (): JSX.Element | null => { const Component = ItemLoginAuthorization({ signIn: itemLoginSignIn, // this is because the itemId can not be undefined in ui - itemId: rootId, + itemId, useCurrentMember, useItem, ForbiddenContent, diff --git a/src/modules/rightPanel/SideContent.tsx b/src/modules/rightPanel/SideContent.tsx index 34e71284b..f4edca58e 100644 --- a/src/modules/rightPanel/SideContent.tsx +++ b/src/modules/rightPanel/SideContent.tsx @@ -1,10 +1,8 @@ import Fullscreen from 'react-fullscreen-crossbrowser'; -import { useSearchParams } from 'react-router-dom'; +import { useParams, useSearchParams } from 'react-router-dom'; -import ForumIcon from '@mui/icons-material/Forum'; import EnterFullscreenIcon from '@mui/icons-material/Fullscreen'; import ExitFullscreenIcon from '@mui/icons-material/FullscreenExit'; -import PushPinIcon from '@mui/icons-material/PushPin'; import { Grid, Stack, Tooltip, styled } from '@mui/material'; import IconButton from '@mui/material/IconButton'; @@ -17,7 +15,6 @@ import { import { usePlayerTranslation } from '@/config/i18n'; import { hooks } from '@/config/queryClient'; -import { useItemContext } from '@/contexts/ItemContext'; import { useLayoutContext } from '@/contexts/LayoutContext'; import { PLAYER } from '@/langs/constants'; import Chatbox from '@/modules/chatbox/Chatbox'; @@ -26,9 +23,7 @@ import Item from '@/modules/item/Item'; import { DRAWER_WIDTH, FLOATING_BUTTON_Z_INDEX } from '../../config/constants'; import { CHATBOX_DRAWER_ID, - ITEM_CHATBOX_BUTTON_ID, ITEM_FULLSCREEN_BUTTON_ID, - ITEM_PINNED_BUTTON_ID, ITEM_PINNED_ID, } from '../../config/selectors'; import SideDrawer from './SideDrawer'; @@ -70,15 +65,16 @@ type Props = { }; const SideContent = ({ content, item }: Props): JSX.Element | null => { - const { descendants, rootId } = useItemContext(); - const { data: tags } = hooks.useItemsTags(descendants?.map(({ id }) => id)); + const { itemId, rootId } = useParams(); + const { data: children } = hooks.useChildren(itemId); + const { data: tags } = hooks.useItemsTags(children?.map(({ id }) => id)); const [searchParams] = useSearchParams(); const { - isPinnedMenuOpen, - setIsPinnedMenuOpen, - isChatboxMenuOpen, - setIsChatboxMenuOpen, + toggleChatbox, + togglePinned, + isChatboxOpen, + isPinnedOpen, isFullscreen, setIsFullscreen, } = useLayoutContext(); @@ -106,47 +102,17 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => { ); const pinnedCount = - descendants?.filter( + children?.filter( ({ id, settings: s }) => s.isPinned && // do not count hidden items as they are not displayed - !tags?.data?.[id].some(({ type }) => type === ItemTagType.Hidden), + !tags?.data?.[id]?.some(({ type }) => type === ItemTagType.Hidden), )?.length || 0; - const toggleChatOpen = () => { - setIsChatboxMenuOpen(!isChatboxMenuOpen); - setIsPinnedMenuOpen(false); - }; - - const togglePinnedOpen = () => { - setIsPinnedMenuOpen(!isPinnedMenuOpen); - setIsChatboxMenuOpen(false); - }; - const toggleFullscreen = () => { setIsFullscreen(!isFullscreen); }; - const displayPinButton = () => { - if (!pinnedCount) return null; - - return ( - - - - - - ); - }; - const displayFullscreenButton = () => { // todo: add this to settings (?) const fullscreen = Boolean(searchParams.get('fullscreen') === 'true'); @@ -175,26 +141,6 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => { ); }; - const displayChatButton = () => { - if (!settings?.showChatbox) return null; - - return ( - - - - - - ); - }; - const displayChatbox = () => { if (!settings?.showChatbox) return null; @@ -202,8 +148,8 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => {
@@ -217,8 +163,8 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => { return ( {/* show parents pinned items */} @@ -239,14 +185,10 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => { {displayPinnedItems()} 0)} + isShifted={isChatboxOpen || (isPinnedOpen && pinnedCount > 0)} > {displayFullscreenButton()} - {displayChatButton()} - - {displayPinButton()} - {content} diff --git a/vite.config.ts b/vite.config.ts index bf8737717..e08c2625a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -18,7 +18,7 @@ export default ({ mode }: { mode: string }): UserConfigExport => { base: '/', server: { port: parseInt(process.env.VITE_PORT, 10), - open: true, + open: mode === 'development', watch: { ignored: ['**/coverage/**'], }, diff --git a/yarn.lock b/yarn.lock index f29055555..1c5bebfd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -68,6 +68,16 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.24.1, @babel/code-frame@npm:^7.24.2": + version: 7.24.2 + resolution: "@babel/code-frame@npm:7.24.2" + dependencies: + "@babel/highlight": "npm:^7.24.2" + picocolors: "npm:^1.0.0" + checksum: 10/7db8f5b36ffa3f47a37f58f61e3d130b9ecad21961f3eede7e2a4ac2c7e4a5efb6e9d03a810c669bc986096831b6c0dfc2c3082673d93351b82359c1b03e0590 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.23.5": version: 7.23.5 resolution: "@babel/compat-data@npm:7.23.5" @@ -75,7 +85,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.20.12, @babel/core@npm:^7.23.5, @babel/core@npm:^7.7.5": +"@babel/core@npm:^7.18.9, @babel/core@npm:^7.20.12, @babel/core@npm:^7.23.5, @babel/core@npm:^7.7.5": version: 7.23.9 resolution: "@babel/core@npm:7.23.9" dependencies: @@ -98,6 +108,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.23.9": + version: 7.24.3 + resolution: "@babel/core@npm:7.24.3" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.24.2" + "@babel/generator": "npm:^7.24.1" + "@babel/helper-compilation-targets": "npm:^7.23.6" + "@babel/helper-module-transforms": "npm:^7.23.3" + "@babel/helpers": "npm:^7.24.1" + "@babel/parser": "npm:^7.24.1" + "@babel/template": "npm:^7.24.0" + "@babel/traverse": "npm:^7.24.1" + "@babel/types": "npm:^7.24.0" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10/3a7b9931fe0d93c500dcdb6b36f038b0f9d5090c048818e62aa8321c8f6e8ccc3d47373f0b40591c1fe3b13e5096bacabb1ade83f9f4d86f57878c39a9d1ade1 + languageName: node + linkType: hard + "@babel/generator@npm:7.17.7": version: 7.17.7 resolution: "@babel/generator@npm:7.17.7" @@ -121,6 +154,18 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/generator@npm:7.24.1" + dependencies: + "@babel/types": "npm:^7.24.0" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^2.5.1" + checksum: 10/c6160e9cd63d7ed7168dee27d827f9c46fab820c45861a5df56cd5c78047f7c3fc97c341e9ccfa1a6f97c87ec2563d9903380b5f92794e3540a6c5f99eb8f075 + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.23.6": version: 7.23.6 resolution: "@babel/helper-compilation-targets@npm:7.23.6" @@ -241,6 +286,17 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/helpers@npm:7.24.1" + dependencies: + "@babel/template": "npm:^7.24.0" + "@babel/traverse": "npm:^7.24.1" + "@babel/types": "npm:^7.24.0" + checksum: 10/82d3cdd3beafc4583f237515ef220bc205ced8b0540c6c6e191fc367a9589bd7304b8f9800d3d7574d4db9f079bd555979816b1874c86e53b3e7dd2032ad6c7c + languageName: node + linkType: hard + "@babel/highlight@npm:^7.23.4": version: 7.23.4 resolution: "@babel/highlight@npm:7.23.4" @@ -252,7 +308,19 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.5, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9": +"@babel/highlight@npm:^7.24.2": + version: 7.24.2 + resolution: "@babel/highlight@npm:7.24.2" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.22.20" + chalk: "npm:^2.4.2" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.0.0" + checksum: 10/4555124235f34403bb28f55b1de58edf598491cc181c75f8afc8fe529903cb598cd52fe3bf2faab9bc1f45c299681ef0e44eea7a848bb85c500c5a4fe13f54f6 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.5, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9": version: 7.23.9 resolution: "@babel/parser@npm:7.23.9" bin: @@ -261,6 +329,15 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.24.0, @babel/parser@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/parser@npm:7.24.1" + bin: + parser: ./bin/babel-parser.js + checksum: 10/561d9454091e07ecfec3828ce79204c0fc9d24e17763f36181c6984392be4ca6b79c8225f2224fdb7b1b3b70940e243368c8f83ac77ec2dc20f46d3d06bd6795 + languageName: node + linkType: hard + "@babel/plugin-transform-react-jsx-self@npm:^7.18.6, @babel/plugin-transform-react-jsx-self@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-react-jsx-self@npm:7.23.3" @@ -312,6 +389,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/template@npm:7.24.0" + dependencies: + "@babel/code-frame": "npm:^7.23.5" + "@babel/parser": "npm:^7.24.0" + "@babel/types": "npm:^7.24.0" + checksum: 10/8c538338c7de8fac8ada691a5a812bdcbd60bd4a4eb5adae2cc9ee19773e8fb1a724312a00af9e1ce49056ffd3c3475e7287b5668cf6360bfb3f8ac827a06ffe + languageName: node + linkType: hard + "@babel/traverse@npm:7.23.2": version: 7.23.2 resolution: "@babel/traverse@npm:7.23.2" @@ -348,6 +436,24 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.24.1": + version: 7.24.1 + resolution: "@babel/traverse@npm:7.24.1" + dependencies: + "@babel/code-frame": "npm:^7.24.1" + "@babel/generator": "npm:^7.24.1" + "@babel/helper-environment-visitor": "npm:^7.22.20" + "@babel/helper-function-name": "npm:^7.23.0" + "@babel/helper-hoist-variables": "npm:^7.22.5" + "@babel/helper-split-export-declaration": "npm:^7.22.6" + "@babel/parser": "npm:^7.24.1" + "@babel/types": "npm:^7.24.0" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/b9b0173c286ef549e179f3725df3c4958069ad79fe5b9840adeb99692eb4a5a08db4e735c0f086aab52e7e08ec711cee9e7c06cb908d8035641d1382172308d3 + languageName: node + linkType: hard + "@babel/types@npm:7.17.0": version: 7.17.0 resolution: "@babel/types@npm:7.17.0" @@ -369,6 +475,17 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.24.0": + version: 7.24.0 + resolution: "@babel/types@npm:7.24.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.23.4" + "@babel/helper-validator-identifier": "npm:^7.22.20" + to-fast-properties: "npm:^2.0.0" + checksum: 10/a0b4875ce2e132f9daff0d5b27c7f4c4fcc97f2b084bdc5834e92c9d32592778489029e65d99d00c406da612d87b72d7a236c0afccaa1435c028d0c94c9b6da4 + languageName: node + linkType: hard + "@base2/pretty-print-object@npm:1.0.1": version: 1.0.1 resolution: "@base2/pretty-print-object@npm:1.0.1" @@ -383,20 +500,20 @@ __metadata: languageName: node linkType: hard -"@commitlint/cli@npm:^19.2.0": - version: 19.2.0 - resolution: "@commitlint/cli@npm:19.2.0" +"@commitlint/cli@npm:^19.2.1": + version: 19.2.1 + resolution: "@commitlint/cli@npm:19.2.1" dependencies: "@commitlint/format": "npm:^19.0.3" "@commitlint/lint": "npm:^19.1.0" "@commitlint/load": "npm:^19.2.0" - "@commitlint/read": "npm:^19.2.0" + "@commitlint/read": "npm:^19.2.1" "@commitlint/types": "npm:^19.0.3" execa: "npm:^8.0.1" yargs: "npm:^17.0.0" bin: commitlint: cli.js - checksum: 10/7b40dacb664cb57c8c6d93d589bf51a26ea9eaab3d0925be24ef2d2d299175c7282c9425027c827381dc05ea07bed59941fa46dd5dcbab2a6d6ea25c04d2c187 + checksum: 10/6d3555039c96e21664d5159b06317558d31ca150f3326a2bd75aa82335032956c8f09481bf30b3aa3a2f8a2037b0a8e1947a787d57f4cb2007e3f69814e9c31f languageName: node linkType: hard @@ -509,16 +626,16 @@ __metadata: languageName: node linkType: hard -"@commitlint/read@npm:^19.2.0": - version: 19.2.0 - resolution: "@commitlint/read@npm:19.2.0" +"@commitlint/read@npm:^19.2.1": + version: 19.2.1 + resolution: "@commitlint/read@npm:19.2.1" dependencies: "@commitlint/top-level": "npm:^19.0.0" "@commitlint/types": "npm:^19.0.3" execa: "npm:^8.0.1" git-raw-commits: "npm:^4.0.0" minimist: "npm:^1.2.8" - checksum: 10/5f8cbaf018459e61f12b84e942078dc6c4afc55025b1cee4c28ea5e87bb7947402b493c0ec4b6eff08fa7494a20e6d32a76a6f7349a9a89e292c7969324a69c2 + checksum: 10/840ebd183b2fe36dea03701552d825a9a1770d300b9416ab2a731fdeed66cf8c9dd8be133d92ac017cb9bf29e2ef5aee91a641f2b643bb5b33005f7b392ec953 languageName: node linkType: hard @@ -575,9 +692,9 @@ __metadata: languageName: node linkType: hard -"@cypress/code-coverage@npm:3.12.26": - version: 3.12.26 - resolution: "@cypress/code-coverage@npm:3.12.26" +"@cypress/code-coverage@npm:3.12.30": + version: 3.12.30 + resolution: "@cypress/code-coverage@npm:3.12.30" dependencies: "@cypress/webpack-preprocessor": "npm:^6.0.0" chalk: "npm:4.1.2" @@ -594,7 +711,7 @@ __metadata: babel-loader: ^8.3 || ^9 cypress: "*" webpack: ^4 || ^5 - checksum: 10/10d74acb89ac3ef533f3d8dd0194a11265cf29a3bb91b9ab7d63ec349c1d8f0c3e598ebb1f5b64f1718dd087e770014188f99e009ba117fff71380842584969f + checksum: 10/3ddac4c971e0989df0428175f452211941df922327a7a9ab7827576528e25ce4e9d5a8e9bb745fe98e0ccad6b9ee88ec7c1dd277f25b114db72a0086581c68a2 languageName: node linkType: hard @@ -1389,21 +1506,20 @@ __metadata: languageName: node linkType: hard -"@graasp/query-client@npm:2.7.1": - version: 2.7.1 - resolution: "@graasp/query-client@npm:2.7.1" +"@graasp/query-client@npm:3.0.1": + version: 3.0.1 + resolution: "@graasp/query-client@npm:3.0.1" dependencies: "@tanstack/react-query": "npm:4.36.1" "@tanstack/react-query-devtools": "npm:4.36.1" - axios: "npm:1.6.7" - crypto-js: "npm:4.2.0" + axios: "npm:1.6.8" http-status-codes: "npm:2.3.0" - qs: "npm:6.11.2" + qs: "npm:6.12.0" peerDependencies: "@graasp/sdk": ^4.0.0 "@graasp/translations": ^1.23.0 react: ^18.0.0 - checksum: 10/7204cbc07cc4e2a5110bca2e5c6124d5998ca69fddb1d0d48e5f7ce397884586a520d6e8d1d09ad967cbb01d395cd1bfff46207434dd815cd01c0d62a0b6088e + checksum: 10/f4a343d47157e1d79fdbfb423e38d8670e8b289528b30671b6cfafa87f9d99c116fdb385e46b75bcd8cb3e6fa4c258a15ec1c542a9d47ba8ca4b02b2a3eae045 languageName: node linkType: hard @@ -1520,7 +1636,7 @@ __metadata: languageName: node linkType: hard -"@istanbuljs/schema@npm:^0.1.2": +"@istanbuljs/schema@npm:^0.1.2, @istanbuljs/schema@npm:^0.1.3": version: 0.1.3 resolution: "@istanbuljs/schema@npm:0.1.3" checksum: 10/a9b1e49acdf5efc2f5b2359f2df7f90c5c725f2656f16099e8b2cd3a000619ecca9fc48cf693ba789cf0fd989f6e0df6a22bc05574be4223ecdbb7997d04384b @@ -1565,6 +1681,17 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" + dependencies: + "@jridgewell/set-array": "npm:^1.2.1" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10/81587b3c4dd8e6c60252122937cea0c637486311f4ed208b52b62aae2e7a87598f63ec330e6cd0984af494bfb16d3f0d60d3b21d7e5b4aedd2602ff3fe9d32e2 + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -1579,6 +1706,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 10/832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 + languageName: node + linkType: hard + "@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.13, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15": version: 1.4.15 resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" @@ -1596,6 +1730,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10/dced32160a44b49d531b80a4a2159dceab6b3ddf0c8e95a0deae4b0e894b172defa63d5ac52a19c2068e1fe7d31ea4ba931fbeec103233ecb4208953967120fc + languageName: node + linkType: hard + "@mui/base@npm:5.0.0-beta.22": version: 5.0.0-beta.22 resolution: "@mui/base@npm:5.0.0-beta.22" @@ -1618,14 +1762,14 @@ __metadata: languageName: node linkType: hard -"@mui/base@npm:5.0.0-beta.37": - version: 5.0.0-beta.37 - resolution: "@mui/base@npm:5.0.0-beta.37" +"@mui/base@npm:5.0.0-beta.40": + version: 5.0.0-beta.40 + resolution: "@mui/base@npm:5.0.0-beta.40" dependencies: "@babel/runtime": "npm:^7.23.9" "@floating-ui/react-dom": "npm:^2.0.8" - "@mui/types": "npm:^7.2.13" - "@mui/utils": "npm:^5.15.11" + "@mui/types": "npm:^7.2.14" + "@mui/utils": "npm:^5.15.14" "@popperjs/core": "npm:^2.11.8" clsx: "npm:^2.1.0" prop-types: "npm:^15.8.1" @@ -1636,20 +1780,20 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/28ac58e46ecf7d43fec77f501604e16687df499c82ace9cb32228623cf2549ebf2b74815aacae2920ccb2feed5402c68ad8c599b1bdc717fa5c4caaa408e9c0a + checksum: 10/ebee3d9e1136710dcb2af5828acc6bd8d54f6b124785d011585c2665a48dc66e35ccb344d5ebc7fd8bfd776cccb8ea434911f151a62bee193677ee9dc67fc7fc languageName: node linkType: hard -"@mui/core-downloads-tracker@npm:^5.15.11": - version: 5.15.11 - resolution: "@mui/core-downloads-tracker@npm:5.15.11" - checksum: 10/7b6b9dc9fbe63e80cd0de85db73eb397031c8e60fbfc4fcd9d6c396f9222c1467bfb2bbe817973e6090576a0016fb0189b4a8ccee3e42210ace99efb5ace52d3 +"@mui/core-downloads-tracker@npm:^5.15.14": + version: 5.15.14 + resolution: "@mui/core-downloads-tracker@npm:5.15.14" + checksum: 10/0a1c63d906af594d0a7fb63d1d574482b3916351ea8908e8621c8bfa16ac38cf4edb5a334f0e28084f583ac928b587cab6e031f992195e0a590186faba13b9a5 languageName: node linkType: hard -"@mui/icons-material@npm:5.15.11": - version: 5.15.11 - resolution: "@mui/icons-material@npm:5.15.11" +"@mui/icons-material@npm:5.15.14": + version: 5.15.14 + resolution: "@mui/icons-material@npm:5.15.14" dependencies: "@babel/runtime": "npm:^7.23.9" peerDependencies: @@ -1659,7 +1803,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/2b337aa7c39e8e75cffd92742aaf7e1a8196597b9e3285322cd3aa3dc7c7f0142ac00d768affaf3a8c7eeab7fd3535de70419a6a8b599fdfa00d65323b6982f8 + checksum: 10/a5033b67d4ff455f5fdd91fc51d26d967d634e861cde194b9dde02a8cc3f557d1b3f7e0b3175bc654b8e944f2118d46620485734ecd9d2ed4a6f748386447933 languageName: node linkType: hard @@ -1693,16 +1837,16 @@ __metadata: languageName: node linkType: hard -"@mui/material@npm:5.15.11": - version: 5.15.11 - resolution: "@mui/material@npm:5.15.11" +"@mui/material@npm:5.15.14": + version: 5.15.14 + resolution: "@mui/material@npm:5.15.14" dependencies: "@babel/runtime": "npm:^7.23.9" - "@mui/base": "npm:5.0.0-beta.37" - "@mui/core-downloads-tracker": "npm:^5.15.11" - "@mui/system": "npm:^5.15.11" - "@mui/types": "npm:^7.2.13" - "@mui/utils": "npm:^5.15.11" + "@mui/base": "npm:5.0.0-beta.40" + "@mui/core-downloads-tracker": "npm:^5.15.14" + "@mui/system": "npm:^5.15.14" + "@mui/types": "npm:^7.2.14" + "@mui/utils": "npm:^5.15.14" "@types/react-transition-group": "npm:^4.4.10" clsx: "npm:^2.1.0" csstype: "npm:^3.1.3" @@ -1722,16 +1866,16 @@ __metadata: optional: true "@types/react": optional: true - checksum: 10/1f95143a9704829179c504404449994cd4c5bcdb6ea536bd15a85113a92874c6ecdbd2cf18df46a2982d98c6855e2d1a9198ea53a059abb02a7411eaa1c630ec + checksum: 10/a2c3355b39b86472bf2debb84d6c032b1ea4ba691fbda0f25803f2702f9106130bb85a7d2757545ce97540fe185f07cf24574d5786a29df26baa298ff7db063b languageName: node linkType: hard -"@mui/private-theming@npm:^5.15.11": - version: 5.15.11 - resolution: "@mui/private-theming@npm:5.15.11" +"@mui/private-theming@npm:^5.15.14": + version: 5.15.14 + resolution: "@mui/private-theming@npm:5.15.14" dependencies: "@babel/runtime": "npm:^7.23.9" - "@mui/utils": "npm:^5.15.11" + "@mui/utils": "npm:^5.15.14" prop-types: "npm:^15.8.1" peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -1739,7 +1883,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/ec185f586586bb1460cf93ebe82cf7bc0b62822d70e5836d95fa50e1525ce84c32b937ce005a32226bc9bab45c3763cb2865c503eac7c96bb98a58498b2d64f5 + checksum: 10/6a14311ed53ee4adccfe0ba93275b43773d22fdd10c0d4ba680b9368fc0616a5e0f38f29d2080bcd7e4ed79123047e5f245c403d3fd822e960a97762be65218d languageName: node linkType: hard @@ -1760,9 +1904,9 @@ __metadata: languageName: node linkType: hard -"@mui/styled-engine@npm:^5.15.11": - version: 5.15.11 - resolution: "@mui/styled-engine@npm:5.15.11" +"@mui/styled-engine@npm:^5.15.14": + version: 5.15.14 + resolution: "@mui/styled-engine@npm:5.15.14" dependencies: "@babel/runtime": "npm:^7.23.9" "@emotion/cache": "npm:^11.11.0" @@ -1777,7 +1921,7 @@ __metadata: optional: true "@emotion/styled": optional: true - checksum: 10/fedbb9891abd633e5072d30aae7405cec9e5e22ac63c9e117c49ddb66e86ec7baaed58f934efc7847ea86cc856a8c9a9ec5a08cd0072a7850184321a968704ad + checksum: 10/2a5e03bb20502aef94cfb908898c50abb769192deb32d7f4237039683ce5266104cdc4055a7f0a8342aa62447d52b7439a4f2d0dda0fa6709c227c3621468cab languageName: node linkType: hard @@ -1830,15 +1974,15 @@ __metadata: languageName: node linkType: hard -"@mui/system@npm:^5.15.11": - version: 5.15.11 - resolution: "@mui/system@npm:5.15.11" +"@mui/system@npm:^5.15.14": + version: 5.15.14 + resolution: "@mui/system@npm:5.15.14" dependencies: "@babel/runtime": "npm:^7.23.9" - "@mui/private-theming": "npm:^5.15.11" - "@mui/styled-engine": "npm:^5.15.11" - "@mui/types": "npm:^7.2.13" - "@mui/utils": "npm:^5.15.11" + "@mui/private-theming": "npm:^5.15.14" + "@mui/styled-engine": "npm:^5.15.14" + "@mui/types": "npm:^7.2.14" + "@mui/utils": "npm:^5.15.14" clsx: "npm:^2.1.0" csstype: "npm:^3.1.3" prop-types: "npm:^15.8.1" @@ -1854,7 +1998,7 @@ __metadata: optional: true "@types/react": optional: true - checksum: 10/004e64a558e6d75ab0036f65555459f0769b9ab8b50aecd583a9a41a0db5358168c3bd9f4146848dec4ececfedd6f5af11f519ba3f7bd2e28224f5487a1eef81 + checksum: 10/64a9eac1bebefad3042cce28a75d0af2828aa71acd4c32fb0267f5e68bc75b16a093b6fb30709db83ec32130f14f1d67c1c27457ef62733e54a9d04f9b027cee languageName: node linkType: hard @@ -1870,6 +2014,18 @@ __metadata: languageName: node linkType: hard +"@mui/types@npm:^7.2.14": + version: 7.2.14 + resolution: "@mui/types@npm:7.2.14" + peerDependencies: + "@types/react": ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/b10cca8f63ea522be4f7c185acd1f4d031947e53824cbf9dc5649c165bcfa8a2749e83fd0bd1809b8e2698f58638ab2b4ce03550095989189d14434ea5c6c0b6 + languageName: node + linkType: hard + "@mui/utils@npm:^5.14.16, @mui/utils@npm:^5.14.3, @mui/utils@npm:^5.15.9": version: 5.15.9 resolution: "@mui/utils@npm:5.15.9" @@ -1888,9 +2044,9 @@ __metadata: languageName: node linkType: hard -"@mui/utils@npm:^5.15.11": - version: 5.15.11 - resolution: "@mui/utils@npm:5.15.11" +"@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" @@ -1902,7 +2058,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/a3c3862a93eb646ddd212c19dee44bef4bee9232fc463a0b27ffc79b0e41a6c4b09b152953156c7ca458b1856dddd0cc4febc078e2151574e3df62868504fb59 + checksum: 10/b3cbe2d0aa7ec65969752dababc39fc6e0b8bb1a9cf8b9bac42ca40e3dd3eaa59b79765bd259019318acc7421d64b9f421bc67e776a581d7c9da6a1c0c50bfbc languageName: node linkType: hard @@ -2855,7 +3011,7 @@ __metadata: languageName: node linkType: hard -"@types/lodash.isarray@npm:^4": +"@types/lodash.isarray@npm:^4.0.9": version: 4.0.9 resolution: "@types/lodash.isarray@npm:4.0.9" dependencies: @@ -2945,12 +3101,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^20.11.20": - version: 20.11.20 - resolution: "@types/node@npm:20.11.20" +"@types/node@npm:^20.11.30": + version: 20.11.30 + resolution: "@types/node@npm:20.11.30" dependencies: undici-types: "npm:~5.26.4" - checksum: 10/ff449bdc94810dadb54e0f77dd587c6505ef79ffa5a208c16eb29b223365b188f4c935a3abaf0906a01d05257c3da1f72465594a841d35bcf7b6deac7a6938fb + checksum: 10/78515bc768d2b878e2e06a1c20eb4f5840072b79b8d28ff3dd0a7feaaf48fd3a2ac03cfa5bc7564da82db5906b43e9ba0e5df9f43d870b7aae2942306e208815 languageName: node linkType: hard @@ -3005,12 +3161,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.2.19": - version: 18.2.19 - resolution: "@types/react-dom@npm:18.2.19" +"@types/react-dom@npm:^18.2.22": + version: 18.2.22 + resolution: "@types/react-dom@npm:18.2.22" dependencies: "@types/react": "npm:*" - checksum: 10/98eb760ce78f1016d97c70f605f0b1a53873a548d3c2192b40c897f694fd9c8bb12baeada16581a9c7b26f5022c1d2613547be98284d8f1b82d1611b1e3e7df0 + checksum: 10/310da22244c1bb65a7f213f8727bda821dd211cfb2dd62d1f9b28dd50ef1c196d59e908494bd5f25c13a3844343f3a6135f39fb830aca6f79646fa56c1b56c08 languageName: node linkType: hard @@ -3034,14 +3190,14 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:18.2.60": - version: 18.2.60 - resolution: "@types/react@npm:18.2.60" +"@types/react@npm:18.2.67": + version: 18.2.67 + resolution: "@types/react@npm:18.2.67" dependencies: "@types/prop-types": "npm:*" "@types/scheduler": "npm:*" csstype: "npm:^3.0.2" - checksum: 10/5f2f6091623f13375a5bbc7e5c222cd212b5d6366ead737b76c853f6f52b314db24af5ae3f688d2d49814c668c216858a75433f145311839d8989d46bb3cbecf + checksum: 10/d7e248dbe8d9d3b05f0d8e128d615fc9c85aa2c5d15634271d20cb9b343dbeffb0875f31a44e7ac63b42afc25949bd4c3633b7ebee45ee4666591ca934a8dffb languageName: node linkType: hard @@ -3859,14 +4015,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.6.7": - version: 1.6.7 - resolution: "axios@npm:1.6.7" +"axios@npm:1.6.8": + version: 1.6.8 + resolution: "axios@npm:1.6.8" dependencies: - follow-redirects: "npm:^1.15.4" + follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 10/a1932b089ece759cd261f175d9ebf4d41c8994cf0c0767cda86055c7a19bcfdade8ae3464bf4cec4c8b142f4a657dc664fb77a41855e8376cf38b86d7a86518f + checksum: 10/3f9a79eaf1d159544fca9576261ff867cbbff64ed30017848e4210e49f3b01e97cf416390150e6fdf6633f336cd43dc1151f890bbd09c3c01ad60bb0891eee63 languageName: node linkType: hard @@ -4432,15 +4588,15 @@ __metadata: languageName: node linkType: hard -"commitlint@npm:19.2.0": - version: 19.2.0 - resolution: "commitlint@npm:19.2.0" +"commitlint@npm:19.2.1": + version: 19.2.1 + resolution: "commitlint@npm:19.2.1" dependencies: - "@commitlint/cli": "npm:^19.2.0" + "@commitlint/cli": "npm:^19.2.1" "@commitlint/types": "npm:^19.0.3" bin: commitlint: cli.js - checksum: 10/1e2c56431a3062f4066f3aeb5deab5a2da19f09fb4e867f17a1522df06c06786be03fc2a9efa099cbac212860b1246705f56ca6ff89ac283c473bd952a1077bf + checksum: 10/920349607ba67b689adbe227098b4a857df7d498a7a3a586e45277d1a6d0bb3114e96e339ebeb64609417956497d65dba145993fdb762d91a75a26a5355ec09c languageName: node linkType: hard @@ -4648,13 +4804,6 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:4.2.0": - version: 4.2.0 - resolution: "crypto-js@npm:4.2.0" - checksum: 10/c7bcc56a6e01c3c397e95aa4a74e4241321f04677f9a618a8f48a63b5781617248afb9adb0629824792e7ec20ca0d4241a49b6b2938ae6f973ec4efc5c53c924 - languageName: node - linkType: hard - "cssjanus@npm:^2.0.1": version: 2.1.0 resolution: "cssjanus@npm:2.1.0" @@ -4765,10 +4914,10 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:3.3.1": - version: 3.3.1 - resolution: "date-fns@npm:3.3.1" - checksum: 10/98231936765dfb6fc6897676319b500a06a39f051b2c3ecbdd541a07ce9b1344b770277b8bfb1049fb7a2f70bf365ac8e6f1e2bb452b10e1a8101d518ca7f95d +"date-fns@npm:3.6.0": + version: 3.6.0 + resolution: "date-fns@npm:3.6.0" + checksum: 10/cac35c58926a3b5d577082ff2b253612ec1c79eb6754fddef46b6a8e826501ea2cb346ecbd211205f1ba382ddd1f9d8c3f00bf433ad63cc3063454d294e3a6b8 languageName: node linkType: hard @@ -5800,9 +5949,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react@npm:7.34.0": - version: 7.34.0 - resolution: "eslint-plugin-react@npm:7.34.0" +"eslint-plugin-react@npm:7.34.1": + version: 7.34.1 + resolution: "eslint-plugin-react@npm:7.34.1" dependencies: array-includes: "npm:^3.1.7" array.prototype.findlast: "npm:^1.2.4" @@ -5824,7 +5973,7 @@ __metadata: string.prototype.matchall: "npm:^4.0.10" peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - checksum: 10/e09623d715e25e012cc442648616ea5f8029c17a397e7b4f54c47da7cc4edb0ffec91af3269c259c1a92b8d83802b10f9c7148280a0c8d7659b15724ee8b50d8 + checksum: 10/ee059971065ea7e73ab5d8728774235c7dbf7a5e9f937c3b47e97f8fa9a5a96ab511d2ae6d5ec76a7e705ca666673d454f1e75a94033720819d041827f50f9c8 languageName: node linkType: hard @@ -5845,6 +5994,13 @@ __metadata: languageName: node linkType: hard +"eslint-visitor-keys@npm:^4.0.0": + version: 4.0.0 + resolution: "eslint-visitor-keys@npm:4.0.0" + checksum: 10/c7617166e6291a15ce2982b5c4b9cdfb6409f5c14562712d12e2584480cdf18609694b21d7dad35b02df0fa2cd037505048ded54d2f405c64f600949564eb334 + languageName: node + linkType: hard + "eslint@npm:^8.57.0": version: 8.57.0 resolution: "eslint@npm:8.57.0" @@ -5893,6 +6049,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^10.0.1": + version: 10.0.1 + resolution: "espree@npm:10.0.1" + dependencies: + acorn: "npm:^8.11.3" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^4.0.0" + checksum: 10/557d6cfb4894b1489effcaed8702682086033f8a2449568933bc59493734733d750f2a87907ba575844d3933340aea2d84288f5e67020c6152f6fd18a86497b2 + languageName: node + linkType: hard + "espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" @@ -6302,13 +6469,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.4": - version: 1.15.5 - resolution: "follow-redirects@npm:1.15.5" +"follow-redirects@npm:^1.15.6": + version: 1.15.6 + resolution: "follow-redirects@npm:1.15.6" peerDependenciesMeta: debug: optional: true - checksum: 10/d467f13c1c6aa734599b8b369cd7a625b20081af358f6204ff515f6f4116eb440de9c4e0c49f10798eeb0df26c95dd05d5e0d9ddc5786ab1a8a8abefe92929b4 + checksum: 10/70c7612c4cab18e546e36b991bbf8009a1a41cf85354afe04b113d1117569abf760269409cb3eb842d9f7b03d62826687086b081c566ea7b1e6613cf29030bf7 languageName: node linkType: hard @@ -6732,37 +6899,37 @@ __metadata: resolution: "graasp-player@workspace:." dependencies: "@commitlint/config-conventional": "npm:19.1.0" - "@cypress/code-coverage": "npm:3.12.26" + "@cypress/code-coverage": "npm:3.12.30" "@emotion/cache": "npm:11.11.0" "@emotion/react": "npm:11.11.4" "@emotion/styled": "npm:11.11.0" "@graasp/chatbox": "npm:3.1.0" - "@graasp/query-client": "npm:2.7.1" + "@graasp/query-client": "npm:3.0.1" "@graasp/sdk": "npm:4.2.1" "@graasp/translations": "npm:1.25.3" "@graasp/ui": "npm:4.11.0" - "@mui/icons-material": "npm:5.15.11" + "@mui/icons-material": "npm:5.15.14" "@mui/lab": "npm:5.0.0-alpha.151" - "@mui/material": "npm:5.15.11" + "@mui/material": "npm:5.15.14" "@sentry/react": "npm:7.108.0" "@trivago/prettier-plugin-sort-imports": "npm:4.3.0" "@types/katex": "npm:^0.16.7" - "@types/lodash.isarray": "npm:^4" + "@types/lodash.isarray": "npm:^4.0.9" "@types/lodash.truncate": "npm:^4.4.9" - "@types/node": "npm:^20.11.20" - "@types/react": "npm:18.2.60" - "@types/react-dom": "npm:^18.2.19" + "@types/node": "npm:^20.11.30" + "@types/react": "npm:18.2.67" + "@types/react-dom": "npm:^18.2.22" "@types/uuid": "npm:9.0.8" "@typescript-eslint/eslint-plugin": "npm:7.3.1" "@typescript-eslint/parser": "npm:7.3.1" "@vitejs/plugin-react": "npm:^4.2.1" classnames: "npm:2.5.1" - commitlint: "npm:19.2.0" + commitlint: "npm:19.2.1" concurrently: "npm:8.2.2" cypress: "npm:13.7.0" cypress-iframe: "npm:1.0.1" cypress-vite: "npm:1.5.0" - date-fns: "npm:3.3.1" + date-fns: "npm:3.6.0" env-cmd: "npm:10.1.0" eslint: "npm:^8.57.0" eslint-config-airbnb: "npm:19.0.4" @@ -6770,7 +6937,7 @@ __metadata: eslint-import-resolver-typescript: "npm:3.6.1" eslint-plugin-import: "npm:2.29.1" eslint-plugin-jsx-a11y: "npm:6.8.0" - eslint-plugin-react: "npm:7.34.0" + eslint-plugin-react: "npm:7.34.1" eslint-plugin-react-hooks: "npm:4.6.0" http-status-codes: "npm:2.3.0" husky: "npm:9.0.11" @@ -6784,12 +6951,12 @@ __metadata: react-dom: "npm:18.2.0" react-fullscreen-crossbrowser: "npm:1.1.3" react-ga4: "npm:2.1.0" - react-i18next: "npm:14.0.5" + react-i18next: "npm:14.1.0" react-intersection-observer: "npm:9.8.1" react-quill: "npm:2.0.0" react-router: "npm:6.22.3" react-router-dom: "npm:6.22.3" - react-toastify: "npm:10.0.4" + react-toastify: "npm:10.0.5" rollup-plugin-visualizer: "npm:5.12.0" stylis: "npm:4.3.1" stylis-plugin-rtl: "npm:2.1.1" @@ -6797,7 +6964,7 @@ __metadata: uuid: "npm:9.0.1" vite: "npm:5.2.3" vite-plugin-checker: "npm:^0.6.4" - vite-plugin-istanbul: "npm:5.0.0" + vite-plugin-istanbul: "npm:6.0.0" languageName: unknown linkType: soft @@ -7708,16 +7875,16 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-instrument@npm:^5.1.0": - version: 5.2.1 - resolution: "istanbul-lib-instrument@npm:5.2.1" +"istanbul-lib-instrument@npm:^6.0.2": + version: 6.0.2 + resolution: "istanbul-lib-instrument@npm:6.0.2" dependencies: - "@babel/core": "npm:^7.12.3" - "@babel/parser": "npm:^7.14.7" - "@istanbuljs/schema": "npm:^0.1.2" + "@babel/core": "npm:^7.23.9" + "@babel/parser": "npm:^7.23.9" + "@istanbuljs/schema": "npm:^0.1.3" istanbul-lib-coverage: "npm:^3.2.0" - semver: "npm:^6.3.0" - checksum: 10/bbc4496c2f304d799f8ec22202ab38c010ac265c441947f075c0f7d46bd440b45c00e46017cf9053453d42182d768b1d6ed0e70a142c95ab00df9843aa5ab80e + semver: "npm:^7.5.4" + checksum: 10/3aee19be199350182827679a137e1df142a306e9d7e20bb5badfd92ecc9023a7d366bc68e7c66e36983654a02a67401d75d8debf29fc6d4b83670fde69a594fc languageName: node linkType: hard @@ -10001,7 +10168,16 @@ __metadata: languageName: node linkType: hard -"qs@npm:6.11.2, qs@npm:^6.10.0": +"qs@npm:6.12.0": + version: 6.12.0 + resolution: "qs@npm:6.12.0" + dependencies: + side-channel: "npm:^1.0.6" + checksum: 10/6156d3f0d74278b7e93a3a6b56e6b87b513ebd45ae65c7330c96d70270d0844fb0af9454a194124cd56b9ebf47b456dd01e28b223cde93c8ab01d1cb53a0e491 + languageName: node + linkType: hard + +"qs@npm:^6.10.0": version: 6.11.2 resolution: "qs@npm:6.11.2" dependencies: @@ -10190,9 +10366,9 @@ __metadata: languageName: node linkType: hard -"react-i18next@npm:14.0.5": - version: 14.0.5 - resolution: "react-i18next@npm:14.0.5" +"react-i18next@npm:14.1.0": + version: 14.1.0 + resolution: "react-i18next@npm:14.1.0" dependencies: "@babel/runtime": "npm:^7.23.9" html-parse-stringify: "npm:^3.0.1" @@ -10204,7 +10380,7 @@ __metadata: optional: true react-native: optional: true - checksum: 10/4c91d4b889ab1ab05d7cda050890f7f41c4a73dadfef4778770a53b0d2f98e9d80f04da5086790706349d8a80cf09eec0c539fc1020a7c6fead562511dd2a2cf + checksum: 10/47b050ff3d8e43e8b56ca6b57f67c3feaecdb629b47eeb3923b4f43312b89af3e9373e93ada72420ad3199fc340605929512134eefcb55844f0eaa6bc0785613 languageName: node linkType: hard @@ -10348,15 +10524,15 @@ __metadata: languageName: node linkType: hard -"react-toastify@npm:10.0.4": - version: 10.0.4 - resolution: "react-toastify@npm:10.0.4" +"react-toastify@npm:10.0.5": + version: 10.0.5 + resolution: "react-toastify@npm:10.0.5" dependencies: clsx: "npm:^2.1.0" peerDependencies: - react: ">=16" - react-dom: ">=16" - checksum: 10/57f4d0032bf328381bdfeb78ab5efa988d425627a61ffa43b0caa184633a0ea44253a349d6b967247fa3d480ad82a2bbaa9063ce3f89be9550eb9b30398a6837 + react: ">=18" + react-dom: ">=18" + checksum: 10/6630f4b6d6902d827efd5e66c09df693c7ab8abeeb6ef24d880080f47b636614ef9cc251dd5e6564d49fe2f6f25f720ce0f7ef72cd4b0cd58a65b7c4b8052fac languageName: node linkType: hard @@ -11008,6 +11184,18 @@ __metadata: languageName: node linkType: hard +"side-channel@npm:^1.0.6": + version: 1.0.6 + resolution: "side-channel@npm:1.0.6" + dependencies: + call-bind: "npm:^1.0.7" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.4" + object-inspect: "npm:^1.13.1" + checksum: 10/eb10944f38cebad8ad643dd02657592fa41273ce15b8bfa928d3291aff2d30c20ff777cfe908f76ccc4551ace2d1245822fdc576657cce40e9066c638ca8fa4d + languageName: node + linkType: hard + "siginfo@npm:^2.0.0": version: 2.0.0 resolution: "siginfo@npm:2.0.0" @@ -12252,18 +12440,19 @@ __metadata: languageName: node linkType: hard -"vite-plugin-istanbul@npm:5.0.0": - version: 5.0.0 - resolution: "vite-plugin-istanbul@npm:5.0.0" +"vite-plugin-istanbul@npm:6.0.0": + version: 6.0.0 + resolution: "vite-plugin-istanbul@npm:6.0.0" dependencies: "@istanbuljs/load-nyc-config": "npm:^1.1.0" - espree: "npm:^9.6.1" - istanbul-lib-instrument: "npm:^5.1.0" + espree: "npm:^10.0.1" + istanbul-lib-instrument: "npm:^6.0.2" picocolors: "npm:^1.0.0" + source-map: "npm:^0.7.4" test-exclude: "npm:^6.0.0" peerDependencies: - vite: ">=2.9.1 <= 5" - checksum: 10/1c2ae560699f88fc89ea77854329e657e41a22232ae91e5eb0080cf794ed16aec6a549f6db03920986cc5c8b81566efb365a3dad791c6a8e79431a82ceb006b7 + vite: ">=4 <=6" + checksum: 10/9a989707b7d6faed1d3d6e58f246f4632d229d0d5524b1886a0338f7ec082b37a172a648132bced5a3e439639a0052dfe5117123944ac7c352df73298687c34a languageName: node linkType: hard