Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/ReCodEx/web-app into grou…
Browse files Browse the repository at this point in the history
…p-tree-improvements
  • Loading branch information
Martin Krulis committed Oct 16, 2017
2 parents edf817f + dccc7e9 commit 99c6156
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 125 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { LoadingIcon, WarningIcon } from '../../icons';

const isLoading = status => status === 'PENDING';
const hasFailed = status => status === 'FAILED';

const defaultLoading = noIcons =>
<span>
{!noIcons && <LoadingIcon />}{' '}
<FormattedMessage
id="app.resourceRenderer.loading"
defaultMessage="Loading ..."
/>
</span>;

const defaultFailed = noIcons =>
<span>
{!noIcons && <WarningIcon />}{' '}
<FormattedMessage
id="app.resourceRenderer.loadingFailed"
defaultMessage="Loading failed."
/>
</span>;

const FetchManyResourceRenderer = ({
noIcons = false,
loading = defaultLoading(noIcons),
failed = defaultFailed(noIcons),
children: ready,
fetchManyStatus,
hiddenUntilReady = false,
forceLoading = false
}) => {
const stillLoading =
!fetchManyStatus || isLoading(fetchManyStatus) || forceLoading;
return stillLoading
? hiddenUntilReady ? null : loading
: hasFailed(fetchManyStatus) ? (hiddenUntilReady ? null : failed) : ready(); // display all ready items
};

FetchManyResourceRenderer.propTypes = {
loading: PropTypes.element,
failed: PropTypes.element,
children: PropTypes.func.isRequired,
fetchManyStatus: PropTypes.string,
hiddenUntilReady: PropTypes.bool,
forceLoading: PropTypes.bool,
noIcons: PropTypes.bool
};

export default FetchManyResourceRenderer;
1 change: 1 addition & 0 deletions src/components/helpers/FetchManyResourceRenderer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default from './FetchManyResourceRenderer';
10 changes: 4 additions & 6 deletions src/components/layout/Page/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ const Page = ({
),
children,
...props
}) => (
}) =>
<ResourceRenderer
resource={resource}
loading={
<PageContent title={loadingTitle} description={loadingDescription} />
}
failed={<PageContent title={failedTitle} description={failedDescription} />}
>
{(...resources) => (
{(...resources) =>
<PageContent
{...props}
title={typeof title === 'function' ? title(...resources) : title}
Expand All @@ -50,10 +50,8 @@ const Page = ({
}
>
{typeof children === 'function' ? children(...resources) : children}
</PageContent>
)}
</ResourceRenderer>
);
</PageContent>}
</ResourceRenderer>;

