diff --git a/.github/workflows/cdelivery-s3-caller.yml b/.github/workflows/cdelivery-s3-caller.yml index fab893fbe..dd9347f2a 100644 --- a/.github/workflows/cdelivery-s3-caller.yml +++ b/.github/workflows/cdelivery-s3-caller.yml @@ -9,23 +9,24 @@ on: jobs: graasp-deploy-s3-workflow: name: Graasp Builder - uses: graasp/graasp-deploy/.github/workflows/cdelivery-s3.yml@bea834ec77096ee815ec190d28d7e2cf7d94ada1 + uses: graasp/graasp-deploy/.github/workflows/cdelivery-s3.yml@1a7c4c74273be6fd1c56eb6c5a8fb4af2d211b86 with: build-folder: 'build' tag: ${{ github.event.client_payload.tag }} secrets: + api-host: ${{ secrets.REACT_APP_API_HOST_STAGE }} + authentication-host: ${{ secrets.REACT_APP_AUTHENTICATION_HOST_STAGE }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_STAGE }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGE }} aws-region: ${{ secrets.AWS_REGION_STAGE }} aws-s3-bucket-name: ${{ secrets.AWS_S3_BUCKET_NAME_GRAASP_COMPOSE_STAGE }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGE }} cloudfront-distribution-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_GRAASP_COMPOSE_STAGE }} - api-host: ${{ secrets.REACT_APP_API_HOST_STAGE }} - show-notifications: ${{ secrets.REACT_APP_SHOW_NOTIFICATIONS }} - authentication-host: ${{ secrets.REACT_APP_AUTHENTICATION_HOST_STAGE }} - graasp-perform-host: ${{ secrets.PLAYER_CLIENT_HOST_STAGE }} - graasp-explorer-host: ${{ secrets.EXPLORER_CLIENT_HOST_STAGE }} - graasp-analyzer-host: ${{ secrets.ANALYZER_CLIENT_HOST_STAGE }} + domain: ${{ secrets.REACT_APP_DOMAIN_STAGE }} ga-measurement-id: ${{ secrets.REACT_APP_GA_MEASUREMENT_ID_STAGE }} + graasp-analyzer-host: ${{ secrets.ANALYZER_CLIENT_HOST_STAGE }} + graasp-explorer-host: ${{ secrets.EXPLORER_CLIENT_HOST_STAGE }} + graasp-perform-host: ${{ secrets.PLAYER_CLIENT_HOST_STAGE }} + h5p-integration-url: ${{ secrets.H5P_INTEGRATION_URL_STAGE }} hidden-item-tag-id: ${{ secrets.REACT_APP_HIDDEN_ITEM_TAG_ID_STAGE }} - domain: ${{ secrets.REACT_APP_DOMAIN_STAGE }} sentry-dsn: ${{ secrets.REACT_APP_SENTRY_DSN }} + show-notifications: ${{ secrets.REACT_APP_SHOW_NOTIFICATIONS }} diff --git a/.github/workflows/cdeployment-s3-caller.yml b/.github/workflows/cdeployment-s3-caller.yml index cb50a269a..114babba8 100644 --- a/.github/workflows/cdeployment-s3-caller.yml +++ b/.github/workflows/cdeployment-s3-caller.yml @@ -13,25 +13,26 @@ jobs: name: Graasp Builder # Replace 'main' with the hash of a commit, so it points to an specific version of the reusable workflow that is used # Reference reusable workflow file. Using the commit SHA is the safest for stability and security - uses: graasp/graasp-deploy/.github/workflows/cdeployment-s3.yml@bea834ec77096ee815ec190d28d7e2cf7d94ada1 - # Replace input build-folder if needed. + uses: graasp/graasp-deploy/.github/workflows/cdeployment-s3.yml@1a7c4c74273be6fd1c56eb6c5a8fb4af2d211b86 + # Replace input build-folder if needed. with: build-folder: 'build' tag: ${{ github.event.client_payload.tag }} # Insert required secrets based on repository with the following format: ${{ secrets.SECRET_NAME }} secrets: + api-host: ${{ secrets.REACT_APP_API_HOST_PROD }} + authentication-host: ${{ secrets.REACT_APP_AUTHENTICATION_HOST_PROD }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_PROD }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }} aws-region: ${{ secrets.AWS_REGION_PROD }} aws-s3-bucket-name: ${{ secrets.AWS_S3_BUCKET_NAME_GRAASP_COMPOSE_PROD }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }} cloudfront-distribution-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_GRAASP_COMPOSE_PROD }} - api-host: ${{ secrets.REACT_APP_API_HOST_PROD }} - show-notifications: ${{ secrets.REACT_APP_SHOW_NOTIFICATIONS }} - authentication-host: ${{ secrets.REACT_APP_AUTHENTICATION_HOST_PROD }} - graasp-perform-host: ${{ secrets.PLAYER_CLIENT_HOST_PROD }} - graasp-explorer-host: ${{ secrets.EXPLORER_CLIENT_HOST_PROD }} - graasp-analyzer-host: ${{ secrets.ANALYZER_CLIENT_HOST_PROD }} + domain: ${{ secrets.REACT_APP_DOMAIN_PROD }} ga-measurement-id: ${{ secrets.REACT_APP_GA_MEASUREMENT_ID_PROD }} + graasp-analyzer-host: ${{ secrets.ANALYZER_CLIENT_HOST_PROD }} + graasp-explorer-host: ${{ secrets.EXPLORER_CLIENT_HOST_PROD }} + graasp-perform-host: ${{ secrets.PLAYER_CLIENT_HOST_PROD }} + h5p-integration-url: ${{ secrets.H5P_INTEGRATION_URL_PROD }} hidden-item-tag-id: ${{ secrets.REACT_APP_HIDDEN_ITEM_TAG_ID_PROD }} - domain: ${{ secrets.REACT_APP_DOMAIN_PROD }} sentry-dsn: ${{ secrets.REACT_APP_SENTRY_DSN }} + show-notifications: ${{ secrets.REACT_APP_SHOW_NOTIFICATIONS }} diff --git a/.github/workflows/cintegration-s3-caller.yml b/.github/workflows/cintegration-s3-caller.yml index f9a3f30d3..63c3eac31 100644 --- a/.github/workflows/cintegration-s3-caller.yml +++ b/.github/workflows/cintegration-s3-caller.yml @@ -16,22 +16,23 @@ jobs: graasp-deploy-s3-workflow: name: Graasp Builder # Reference reusable workflow file. Using the commit SHA is the safest for stability and security - uses: graasp/graasp-deploy/.github/workflows/cintegration-s3.yml@bea834ec77096ee815ec190d28d7e2cf7d94ada1 + uses: graasp/graasp-deploy/.github/workflows/cintegration-s3.yml@1a7c4c74273be6fd1c56eb6c5a8fb4af2d211b86 with: build-folder: 'build' secrets: + api-host: ${{ secrets.REACT_APP_API_HOST_DEV }} + authentication-host: ${{ secrets.REACT_APP_AUTHENTICATION_HOST_DEV }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_DEV }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }} aws-region: ${{ secrets.AWS_REGION_DEV }} aws-s3-bucket-name: ${{ secrets.AWS_S3_BUCKET_NAME_GRAASP_COMPOSE_DEV }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }} cloudfront-distribution-id: ${{ secrets.CLOUDFRONT_DISTRIBUTION_GRAASP_COMPOSE_DEV }} - api-host: ${{ secrets.REACT_APP_API_HOST_DEV }} - show-notifications: ${{ secrets.REACT_APP_SHOW_NOTIFICATIONS }} - authentication-host: ${{ secrets.REACT_APP_AUTHENTICATION_HOST_DEV }} - graasp-perform-host: ${{ secrets.PLAYER_CLIENT_HOST_DEV }} - graasp-explorer-host: ${{ secrets.EXPLORER_CLIENT_HOST_DEV }} - graasp-analyzer-host: ${{ secrets.ANALYZER_CLIENT_HOST_DEV }} + domain: ${{ secrets.REACT_APP_DOMAIN_DEV }} ga-measurement-id: ${{ secrets.REACT_APP_GA_MEASUREMENT_ID_DEV }} + graasp-analyzer-host: ${{ secrets.ANALYZER_CLIENT_HOST_DEV }} + graasp-explorer-host: ${{ secrets.EXPLORER_CLIENT_HOST_DEV }} + graasp-perform-host: ${{ secrets.PLAYER_CLIENT_HOST_DEV }} + h5p-integration-url: ${{ secrets.H5P_INTEGRATION_URL_DEV }} hidden-item-tag-id: ${{ secrets.REACT_APP_HIDDEN_ITEM_TAG_ID_DEV }} - domain: ${{ secrets.REACT_APP_DOMAIN_DEV }} sentry-dsn: ${{ secrets.REACT_APP_SENTRY_DSN }} + show-notifications: ${{ secrets.REACT_APP_SHOW_NOTIFICATIONS }} diff --git a/README.md b/README.md index 63d6a886e..9e43c908d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ REACT_APP_API_HOST=http://localhost:3000 PORT=3111 REACT_APP_SHOW_NOTIFICATIONS=true REACT_APP_AUTHENTICATION_HOST=http://localhost:3001 +REACT_APP_H5P_INTEGRATION_URL= ``` 4. Run `yarn start`. The client should be accessible at `localhost:3111` @@ -36,6 +37,7 @@ REACT_APP_API_HOST=http://localhost:3000 PORT=3111 REACT_APP_SHOW_NOTIFICATIONS=false REACT_APP_NODE_ENV=test +REACT_APP_H5P_INTEGRATION_URL= ``` Run `yarn cypress`. This should run every tests headlessly. diff --git a/package.json b/package.json index 743b1fea8..cc64626ff 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "@commitlint/config-conventional": "16.2.1", "@cypress/code-coverage": "3.9.12", "@cypress/instrument-cra": "1.4.0", - "@graasp/websockets": "github:graasp/graasp-websockets.git#master", + "@graasp/websockets": "github:graasp/graasp-websockets.git", "@testing-library/jest-dom": "^5.16.3", "@testing-library/react": "^12.1.4", "@testing-library/user-event": "^13.5.0", diff --git a/src/components/item/ItemContent.js b/src/components/item/ItemContent.js index 8ae3b4fec..3f7084d19 100644 --- a/src/components/item/ItemContent.js +++ b/src/components/item/ItemContent.js @@ -1,9 +1,15 @@ -import React, { useContext } from 'react'; -import PropTypes from 'prop-types'; -import { Record } from 'immutable'; +import { Api, MUTATION_KEYS } from '@graasp/query-client'; +import { AppItem, DocumentItem, FileItem, H5PItem, LinkItem } from '@graasp/ui'; import { makeStyles } from '@material-ui/core'; -import { FileItem, DocumentItem, LinkItem, AppItem } from '@graasp/ui'; -import { MUTATION_KEYS, Api } from '@graasp/query-client'; +import { Record } from 'immutable'; +import PropTypes from 'prop-types'; +import React, { useContext } from 'react'; +import { + API_HOST, + CONTEXT_BUILDER, + H5P_INTEGRATION_URL, + ITEM_DEFAULT_HEIGHT, +} from '../../config/constants'; import { hooks, useMutation } from '../../config/queryClient'; import { buildFileItemId, @@ -13,19 +19,14 @@ import { ITEM_SCREEN_ERROR_ALERT_ID, } from '../../config/selectors'; import { ITEM_KEYS, ITEM_TYPES } from '../../enums'; -import Loader from '../common/Loader'; +import { buildDocumentExtra, getDocumentExtra } from '../../utils/itemExtra'; import ErrorAlert from '../common/ErrorAlert'; -import { - API_HOST, - ITEM_DEFAULT_HEIGHT, - CONTEXT_BUILDER, -} from '../../config/constants'; +import Loader from '../common/Loader'; +import { CurrentUserContext } from '../context/CurrentUserContext'; import { LayoutContext } from '../context/LayoutContext'; +import ItemActions from '../main/ItemActions'; import Items from '../main/Items'; -import { buildDocumentExtra, getDocumentExtra } from '../../utils/itemExtra'; import NewItemButton from '../main/NewItemButton'; -import { CurrentUserContext } from '../context/CurrentUserContext'; -import ItemActions from '../main/ItemActions'; const { useChildren, useFileContent } = hooks; @@ -173,6 +174,20 @@ const ItemContent = ({ item, enableEditing, permission }) => { /> ); + case ITEM_TYPES.H5P: { + const contentId = item.get('extra')?.h5p?.contentId; + if (!contentId) { + return ; + } + + return ( + + ); + } default: return ; diff --git a/src/components/main/ImportH5P.js b/src/components/main/ImportH5P.js new file mode 100644 index 000000000..b8cc88b49 --- /dev/null +++ b/src/components/main/ImportH5P.js @@ -0,0 +1,99 @@ +import { routines } from '@graasp/query-client'; +import Typography from '@material-ui/core/Typography'; +import '@uppy/dashboard/dist/style.css'; +import { Dashboard } from '@uppy/react'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMatch } from 'react-router'; +import notifier from '../../config/notifier'; +import { buildItemPath } from '../../config/paths'; +import { H5P_DASHBOARD_UPLOADER_ID } from '../../config/selectors'; +import { configureH5PImportUppy } from '../../utils/uppy'; + +const ImportH5P = () => { + const [uppy, setUppy] = useState(null); + const match = useMatch(buildItemPath()); + const itemId = match?.params?.itemId; + const { t } = useTranslation(); + + const onComplete = (result) => { + // update app on complete + // todo: improve with websockets or by receiving corresponding items + if (!result?.failed.length) { + notifier({ type: routines.importH5PRoutine.SUCCESS }); + } + }; + const onError = (error) => { + notifier({ type: routines.importH5PRoutine.FAILURE, payload: { error } }); + }; + + const onUpload = () => { + notifier({ type: routines.importH5PRoutine.REQUEST }); + }; + + const applyUppy = () => + setUppy( + configureH5PImportUppy({ + itemId, + onComplete, + onError, + onUpload, + }), + ); + + useEffect(() => { + applyUppy(); + + return () => { + uppy?.close(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + applyUppy(); + // update uppy configuration each time itemId changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [itemId]); + + if (!uppy) { + return null; + } + + return ( + <> + {t('Import H5P rich content')} + + {t( + 'You can upload H5P rich content by uploading exported .h5p files (e.g. from H5P.com, external Moodle services, etc).', + )} + +
+ + {t( + 'Once your file is accepted, it will take several minutes for it to be available.', + )} + +
+ +
+ + ); +}; + +export default ImportH5P; diff --git a/src/components/main/ItemTypeTabs.js b/src/components/main/ItemTypeTabs.js index 417763b35..967072db4 100644 --- a/src/components/main/ItemTypeTabs.js +++ b/src/components/main/ItemTypeTabs.js @@ -11,6 +11,7 @@ import { CREATE_ITEM_DOCUMENT_ID, CREATE_ITEM_APP_ID, CREATE_ITEM_ZIP_ID, + CREATE_ITEM_H5P_ID, } from '../../config/selectors'; const useStyles = makeStyles((theme) => ({ @@ -97,6 +98,13 @@ const ItemTypeTabs = ({ onTypeChange, initialValue }) => { icon={zipIcon} classes={{ wrapper: classes.wrapper }} /> + } + classes={{ wrapper: classes.wrapper }} + /> ); }; diff --git a/src/components/main/NewItemModal.js b/src/components/main/NewItemModal.js index 55d14c6f9..3c14b0f5f 100644 --- a/src/components/main/NewItemModal.js +++ b/src/components/main/NewItemModal.js @@ -25,6 +25,7 @@ import AppForm from '../item/form/AppForm'; import ItemTypeTabs from './ItemTypeTabs'; import ImportZip from './ImportZip'; import { DOUBLE_CLICK_DELAY_MS } from '../../config/constants'; +import ImportH5P from './ImportH5P'; const useStyles = makeStyles((theme) => ({ dialogContent: { @@ -104,6 +105,8 @@ const NewItemModal = ({ open, handleClose }) => { return ; case ITEM_TYPES.ZIP: return ; + case ITEM_TYPES.H5P: + return ; case ITEM_TYPES.APP: return ( { ); case ITEM_TYPES.FILE: case ITEM_TYPES.ZIP: + case ITEM_TYPES.H5P: return (