Skip to content

Commit 8e7ca2a

Browse files
committed
Visualization improvements and error reporting.
1 parent 9ec8448 commit 8e7ca2a

File tree

4 files changed

+59
-23
lines changed

4 files changed

+59
-23
lines changed

src/components/Exercises/FilesTable/FilesLinksTable.js

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useState } from 'react';
22
import PropTypes from 'prop-types';
3+
import ImmutablePropTypes from 'react-immutable-proptypes';
34
import { injectIntl, FormattedMessage } from 'react-intl';
45
import { Table, Modal } from 'react-bootstrap';
56
import { CopyToClipboard } from 'react-copy-to-clipboard';
@@ -9,18 +10,36 @@ import ExerciseFilesTableContainer from '../../../containers/ExerciseFilesTableC
910
import ExerciseFileLinkForm, { initialValues } from '../../forms/ExerciseFileLinkForm';
1011
import Button, { TheButtonGroup } from '../../widgets/TheButton';
1112
import Icon, { AddIcon, DeleteIcon, DownloadIcon, EditIcon, LoadingIcon, WarningIcon, VisibleIcon } from '../../icons';
13+
import Callout from '../../widgets/Callout/Callout.js';
14+
import Explanation from '../../widgets/Explanation';
1215
import { prettyPrintBytes } from '../../helpers/stringFormatters.js';
1316
import { UserRoleIcon } from '../../helpers/usersRoles.js';
14-
import Explanation from '../../widgets/Explanation';
1517
import { getFileLinkUrl } from '../../../helpers/localizedData.js';
18+
import { resourceStatus } from '../../../redux/helpers/resourceManager';
19+
import { getErrorMessage } from '../../../locales/apiErrorMessages.js';
1620

1721
const sortLinks = lruMemoize(links => links.slice().sort((a, b) => a.key.localeCompare(b.key)));
1822

