11import React , { useState } from 'react' ;
22import PropTypes from 'prop-types' ;
3+ import ImmutablePropTypes from 'react-immutable-proptypes' ;
34import { injectIntl , FormattedMessage } from 'react-intl' ;
45import { Table , Modal } from 'react-bootstrap' ;
56import { CopyToClipboard } from 'react-copy-to-clipboard' ;
@@ -9,18 +10,36 @@ import ExerciseFilesTableContainer from '../../../containers/ExerciseFilesTableC
910import ExerciseFileLinkForm , { initialValues } from '../../forms/ExerciseFileLinkForm' ;
1011import Button , { TheButtonGroup } from '../../widgets/TheButton' ;
1112import Icon , { AddIcon , DeleteIcon , DownloadIcon , EditIcon , LoadingIcon , WarningIcon , VisibleIcon } from '../../icons' ;
13+ import Callout from '../../widgets/Callout/Callout.js' ;
14+ import Explanation from '../../widgets/Explanation' ;
1215import { prettyPrintBytes } from '../../helpers/stringFormatters.js' ;
1316import { UserRoleIcon } from '../../helpers/usersRoles.js' ;
14- import Explanation from '../../widgets/Explanation' ;
1517import { getFileLinkUrl } from '../../../helpers/localizedData.js' ;
18+ import { resourceStatus } from '../../../redux/helpers/resourceManager' ;
19+ import { getErrorMessage } from '../../../locales/apiErrorMessages.js' ;
1620
1721const 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
246278export default injectIntl ( FilesLinksTable ) ;
0 commit comments