const stringOrFormattedMessage = PropTypes.oneOfType([
PropTypes.string,
Expand Down
144 changes: 90 additions & 54 deletions src/pages/Exercises/Exercises.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ import {
ButtonGroup
} from 'react-bootstrap';

import Page from '../../components/layout/Page';
import PageContent from '../../components/layout/PageContent';
import Box from '../../components/widgets/Box';
import { AddIcon, EditIcon, SearchIcon } from '../../components/icons';
import { exercisesSelector } from '../../redux/selectors/exercises';
import {
exercisesSelector,
fetchManyStatus
} from '../../redux/selectors/exercises';
import { canEditExercise } from '../../redux/selectors/users';
import { loggedInUserIdSelector } from '../../redux/selectors/auth';
import {
Expand All @@ -27,6 +30,8 @@ import {
} from '../../redux/modules/exercises';
import ExercisesList from '../../components/Exercises/ExercisesList';

import FetchManyResourceRenderer from '../../components/helpers/FetchManyResourceRenderer';
import { getJsData } from '../../redux/helpers/resourceManager';
import withLinks from '../../hoc/withLinks';

class Exercises extends Component {
Expand Down Expand Up @@ -71,40 +76,74 @@ class Exercises extends Component {

render() {
const {
exercises,
isAuthorOfExercise,
exercises,
fetchStatus,
links: { EXERCISE_EDIT_URI_FACTORY, EXERCISE_EDIT_CONFIG_URI_FACTORY }
} = this.props;

return (
<Page
resource={exercises.toArray()}
title={
<FormattedMessage
id="app.exercises.title"
defaultMessage="Exercise list"
<FetchManyResourceRenderer
fetchManyStatus={fetchStatus}
loading={
<PageContent
title={
<FormattedMessage
id="app.page.exercises.loading"
defaultMessage="Loading list of exercises ..."
/>
}
description={
<FormattedMessage
id="app.page.exercises.loadingDescription"
defaultMessage="Please wait while we are getting the list of exercises ready."
/>
}
/>
}
description={
<FormattedMessage
id="app.instance.description"
defaultMessage="List and assign exercises to your groups."
failed={
<PageContent
title={
<FormattedMessage
id="app.page.exercises.failed"
defaultMessage="Cannot load the list of exercises"
/>
}
description={
<FormattedMessage
id="app.page.exercises.failedDescription"
defaultMessage="We are sorry for the inconvenience, please try again later."
/>
}
/>
}
breadcrumbs={[
{
text: (
>
{() =>
<PageContent
title={
<FormattedMessage
id="app.exercises.title"
defaultMessage="Exercise list"
/>
),
iconName: 'puzzle-piece'
}
]}
>
{(...exercises) => (
<div>
}
description={
<FormattedMessage
id="app.instance.description"
defaultMessage="List and assign exercises to your groups."
/>
}
breadcrumbs={[
{
text: (
<FormattedMessage
id="app.exercises.title"
defaultMessage="Exercise list"
/>
),
iconName: 'puzzle-piece'
}
]}
>
<Box
title={
<FormattedMessage
Expand Down Expand Up @@ -153,7 +192,7 @@ class Exercises extends Component {
type="submit"
onClick={e => {
e.preventDefault();
this.onChange(this.query, exercises);
this.onChange(this.query, exercises.map(getJsData));
}}
disabled={false}
>
Expand All @@ -166,37 +205,33 @@ class Exercises extends Component {
<ExercisesList
exercises={this.state.visibleExercises}
createActions={id =>
isAuthorOfExercise(id) && (
<ButtonGroup>
<LinkContainer to={EXERCISE_EDIT_URI_FACTORY(id)}>
<Button bsSize="xs" bsStyle="warning">
<EditIcon />{' '}
<FormattedMessage
id="app.exercises.listEdit"
defaultMessage="Edit"
/>
</Button>
</LinkContainer>
<LinkContainer
to={EXERCISE_EDIT_CONFIG_URI_FACTORY(id)}
>
<Button bsSize="xs" bsStyle="warning">
<EditIcon />{' '}
<FormattedMessage
id="app.exercises.listEditConfig"
defaultMessage="Edit config"
/>
</Button>
</LinkContainer>
<DeleteExerciseButtonContainer id={id} bsSize="xs" />
</ButtonGroup>
)}
isAuthorOfExercise(id) &&
<ButtonGroup>
<LinkContainer to={EXERCISE_EDIT_URI_FACTORY(id)}>
<Button bsSize="xs" bsStyle="warning">
<EditIcon />{' '}
<FormattedMessage
id="app.exercises.listEdit"
defaultMessage="Edit"
/>
</Button>
</LinkContainer>
<LinkContainer to={EXERCISE_EDIT_CONFIG_URI_FACTORY(id)}>
<Button bsSize="xs" bsStyle="warning">
<EditIcon />{' '}
<FormattedMessage
id="app.exercises.listEditConfig"
defaultMessage="Edit config"
/>
</Button>
</LinkContainer>
<DeleteExerciseButtonContainer id={id} bsSize="xs" />
</ButtonGroup>}
/>
</div>
</Box>
</div>
)}
</Page>
</PageContent>}
</FetchManyResourceRenderer>
);
}
}
Expand All @@ -207,16 +242,17 @@ Exercises.propTypes = {
createExercise: PropTypes.func.isRequired,
isAuthorOfExercise: PropTypes.func.isRequired,
push: PropTypes.func.isRequired,
links: PropTypes.object.isRequired
links: PropTypes.object.isRequired,
fetchStatus: PropTypes.string
};

export default withLinks(
connect(
state => {
const userId = loggedInUserIdSelector(state);

return {
exercises: exercisesSelector(state),
fetchStatus: fetchManyStatus(state),
isAuthorOfExercise: exerciseId =>
canEditExercise(userId, exerciseId)(state)
};
Expand Down
42 changes: 42 additions & 0 deletions src/redux/helpers/api/download.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createApiAction } from '../../middleware/apiMiddleware';
import { saveAs } from 'file-saver';
import { addNotification } from '../../modules/notifications';

export const downloadHelper = ({
fetch,
endpoint,
actionType,
fileNameSelector,
contentType
}) => id => (dispatch, getState) =>
dispatch(fetch(id))
.then(() =>
dispatch(
createApiAction({
type: actionType,
method: 'GET',
endpoint: endpoint(id),
doNotProcess: true // the response is not (does not have to be) a JSON
})
)
)
.then(result => {
const { value: { ok, status } } = result;
if (ok === false) {
const msg =
status === 404
? 'The file could not be found on the server.'
: `This file cannot be downloaded (${status}).`;
throw new Error(msg);
}

return result;
})
.then(({ value }) => value.blob())
.then(blob => {
const typedBlob = new Blob([blob], { type: contentType });
const fileName = fileNameSelector(id, getState());
saveAs(typedBlob, fileName);
return Promise.resolve();
})
.catch(e => dispatch(addNotification(e.message, false)));
2 changes: 1 addition & 1 deletion src/redux/helpers/resourceManager/reducerFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const reducerFactory = (actionTypes, id = 'id') => ({
),
state
)
.setIn(['fetchManyStatus', endpoint], resourceStatus.LOADED),
.setIn(['fetchManyStatus', endpoint], resourceStatus.FULFILLED),

[actionTypes.UPDATE_FULFILLED]: (state, { payload, meta: { id } }) =>
state.setIn(['resources', id, 'data'], fromJS(payload)),
Expand Down
4 changes: 3 additions & 1 deletion src/redux/modules/exercises.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ export const fetchExercisesIfNeeded = actions.fetchIfNeeded;
export const fetchExercise = actions.fetchResource;
export const fetchExerciseIfNeeded = actions.fetchOneIfNeeded;

export const fetchManyEndpoint = '/exercises';

export const fetchExercises = () =>
actions.fetchMany({
endpoint: '/exercises'
endpoint: fetchManyEndpoint
});

export const fetchGroupExercises = groupId =>
Expand Down
Loading

0 comments on commit 99c6156

Please sign in to comment.