diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 6d142d7d5..9b950b0e7 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -4,8 +4,6 @@ name: Cypress CI on: # Triggers the workflow on push events except for the main branch push: - branches-ignore: - - main concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} @@ -18,12 +16,12 @@ jobs: timeout-minutes: 50 steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v3 with: - node-version: '18' + node-version: 18 - name: Yarn Install and Cache uses: graasp/graasp-deploy/.github/actions/yarn-install-and-cache@v1 @@ -56,7 +54,6 @@ jobs: install: false # we launch the app in preview mode to avoid issues with hmr websockets from vite polluting the mocks start: yarn preview:test - browser: chrome quiet: true config-file: cypress.config.ts cache-key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} diff --git a/cypress.config.ts b/cypress.config.ts index 61a066163..b5fc2c9ee 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -19,12 +19,6 @@ export default defineConfig({ setupNodeEvents(on, config) { // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require require('@cypress/code-coverage/task')(on, config); - on('before:browser:launch', (browser, launchOptions) => { - if (browser.name === 'chrome' && browser.isHeadless) { - launchOptions.args.push('--headless=old'); - } - return launchOptions; - }); return config; }, baseUrl: `http://localhost:${process.env.VITE_PORT || 3333}`, diff --git a/cypress/e2e/item/create/createApp.cy.ts b/cypress/e2e/item/create/createApp.cy.ts index 4ac9830ea..8a58b50e2 100644 --- a/cypress/e2e/item/create/createApp.cy.ts +++ b/cypress/e2e/item/create/createApp.cy.ts @@ -1,6 +1,9 @@ import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import ITEM_LAYOUT_MODES from '../../../../src/enums/itemLayoutModes'; -import { GRAASP_APP_ITEM } from '../../../fixtures/apps'; +import { + GRAASP_APP_ITEM, + GRAASP_CUSTOM_APP_ITEM, +} from '../../../fixtures/apps'; import { SAMPLE_ITEMS } from '../../../fixtures/items'; import { createApp } from '../../../support/createUtils'; @@ -56,7 +59,7 @@ describe('Create App', () => { }); }); - it('Create app by typing', () => { + it('Create a custom app', () => { cy.setUpApi(SAMPLE_ITEMS); const { id } = SAMPLE_ITEMS.items[0]; @@ -66,7 +69,7 @@ describe('Create App', () => { cy.switchMode(ITEM_LAYOUT_MODES.LIST); // create - createApp(GRAASP_APP_ITEM); + createApp(GRAASP_CUSTOM_APP_ITEM, { custom: true }); cy.wait('@postItem').then(() => { // expect update diff --git a/cypress/e2e/item/download/downloadItem.cy.ts b/cypress/e2e/item/download/downloadItem.cy.ts new file mode 100644 index 000000000..f3d47a527 --- /dev/null +++ b/cypress/e2e/item/download/downloadItem.cy.ts @@ -0,0 +1,42 @@ +import { buildDownloadButtonId } from '@/config/selectors'; +import { ITEM_LAYOUT_MODES } from '@/enums'; + +import { SHARED_ITEMS_PATH, buildItemPath } from '../../../../src/config/paths'; +import { SAMPLE_PUBLIC_ITEMS } from '../../../fixtures/items'; +import { SIGNED_OUT_MEMBER } from '../../../fixtures/members'; +import { SHARED_ITEMS } from '../../../fixtures/sharedItems'; + +describe('Download Item', () => { + it('Table View', () => { + cy.setUpApi(SHARED_ITEMS); + cy.visit(SHARED_ITEMS_PATH); + cy.wait('@getSharedItems').then(({ response: { body } }) => { + for (const item of body) { + cy.get(`#${buildDownloadButtonId(item.id)}`).should('exist'); + } + }); + }); + it('Grid view', () => { + cy.setUpApi(SHARED_ITEMS); + cy.visit(SHARED_ITEMS_PATH); + cy.switchMode(ITEM_LAYOUT_MODES.GRID); + cy.wait('@getSharedItems').then(({ response: { body } }) => { + for (const item of body) { + cy.get(`#${buildDownloadButtonId(item.id)}`).should('exist'); + } + }); + }); + it('download button for public item should be exist', () => { + const currentMember = SIGNED_OUT_MEMBER; + cy.setUpApi({ + ...SAMPLE_PUBLIC_ITEMS, + currentMember, + }); + const item = SAMPLE_PUBLIC_ITEMS.items[4]; + cy.visit(buildItemPath(item.id)); + cy.wait('@getItem').then(({ response: { body } }) => { + expect(body.id).to.equal(item.id); + cy.get(`#${buildDownloadButtonId(item.id)}`).should('exist'); + }); + }); +}); diff --git a/cypress/e2e/item/share/shareItem.cy.ts b/cypress/e2e/item/share/shareItem.cy.ts index ba816beac..fc65060a3 100644 --- a/cypress/e2e/item/share/shareItem.cy.ts +++ b/cypress/e2e/item/share/shareItem.cy.ts @@ -1,5 +1,7 @@ import { Context, ItemLoginSchemaType, ItemTagType } from '@graasp/sdk'; +import shortUUID from 'short-uuid'; + import { buildItemPath } from '@/config/paths'; import { SETTINGS } from '../../../../src/config/constants'; @@ -7,6 +9,8 @@ import { SHARE_ITEM_DIALOG_LINK_ID, SHARE_ITEM_DIALOG_LINK_SELECT_ID, SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID, + SHARE_ITEM_QR_BTN_ID, + SHARE_ITEM_QR_DIALOG_ID, SHARE_ITEM_VISIBILITY_SELECT_ID, buildShareButtonId, } from '../../../../src/config/selectors'; @@ -37,16 +41,17 @@ describe('Share Item', () => { cy.visit(buildItemPath(item.id)); openShareItemTab(item.id); + const { fromUUID } = shortUUID(); // sharing link cy.get(`#${SHARE_ITEM_DIALOG_LINK_ID}`).should( 'contain', - `${buildGraaspPlayerView(item.id)}`, + `${buildGraaspPlayerView(fromUUID(item.id))}`, ); cy.get(`#${SHARE_ITEM_DIALOG_LINK_SELECT_ID}`).click(); cy.get(`li[data-value="${Context.Builder}"]`).click(); cy.get(`#${SHARE_ITEM_DIALOG_LINK_ID}`).should( 'have.text', - `${buildGraaspBuilderView(item.id)}`, + `${buildGraaspBuilderView(fromUUID(item.id))}`, ); const visiblitySelect = cy.get( @@ -126,4 +131,15 @@ describe('Share Item', () => { expect(url).to.include(item.id); }); }); + + it('Share Item with QR Code', () => { + cy.setUpApi({ ...SAMPLE_PUBLIC_ITEMS }); + const item = SAMPLE_PUBLIC_ITEMS.items[0]; + cy.visit(buildItemPath(item.id)); + openShareItemTab(item.id); + + cy.get(`#${SHARE_ITEM_QR_BTN_ID}`).click(); + + cy.get(`#${SHARE_ITEM_QR_DIALOG_ID}`).should('exist'); + }); }); diff --git a/cypress/fixtures/apps.ts b/cypress/fixtures/apps.ts index c221b05d7..51e26cc94 100644 --- a/cypress/fixtures/apps.ts +++ b/cypress/fixtures/apps.ts @@ -23,7 +23,19 @@ export const GRAASP_APP_ITEM: AppItemType = { ...DEFAULT_FOLDER_ITEM, id: 'ecafbd2a-5688-12eb-ae91-0272ac130002', path: 'ecafbd2a_5688_12eb_ae91_0272ac130002', - name: 'my app', + name: 'test app', + description: 'my app description', + type: ItemType.APP, + extra: { + [ItemType.APP]: { url: APPS_LIST[0].url }, + }, + creator: CURRENT_USER, +}; +export const GRAASP_CUSTOM_APP_ITEM: AppItemType = { + ...DEFAULT_FOLDER_ITEM, + id: 'ecafbd2a-5688-12eb-ae91-0272ac130002', + path: 'ecafbd2a_5688_12eb_ae91_0272ac130002', + name: 'Add Your Custom App', description: 'my app description', type: ItemType.APP, extra: { diff --git a/cypress/fixtures/apps/apps.ts b/cypress/fixtures/apps/apps.ts index b0c149cd8..a3a5753cf 100644 --- a/cypress/fixtures/apps/apps.ts +++ b/cypress/fixtures/apps/apps.ts @@ -2,6 +2,7 @@ import { App, Publisher } from '@graasp/sdk'; export const APP_NAME = 'test app'; export const NEW_APP_NAME = 'my new test app'; +export const CUSTOM_APP_URL = 'http://testapp.com'; export const publisher: Publisher = { id: 'publisher-id', diff --git a/cypress/fixtures/files.ts b/cypress/fixtures/files.ts index 6f11e6dd3..e1afac62e 100644 --- a/cypress/fixtures/files.ts +++ b/cypress/fixtures/files.ts @@ -26,6 +26,7 @@ export const IMAGE_ITEM_DEFAULT: LocalFileItemForTest = { size: 32439, mimetype: 'image/png', altText: 'myAltText', + content: '', }), // for testing: creating needs a fixture, reading needs an url createFilepath: ICON_FILEPATH, @@ -50,6 +51,7 @@ export const IMAGE_ITEM_DEFAULT_WITH_MAX_WIDTH: LocalFileItemForTest = { size: 32439, mimetype: 'image/png', altText: 'myAltText', + content: '', }), // for testing: creating needs a fixture, reading needs an url createFilepath: ICON_FILEPATH, @@ -72,6 +74,7 @@ export const VIDEO_ITEM_DEFAULT: LocalFileItemForTest = { size: 52345, mimetype: MimeTypes.Video.MP4, altText: 'myAltText', + content: '', }), // for testing: creating needs a fixture, reading needs an url createFilepath: VIDEO_FILEPATH, @@ -94,6 +97,7 @@ export const PDF_ITEM_DEFAULT: LocalFileItemForTest = { size: 54321, mimetype: MimeTypes.PDF, altText: 'myAltText', + content: '', }), // for testing: creating needs a fixture, reading needs an url createFilepath: ICON_FILEPATH, @@ -121,6 +125,7 @@ export const IMAGE_ITEM_S3: S3FileItemForTest = { mimetype: MimeTypes.Image.PNG, name: 'myfile', altText: 'myAltText', + content: '', }), // for testing: creating needs a fixture, reading needs an url createFilepath: ICON_FILEPATH, @@ -143,6 +148,7 @@ export const VIDEO_ITEM_S3: S3FileItemForTest = { mimetype: MimeTypes.Video.MP4, name: 'myfile', altText: 'myAltText', + content: '', }), // for testing: creating needs a fixture, reading needs an url createFilepath: VIDEO_FILEPATH, @@ -165,6 +171,7 @@ export const PDF_ITEM_S3: S3FileItemForTest = { mimetype: MimeTypes.PDF, name: 'myfile', altText: 'myAltText', + content: '', }), // for testing: creating needs a fixture, reading needs an url createFilepath: ICON_FILEPATH, diff --git a/cypress/support/commands/item.ts b/cypress/support/commands/item.ts index 3fee9a487..3379496f6 100644 --- a/cypress/support/commands/item.ts +++ b/cypress/support/commands/item.ts @@ -1,6 +1,7 @@ import { ItemType, getAppExtra, getDocumentExtra } from '@graasp/sdk'; import { + CUSTOM_APP_URL_ID, FOLDER_FORM_DESCRIPTION_ID, ITEM_FORM_APP_URL_ID, ITEM_FORM_CONFIRM_BUTTON_ID, @@ -17,7 +18,11 @@ import { buildTreeItemId, } from '../../../src/config/selectors'; import { getParentsIdsFromPath } from '../../../src/utils/item'; -import { APP_NAME, NEW_APP_NAME } from '../../fixtures/apps/apps'; +import { + APP_NAME, + CUSTOM_APP_URL, + NEW_APP_NAME, +} from '../../fixtures/apps/apps'; import { TREE_VIEW_PAUSE } from '../constants'; Cypress.Commands.add( @@ -137,14 +142,20 @@ Cypress.Commands.add( Cypress.Commands.add( 'fillAppModal', - ({ name = '', extra }, { confirm = true, type = false } = {}) => { + ( + { name = '', extra }, + { confirm = true, type = false, custom = false } = {}, + ) => { cy.fillBaseItemModal({ name }, { confirm: false }); - cy.get(`#${ITEM_FORM_APP_URL_ID}`).click(); if (type) { cy.get(`#${ITEM_FORM_APP_URL_ID}`).type(getAppExtra(extra)?.url); + } else if (custom) { + cy.get(`#${buildItemFormAppOptionId(name)}`).click(); + // check name get added automatically + cy.get(`#${CUSTOM_APP_URL_ID}`).type(CUSTOM_APP_URL); } else { - cy.get(`#${buildItemFormAppOptionId(APP_NAME)}`).click(); + cy.get(`#${buildItemFormAppOptionId(name)}`).click(); // check name get added automatically cy.get(`#${ITEM_FORM_NAME_INPUT_ID}`).should('have.value', APP_NAME); // edit the app name diff --git a/cypress/support/createUtils.ts b/cypress/support/createUtils.ts index ca7f012ab..50e5d771a 100644 --- a/cypress/support/createUtils.ts +++ b/cypress/support/createUtils.ts @@ -23,7 +23,7 @@ import { FileItemForTest } from './types'; export const createApp = ( payload: AppItemType, - options?: { confirm?: boolean }, + options?: { confirm?: boolean; custom?: boolean }, ): void => { cy.get(`#${CREATE_ITEM_BUTTON_ID}`).click(); cy.get(`#${CREATE_ITEM_APP_ID}`).click(); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 3af8af774..feb963d22 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -63,7 +63,7 @@ declare global { ): void; fillAppModal( payload: { name: string; extra?: AppItemExtra }, - options?: { type?: boolean; confirm?: boolean }, + options?: { type?: boolean; confirm?: boolean; custom?: boolean }, ): void; fillFolderModal( arg1: { name?: string; description?: string }, diff --git a/package.json b/package.json index b5e3d8540..ae5e5b2f8 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ "@emotion/react": "11.11.1", "@emotion/styled": "11.11.0", "@graasp/chatbox": "2.0.1", - "@graasp/query-client": "1.5.0", - "@graasp/sdk": "1.4.0", + "@graasp/query-client": "1.8.2", + "@graasp/sdk": "1.6.0", "@graasp/translations": "1.19.0", - "@graasp/ui": "3.4.0", + "@graasp/ui": "3.5.1", "@mui/icons-material": "5.14.6", "@mui/lab": "5.0.0-alpha.141", "@mui/material": "5.14.6", @@ -55,11 +55,13 @@ "react-ga4": "2.1.0", "react-i18next": "13.2.0", "react-image-crop": "9.1.1", + "react-qr-code": "2.0.12", "react-query": "3.39.3", "react-quill": "2.0.0", "react-router": "6.15.0", "react-router-dom": "6.15.0", "react-toastify": "9.1.3", + "short-uuid": "4.2.2", "stylis": "4.3.0", "stylis-plugin-rtl": "2.1.1", "uuid": "9.0.0", @@ -124,7 +126,7 @@ "@typescript-eslint/parser": "6.5.0", "@vitejs/plugin-react": "4.0.4", "concurrently": "8.2.1", - "cypress": "12.17.4", + "cypress": "13.2.0", "cypress-localstorage-commands": "2.2.4", "env-cmd": "10.1.0", "eslint": "^8.47.0", diff --git a/src/components/common/QRCode.tsx b/src/components/common/QRCode.tsx new file mode 100644 index 000000000..698fc3865 --- /dev/null +++ b/src/components/common/QRCode.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import QR from 'react-qr-code'; + +import { QrCode2 } from '@mui/icons-material'; +import CloseIcon from '@mui/icons-material/Close'; +import { Box, Dialog, DialogContent, IconButton, Tooltip } from '@mui/material'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { + SHARE_ITEM_QR_BTN_ID, + SHARE_ITEM_QR_DIALOG_ID, +} from '@/config/selectors'; +import { BUILDER } from '@/langs/constants'; + +type Props = { + value: string; +}; + +const QRCode = ({ value }: Props): JSX.Element => { + const { t: translateBuilder } = useBuilderTranslation(); + const [openQrModal, setOpenQrModal] = useState(false); + + return ( + <> + + setOpenQrModal(true)} + id={SHARE_ITEM_QR_BTN_ID} + > + + + + setOpenQrModal(false)} + id={SHARE_ITEM_QR_DIALOG_ID} + > + setOpenQrModal(false)} + sx={{ + position: 'absolute', + right: 8, + top: 8, + }} + > + + + + + + + + + + ); +}; + +export default QRCode; diff --git a/src/components/item/form/AppForm.tsx b/src/components/item/form/AppForm.tsx index ee491bc28..4910a7f84 100644 --- a/src/components/item/form/AppForm.tsx +++ b/src/components/item/form/AppForm.tsx @@ -1,19 +1,18 @@ -import { HTMLAttributes, useState } from 'react'; +import React, { useState } from 'react'; -import { TextField } from '@mui/material'; -import Autocomplete from '@mui/material/Autocomplete'; +import CloseIcon from '@mui/icons-material/Close'; +import { Box, IconButton, InputAdornment, TextField } from '@mui/material'; import Skeleton from '@mui/material/Skeleton'; import Typography from '@mui/material/Typography'; -import { AppItemType, DiscriminatedItem, Item, getAppExtra } from '@graasp/sdk'; +import { AppItemType, DiscriminatedItem } from '@graasp/sdk'; import { AppRecord } from '@graasp/sdk/frontend'; +import AppCard from '@/components/main/AppCard'; +import { CUSTOM_APP_URL_ID } from '@/config/selectors'; + import { useBuilderTranslation } from '../../../config/i18n'; import { hooks } from '../../../config/queryClient'; -import { - ITEM_FORM_APP_URL_ID, - buildItemFormAppOptionId, -} from '../../../config/selectors'; import { BUILDER } from '../../../langs/constants'; import { buildAppExtra } from '../../../utils/itemExtra'; import BaseItemForm from './NameForm'; @@ -31,8 +30,11 @@ const AppForm = ({ }: Props): JSX.Element => { const { t: translateBuilder } = useBuilderTranslation(); const [newName, setNewName] = useState(item?.name ?? ''); + const [isCustomApp, setIsCustomApp] = useState(false); - const handleAppSelection = (_event: any, newValue: AppRecord | null) => { + const handleAppSelection = ( + newValue: AppRecord | null | { url: string; name: string }, + ) => { if (!newValue) { return console.error('new value is undefined'); } @@ -51,23 +53,19 @@ const AppForm = ({ return onChange(props); }; - const handleAppInput = (_event: any, url: string) => { - // TODO: improve types - const props = { - ...item, - extra: buildAppExtra({ url }), - } as unknown as Item; - onChange(props); - }; - const { useApps } = hooks; const { data, isLoading: isAppsLoading } = useApps(); - const url = getAppExtra(item?.extra)?.url; + const url = (updatedProperties?.extra?.app as { url: string })?.url; - // todo: fix type -> we will change the interface - const value = data?.find((app) => app.url === url) || (url as any); + const addCustomApp = () => { + setIsCustomApp(true); + handleAppSelection({ url: '', name: '' }); + }; + if (isAppsLoading) { + return ; + } return (
@@ -83,67 +81,60 @@ const AppForm = ({ } as Partial } /> +
- {isAppsLoading ? ( - - ) : ( - { - if (typeof option === 'string') { - return option; + {isCustomApp ? ( + + + handleAppSelection({ url: e.target.value, name: '' }) } - return option.url; - }} - filterOptions={(options, state) => { - const filteredOptionsByName = options.filter((opt: AppRecord) => - opt.name.toLowerCase().includes(state.inputValue.toLowerCase()), - ); - return filteredOptionsByName; + InputProps={{ + endAdornment: ( + + { + setIsCustomApp(false); + handleAppSelection({ url: '', name: '' }); + }} + > + + + + ), + }} + /> + + ) : ( + , - option: AppRecord, - ) => ( -
  • - {option.name} - - {option.name} - - {option.description} -
  • - )} - renderInput={(params) => ( - + {data?.map((ele) => ( + - )} - /> + ))} + +
    )}
    ); diff --git a/src/components/item/header/ItemHeader.tsx b/src/components/item/header/ItemHeader.tsx index 6185407c5..a21085ae3 100644 --- a/src/components/item/header/ItemHeader.tsx +++ b/src/components/item/header/ItemHeader.tsx @@ -1,10 +1,9 @@ -import { useMatch } from 'react-router'; - import Stack from '@mui/material/Stack'; -import { Loader } from '@graasp/ui'; +import { Loader, useShortenURLParams } from '@graasp/ui'; + +import { ITEM_ID_PARAMS } from '@/config/paths'; -import { buildItemPath } from '../../../config/paths'; import { hooks } from '../../../config/queryClient'; import { ITEM_HEADER_ID } from '../../../config/selectors'; import ErrorAlert from '../../common/ErrorAlert'; @@ -18,8 +17,7 @@ type Props = { }; const ItemHeader = ({ showNavigation = true }: Props): JSX.Element => { - const match = useMatch(buildItemPath()); - const itemId = match?.params?.itemId; + const itemId = useShortenURLParams(ITEM_ID_PARAMS); const { data: item, isLoading: isItemLoading, isError } = useItem(itemId); if (isItemLoading) { diff --git a/src/components/item/header/ItemHeaderActions.tsx b/src/components/item/header/ItemHeaderActions.tsx index daf00ac9c..95c5c1ff4 100644 --- a/src/components/item/header/ItemHeaderActions.tsx +++ b/src/components/item/header/ItemHeaderActions.tsx @@ -5,6 +5,7 @@ import { ItemRecord } from '@graasp/sdk/frontend'; import { ChatboxButton } from '@graasp/ui'; import EditButton from '@/components/common/EditButton'; +import DownloadButton from '@/components/main/DownloadButton'; import { ITEM_TYPES_WITH_CAPTIONS } from '../../../config/constants'; import { useBuilderTranslation } from '../../../config/i18n'; @@ -93,6 +94,7 @@ const ItemHeaderActions = ({ item }: Props): JSX.Element => { <> {openedActionTabId !== ItemActionTabs.Settings && activeActions} {canEdit && } + ); } diff --git a/src/components/item/sharing/SharingLink.tsx b/src/components/item/sharing/SharingLink.tsx index 82e303b1a..a0956aa8b 100644 --- a/src/components/item/sharing/SharingLink.tsx +++ b/src/components/item/sharing/SharingLink.tsx @@ -11,6 +11,9 @@ import Tooltip from '@mui/material/Tooltip'; import { Context } from '@graasp/sdk'; import { FAILURE_MESSAGES, SUCCESS_MESSAGES } from '@graasp/translations'; +import shortUUID from 'short-uuid'; + +import QRCode from '@/components/common/QRCode'; import { SHARE_LINK_COLOR, SHARE_LINK_CONTAINER_BORDER_STYLE, @@ -57,6 +60,7 @@ type Props = { itemId?: string; }; +const { fromUUID } = shortUUID(); const SharingLink = ({ itemId }: Props): JSX.Element => { const { t: translateMessages } = useMessagesTranslation(); const { t: translateBuilder } = useBuilderTranslation(); @@ -69,11 +73,11 @@ const SharingLink = ({ itemId }: Props): JSX.Element => { if (itemId) { switch (linkType) { case Context.Builder: { - setLink(buildGraaspBuilderView(itemId)); + setLink(buildGraaspBuilderView(fromUUID(itemId || ''))); break; } case Context.Player: { - setLink(buildGraaspPlayerView(itemId)); + setLink(buildGraaspPlayerView(fromUUID(itemId || ''))); break; } default: @@ -153,6 +157,7 @@ const SharingLink = ({ itemId }: Props): JSX.Element => { + {link && } ); diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index 1bbcc1f6e..6b30d1914 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -1,11 +1,17 @@ -import { useLocation, useMatch } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; -import { HomeMenu, ItemMenu, Navigation } from '@graasp/ui'; +import { + HomeMenu, + ItemMenu, + Navigation, + useShortenURLParams, +} from '@graasp/ui'; import { useBuilderTranslation } from '../../config/i18n'; import { FAVORITE_ITEMS_PATH, HOME_PATH, + ITEM_ID_PARAMS, SHARED_ITEMS_PATH, buildItemPath, } from '../../config/paths'; @@ -27,9 +33,8 @@ const { const Navigator = (): JSX.Element | null => { const { t: translateBuilder } = useBuilderTranslation(); - const match = useMatch(buildItemPath()); + const itemId = useShortenURLParams(ITEM_ID_PARAMS); const { pathname } = useLocation(); - const itemId = match?.params?.itemId; const { data: currentMember } = useCurrentMember(); const { data: item, isLoading: isItemLoading } = useItem(itemId); const itemPath = item?.path; diff --git a/src/components/main/AppCard.tsx b/src/components/main/AppCard.tsx new file mode 100644 index 000000000..1d2f713a4 --- /dev/null +++ b/src/components/main/AppCard.tsx @@ -0,0 +1,93 @@ +import React from 'react'; + +import CloseIcon from '@mui/icons-material/Close'; +import { + Card, + CardActionArea, + CardContent, + CardMedia, + IconButton, + Typography, +} from '@mui/material'; + +import { buildItemFormAppOptionId } from '@/config/selectors'; + +import AddNewIcon from '../../resources/addNew.png'; + +export type Props = { + url?: string; + description?: string; + name: string; + extra?: { image?: string }; + handleSelect: any; + selected?: boolean; +}; + +const AppCard = ({ + url, + description, + name, + extra, + handleSelect, + selected = false, +}: Props): JSX.Element => { + const clearApp = (e: React.MouseEvent) => { + e.stopPropagation(); + handleSelect({ url: '', name: '' }); + }; + return ( + { + handleSelect({ url, description, name, extra }); + }} + id={buildItemFormAppOptionId(name)} + > + {selected && ( + + + + )} + + + + + {name} + + + {description} + + + + + ); +}; + +export default AppCard; diff --git a/src/components/main/DownloadButton.tsx b/src/components/main/DownloadButton.tsx index 51c2d386e..ddafc0621 100644 --- a/src/components/main/DownloadButton.tsx +++ b/src/components/main/DownloadButton.tsx @@ -2,6 +2,8 @@ import { useEffect } from 'react'; import { DownloadButton as Button } from '@graasp/ui'; +import { buildDownloadButtonId } from '@/config/selectors'; + import { useBuilderTranslation } from '../../config/i18n'; import { mutations } from '../../config/queryClient'; import { BUILDER } from '../../langs/constants'; @@ -37,14 +39,16 @@ export const DownloadButton = ({ id, name }: Props): JSX.Element => { downloadItem({ id }); }; return ( -