19-
const FilesLinksTable = ({ exercise, links, files = null, createLink, updateLink, deleteLink }) => {
23+
const FilesLinksTable = ({
24+
exercise,
25+
links,
26+
files = null,
27+
operation,
28+
createLink,
29+
updateLink,
30+
deleteLink,
31+
intl: { formatMessage },
32+
}) => {
2033
const [filesOpen, setFilesOpen] = useState(false);
2134
const [formOpen, setFormOpen] = useState(false);
2235
const [editLink, setEditLink] = useState(null);
2336

37+
const operationPending = operation && operation.get('state') === resourceStatus.PENDING;
38+
const operationFailed = operation && operation.get('state') === resourceStatus.FAILED;
39+
const operationError = operationFailed ? operation.get('error')?.toJS() : null;
40+
const operationType = operation && operation.get('type');
41+
const operationLinkId = operation && operation.get('linkId');
42+
2443
return (
2544
<div>
2645
{links.length > 0 && (
@@ -64,7 +83,7 @@ const FilesLinksTable = ({ exercise, links, files = null, createLink, updateLink
6483
<tr key={link.id}>
6584
<td className="shrink-col text-center">
6685
{link.requiredRole ? (
67-
<UserRoleIcon role={link.requiredRole} />
86+
<UserRoleIcon role={link.requiredRole} showTooltip tooltipId={`visibility-${link.id}`} />
6887
) : (
6988
<VisibleIcon
7089
className="text-primary"
@@ -74,7 +93,7 @@ const FilesLinksTable = ({ exercise, links, files = null, createLink, updateLink
7493
defaultMessage="Visible to all users (without login)"
7594
/>
7695
}
77-
tooltipId={`visible-${link.id}`}
96+
tooltipId={`visibility-${link.id}`}
7897
tooltipPlacement="bottom"
7998
/>
8099
)}
@@ -121,24 +140,29 @@ const FilesLinksTable = ({ exercise, links, files = null, createLink, updateLink
121140
<Button
122141
variant="warning"
123142
size="xs"
124-
disabled={!files}
143+
disabled={!files || operationPending}
125144
onClick={() => {
126145
setFormOpen(true);
127146
setEditLink(link);
128147
}}>
129-
<EditIcon
130-
fixedWidth
131-
tooltip={<FormattedMessage id="generic.edit" defaultMessage="Edit" />}
132-
tooltipId={`edit-${link.id}`}
133-
tooltipPlacement="bottom"
134-
/>
148+
{operationPending && operationType === 'update' && operationLinkId === link.id ? (
149+
<LoadingIcon />
150+
) : (
151+
<EditIcon
152+
fixedWidth
153+
tooltip={<FormattedMessage id="generic.edit" defaultMessage="Edit" />}
154+
tooltipId={`edit-${link.id}`}
155+
tooltipPlacement="bottom"
156+
/>
157+
)}
135158
</Button>
136159
)}
137160

138161
{Boolean(deleteLink) && (
139162
<Button
140163
variant="danger"
141164
size="xs"
165+
disabled={operationPending}
142166
confirm={
143167
<FormattedMessage
144168
id="app.filesLinksTable.deleteLinkConfirm"
@@ -147,12 +171,16 @@ const FilesLinksTable = ({ exercise, links, files = null, createLink, updateLink
147171
}
148172
confirmId={`delete-link-${link.id}`}
149173
onClick={() => deleteLink(link.id)}>
150-
<DeleteIcon
151-
fixedWidth
152-
tooltip={<FormattedMessage id="generic.delete" defaultMessage="Delete" />}
153-
tooltipId={`delete-${link.id}`}
154-
tooltipPlacement="bottom"
155-
/>
174+
{operationPending && operationType === 'remove' && operationLinkId === link.id ? (
175+
<LoadingIcon />
176+
) : (
177+
<DeleteIcon
178+
fixedWidth
179+
tooltip={<FormattedMessage id="generic.delete" defaultMessage="Delete" />}
180+
tooltipId={`delete-${link.id}`}
181+
tooltipPlacement="bottom"
182+
/>
183+
)}
156184
</Button>
157185
)}
158186
</TheButtonGroup>
@@ -170,22 +198,24 @@ const FilesLinksTable = ({ exercise, links, files = null, createLink, updateLink
170198
</p>
171199
)}
172200

201+
{operationError && <Callout variant="danger">{getErrorMessage(formatMessage)(operationError)}</Callout>}
202+
173203
<div className="text-center">
174204
<TheButtonGroup>
175-
<Button variant="primary" onClick={() => setFilesOpen(true)}>
205+
<Button variant="primary" disabled={operationPending} onClick={() => setFilesOpen(true)}>
176206
<Icon icon="folder-tree" gapRight={2} />
177207
<FormattedMessage id="app.filesLinksTable.manageExerciseFiles" defaultMessage="Manage Exercise Files" />
178208
</Button>
179209

180210
{Boolean(createLink) && (
181211
<Button
182212
variant="success"
183-
disabled={!files}
213+
disabled={!files || operationPending}
184214
onClick={() => {
185215
setFormOpen(true);
186216
setEditLink(null);
187217
}}>
188-
<AddIcon gapRight={2} />
218+
{operationPending && operationType === 'create' ? <LoadingIcon gapRight={2} /> : <AddIcon gapRight={2} />}
189219
<FormattedMessage id="app.filesLinksTable.addLink" defaultMessage="Add Link" />
190220
</Button>
191221
)}
@@ -238,9 +268,11 @@ FilesLinksTable.propTypes = {
238268
}).isRequired,
239269
links: PropTypes.array.isRequired,
240270
files: PropTypes.object,
271+
operation: ImmutablePropTypes.map,
241272
createLink: PropTypes.func,
242273
updateLink: PropTypes.func,
243274
deleteLink: PropTypes.func,
275+
intl: PropTypes.object,
244276
};
245277

246278
export default injectIntl(FilesLinksTable);

src/containers/ExerciseFilesLinksContainer/ExerciseFilesLinksContainer.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
removeExerciseFileLink,
1818
} from '../../redux/modules/exerciseFilesLinks.js';
1919
import { getFilesForExercise, fetchFilesForExerciseStatus } from '../../redux/selectors/exerciseFiles.js';
20-
import { getExerciseFilesLinks } from '../../redux/selectors/exerciseFilesLinks.js';
20+
import { getExerciseFilesLinks, getExerciseFilesLinksOperation } from '../../redux/selectors/exerciseFilesLinks.js';
2121
import { isFailedState, isLoadingState } from '../../redux/helpers/resourceManager/status.js';
2222
import { objectMap } from '../../helpers/common.js';
2323

@@ -48,6 +48,7 @@ class ExerciseFilesLinksContainer extends Component {
4848
const {
4949
exercise,
5050
exerciseFilesLinks,
51+
exerciseFilesLinksOperation,
5152
exerciseFiles,
5253
exerciseFilesStatus,
5354
createLink,
@@ -67,6 +68,7 @@ class ExerciseFilesLinksContainer extends Component {
6768
exercise={exercise}
6869
links={Object.values(links)}
6970
files={extractExerciseFiles(exerciseFiles, exerciseFilesStatus)}
71+
operation={exerciseFilesLinksOperation}
7072
createLink={createLink}
7173
updateLink={updateLink}
7274
deleteLink={deleteLink}
@@ -86,6 +88,7 @@ ExerciseFilesLinksContainer.propTypes = {
8688
permissionHints: PropTypes.object.isRequired,
8789
}).isRequired,
8890
exerciseFilesLinks: ImmutablePropTypes.map,
91+
exerciseFilesLinksOperation: ImmutablePropTypes.map,
8992
exerciseFiles: ImmutablePropTypes.map,
9093
exerciseFilesStatus: PropTypes.string,
9194
loadLinks: PropTypes.func.isRequired,
@@ -104,6 +107,7 @@ export default connect(
104107
(state, { exercise }) => {
105108
return {
106109
exerciseFilesLinks: getExerciseFilesLinks(state, exercise.id),
110+
exerciseFilesLinksOperation: getExerciseFilesLinksOperation(state, exercise.id),
107111
exerciseFiles: getFilesForExercise(exercise.id)(state),
108112
exerciseFilesStatus: fetchFilesForExerciseStatus(state)(exercise.id),
109113
};

src/locales/cs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2170,4 +2170,4 @@
21702170
"recodex-judge-shuffle-all": "Sudí neuspořádaných tokenů a řádků",
21712171
"recodex-judge-shuffle-newline": "Sudí neuspořádaných tokenů (ignorující konce řádků)",
21722172
"recodex-judge-shuffle-rows": "Sudí neuspořádaných řádků"
2173-
}
2173+
}

src/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2170,4 +2170,4 @@
21702170
"recodex-judge-shuffle-all": "Unordered-tokens-and-rows judge",
21712171
"recodex-judge-shuffle-newline": "Unordered-tokens judge (ignoring ends of lines)",
21722172
"recodex-judge-shuffle-rows": "Unordered-rows judge"
2173-
}
2173+
}

0 commit comments

Comments
 (0)