Skip to content

Commit

Permalink
feat: add H5P import and display (#383)
Browse files Browse the repository at this point in the history
* feat: add H5P upload tab

* feat: add H5P player display stubs

* feat: add path builder for h5p content

* fix: update h5p integration within iframe

* refactor: apply PR feedback (#383)

* refactor: use h5p iframe integration

* build: add h5p env var in workflows
  • Loading branch information
Alexandre Chau authored Aug 17, 2022
1 parent eb58f71 commit ef4d3b7
Show file tree
Hide file tree
Showing 19 changed files with 224 additions and 48 deletions.
19 changes: 10 additions & 9 deletions .github/workflows/cdelivery-s3-caller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
21 changes: 11 additions & 10 deletions .github/workflows/cdeployment-s3-caller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
19 changes: 10 additions & 9 deletions .github/workflows/cintegration-s3-caller.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 29 additions & 14 deletions src/components/item/ItemContent.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -173,6 +174,20 @@ const ItemContent = ({ item, enableEditing, permission }) => {
/>
</>
);
case ITEM_TYPES.H5P: {
const contentId = item.get('extra')?.h5p?.contentId;
if (!contentId) {
return <ErrorAlert id={ITEM_SCREEN_ERROR_ALERT_ID} />;
}

return (
<H5PItem
itemId={itemId}
contentId={contentId}
integrationUrl={H5P_INTEGRATION_URL}
/>
);
}

default:
return <ErrorAlert id={ITEM_SCREEN_ERROR_ALERT_ID} />;
Expand Down
99 changes: 99 additions & 0 deletions src/components/main/ImportH5P.js
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Typography variant="h6">{t('Import H5P rich content')}</Typography>
<Typography variant="body">
{t(
'You can upload H5P rich content by uploading exported .h5p files (e.g. from H5P.com, external Moodle services, etc).',
)}
</Typography>
<br />
<Typography variant="body">
{t(
'Once your file is accepted, it will take several minutes for it to be available.',
)}
</Typography>
<div id={H5P_DASHBOARD_UPLOADER_ID}>
<Dashboard
uppy={uppy}
height={200}
width="100%"
proudlyDisplayPoweredByUppy={false}
note={t('Upload a file')}
locale={{
strings: {
// Text to show on the droppable area.
// `%{browse}` is replaced with a link that opens the system file selection dialog.
dropPaste: `${t('Drop here or')} %{browse}`,
// Used as the label for the link that opens the system file selection dialog.
browse: t('Browse'),
},
}}
/>
</div>
</>
);
};

export default ImportH5P;
8 changes: 8 additions & 0 deletions src/components/main/ItemTypeTabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down Expand Up @@ -97,6 +98,13 @@ const ItemTypeTabs = ({ onTypeChange, initialValue }) => {
icon={zipIcon}
classes={{ wrapper: classes.wrapper }}
/>
<Tab
id={CREATE_ITEM_H5P_ID}
value={ITEM_TYPES.H5P}
label={t('Import H5P')}
icon={<ItemIcon type={ITEM_TYPES.H5P} iconClass={classes.icon} />}
classes={{ wrapper: classes.wrapper }}
/>
</Tabs>
);
};
Expand Down
4 changes: 4 additions & 0 deletions src/components/main/NewItemModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -104,6 +105,8 @@ const NewItemModal = ({ open, handleClose }) => {
return <FileDashboardUploader />;
case ITEM_TYPES.ZIP:
return <ImportZip />;
case ITEM_TYPES.H5P:
return <ImportH5P />;
case ITEM_TYPES.APP:
return (
<AppForm
Expand Down Expand Up @@ -158,6 +161,7 @@ const NewItemModal = ({ open, handleClose }) => {
);
case ITEM_TYPES.FILE:
case ITEM_TYPES.ZIP:
case ITEM_TYPES.H5P:
return (
<Button id={CREATE_ITEM_CLOSE_BUTTON_ID} onClick={handleClose}>
{t('Close')}
Expand Down
12 changes: 10 additions & 2 deletions src/config/constants.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Context, buildSignInPath } from '@graasp/utils';
import { buildSignInPath, Context } from '@graasp/utils';
import { ITEM_LAYOUT_MODES, ITEM_TYPES, PERMISSION_LEVELS } from '../enums';
import env from '../env.json';
import { ITEM_LAYOUT_MODES, PERMISSION_LEVELS, ITEM_TYPES } from '../enums';

const {
API_HOST: ENV_API_HOST,
Expand All @@ -11,6 +11,7 @@ const {
GA_MEASUREMENT_ID: ENV_GA_MEASUREMENT_ID,
HIDDEN_ITEM_TAG_ID: ENV_HIDDEN_ITEM_TAG_ID,
GRAASP_EXPLORE_HOST: ENV_GRAASP_EXPLORE_HOST,
H5P_INTEGRATION_URL: ENV_H5P_INTEGRATION_URL,
REACT_APP_SENTRY_DSN: ENV_SENTRY_DSN,
DOMAIN: ENV_DOMAIN,
} = env;
Expand Down Expand Up @@ -53,6 +54,11 @@ export const GRAASP_LIBRARY_HOST =
process.env.REACT_APP_GRAASP_EXPLORE_HOST ||
'http://localhost:3005';

export const H5P_INTEGRATION_URL =
ENV_H5P_INTEGRATION_URL ||
process.env.REACT_APP_H5P_INTEGRATION_URL ||
`${API_HOST}/p/h5p-integration`;

export const GRAASP_ANALYZER_HOST =
process.env.REACT_APP_GRAASP_ANALYZER_HOST || 'http://localhost:3113';

Expand Down Expand Up @@ -190,6 +196,8 @@ export const THUMBNAIL_EXTENSION = 'image/jpeg';
export const THUMBNAIL_SETTING_MAX_WIDTH = 300;
export const THUMBNAIL_SETTING_MAX_HEIGHT = 200;

export const H5P_FILE_DOT_EXTENSION = '.h5p';

export const CATEGORY_TYPES = {
LEVEL: 'level',
DISCIPLINE: 'discipline',
Expand Down
2 changes: 2 additions & 0 deletions src/config/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const IMPORT_ZIP_FAILURE_MESSAGE =
'An error occurred while importing The ZIP archive.';
export const IMPORT_ZIP_PROGRESS_MESSAGE =
'The ZIP is being processed. Please wait a moment.';
export const IMPORT_H5P_PROGRESS_MESSAGE =
'The H5P is being processed. Please wait a moment.';
export const EXPORT_ZIP_FAILURE_MESSAGE =
'An error occurred while downloading the item as ZIP archive. Please try again later.';

Expand Down
Loading

0 comments on commit ef4d3b7

Please sign in to comment.