Skip to content

Commit b537949

Browse files
committed
Adding icon mark and usage info for exercise files that has been linked (so they cannot be deleted).
1 parent e77c08a commit b537949

File tree

8 files changed

+88
-49
lines changed

8 files changed

+88
-49
lines changed

src/components/Exercises/FilesTable/ExerciseFilesTableRow.js

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Button from '../../widgets/TheButton';
77
import DateTime from '../../widgets/DateTime';
88
import { prettyPrintBytes } from '../../helpers/stringFormatters.js';
99
import Confirm from '../../../components/forms/Confirm';
10-
import Icon, { DeleteIcon } from '../../../components/icons';
10+
import Icon, { DeleteIcon, LinkIcon } from '../../../components/icons';
1111

1212
const ExerciseFilesTableRow = ({
1313
id,
@@ -18,6 +18,7 @@ const ExerciseFilesTableRow = ({
1818
removeFile,
1919
viewOnly,
2020
isBeingUsed = false,
21+
linkedFilesIds,
2122
}) => (
2223
<tr>
2324
<td>
@@ -28,6 +29,20 @@ const ExerciseFilesTableRow = ({
2829
) : (
2930
<span>{name}</span>
3031
)}
32+
{linkedFilesIds?.has(id) && (
33+
<LinkIcon
34+
className="text-warning"
35+
gapLeft={2}
36+
tooltipId={`linked-${id}`}
37+
tooltipPlacement="bottom"
38+
tooltip={
39+
<FormattedMessage
40+
id="app.exerciseFiles.linkedFile"
41+
defaultMessage="This file has been made accessible via a link."
42+
/>
43+
}
44+
/>
45+
)}
3146
</td>
3247
<td>{prettyPrintBytes(size)}</td>
3348
<td>
@@ -36,7 +51,34 @@ const ExerciseFilesTableRow = ({
3651
{!viewOnly && (
3752
<td className="shrink-col text-nowrap">
3853
{removeFile &&
39-
(!isBeingUsed ? (
54+
(isBeingUsed || linkedFilesIds?.has(id) ? (
55+
<OverlayTrigger
56+
placement="bottom"
57+
overlay={
58+
<Tooltip id={`cannot-delete-${id}`}>
59+
{isBeingUsed ? (
60+
<FormattedMessage
61+
id="app.exerciseFiles.cannotDeleteUsedExplain"
62+
defaultMessage="The file cannot be deleted since it is being used in the configuration."
63+
/>
64+
) : (
65+
<FormattedMessage
66+
id="app.exerciseFiles.cannotDeleteLinkedExplain"
67+
defaultMessage="The file cannot be deleted since it has an associated download link."
68+
/>
69+
)}
70+
</Tooltip>
71+
}>
72+
<em className="text-body-secondary">
73+
{isBeingUsed ? (
74+
<Icon icon="paperclip" gapRight={2} className="text-success" />
75+
) : (
76+
<LinkIcon className="text-success" gapRight={2} />
77+
)}
78+
<FormattedMessage id="generic.inUse" defaultMessage="in use" />
79+
</em>
80+
</OverlayTrigger>
81+
) : (
4082
<Confirm
4183
id={id}
4284
onConfirmed={() => removeFile(id)}
@@ -52,22 +94,6 @@ const ExerciseFilesTableRow = ({
5294
<FormattedMessage id="generic.delete" defaultMessage="Delete" />
5395
</Button>
5496
</Confirm>
55-
) : (
56-
<OverlayTrigger
57-
placement="bottom"
58-
overlay={
59-
<Tooltip id={`cannot-delete-${id}`}>
60-
<FormattedMessage
61-
id="app.exerciseFiles.cannotDeleteExplain"
62-
defaultMessage="The file cannot be deleted since it is being used in the configuration."
63-
/>
64-
</Tooltip>
65-
}>
66-
<em className="text-body-secondary">
67-
<Icon icon="paperclip" gapRight={2} className="text-success" />
68-
<FormattedMessage id="generic.inUse" defaultMessage="in use" />
69-
</em>
70-
</OverlayTrigger>
7197
))}
7298
</td>
7399
)}
@@ -83,6 +109,7 @@ ExerciseFilesTableRow.propTypes = {
83109
removeFile: PropTypes.func,
84110
viewOnly: PropTypes.bool,
85111
isBeingUsed: PropTypes.bool,
112+
linkedFilesIds: PropTypes.instanceOf(Set),
86113
};
87114

88115
export default ExerciseFilesTableRow;

src/components/Exercises/FilesTable/FilesLinksTable.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ const FilesLinksTable = ({
229229
</Modal.Title>
230230
</Modal.Header>
231231
<Modal.Body>
232-
<ExerciseFilesTableContainer exercise={exercise} noBox noRemove />
232+
<ExerciseFilesTableContainer exercise={exercise} exerciseFilesLinks={links} noBox noRemove />
233233
</Modal.Body>
234234
</Modal>
235235

src/components/Exercises/FilesTable/FilesTable.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const FilesTable = ({
2929
viewOnly = false,
3030
noRemove = false,
3131
downloadArchive,
32+
...restProps
3233
}) => (
3334
<div>
3435
{description && (
@@ -56,13 +57,14 @@ const FilesTable = ({
5657
{files.length > 0 && (
5758
<Table responsive>
5859
<thead>
59-
<HeaderComponent viewOnly={viewOnly} />
60+
<HeaderComponent {...restProps} viewOnly={viewOnly} />
6061
</thead>
6162
<tbody>
6263
{files
6364
.sort((a, b) => a.name.localeCompare(b.name, intl.locale))
6465
.map((fileData, i) => (
6566
<RowComponent
67+
{...restProps}
6668
{...fileData}
6769
removeFile={noRemove ? null : removeFile}
6870
downloadFile={downloadFile}

src/containers/ExerciseFilesTableContainer/ExerciseFilesTableContainer.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import PropTypes from 'prop-types';
33
import { connect } from 'react-redux';
44
import ImmutablePropTypes from 'react-immutable-proptypes';
55
import { FormattedMessage } from 'react-intl';
6+
import { lruMemoize } from 'reselect';
67

78
import FilesTableContainer from '../FilesTableContainer';
89
import { ExerciseFilesTableHeaderRow, ExerciseFilesTableRow } from '../../components/Exercises/FilesTable';
10+
import { InfoIcon } from '../../components/icons/index.js';
911

1012
import {
1113
fetchFilesForExercise,
@@ -14,13 +16,21 @@ import {
1416
downloadExerciseFilesArchive,
1517
} from '../../redux/modules/exerciseFiles.js';
1618
import { download } from '../../redux/modules/files.js';
19+
import { fetchExerciseFileLinks, fetchExerciseFileLinksIfNeeded } from '../../redux/modules/exerciseFilesLinks.js';
1720
import { getFilesForExercise, fetchFilesForExerciseStatus } from '../../redux/selectors/exerciseFiles.js';
18-
import { InfoIcon } from '../../components/icons/index.js';
21+
import { getExerciseFilesLinks } from '../../redux/selectors/exerciseFilesLinks.js';
22+
23+
import { isReadyOrReloading, getJsData } from '../../redux/helpers/resourceManager';
24+
25+
const createFilesLinksIndex = lruMemoize(filesLinks =>
26+
isReadyOrReloading(filesLinks) ? new Set(Object.values(getJsData(filesLinks)).map(link => link.exerciseFileId)) : null
27+
);
1928

2029
const ExerciseFilesTableContainer = ({
2130
exercise,
2231
exerciseFiles,
2332
exerciseFilesStatus,
33+
exerciseFilesLinks,
2434
usedFiles,
2535
loadFiles,
2636
addFiles,
@@ -52,6 +62,7 @@ const ExerciseFilesTableContainer = ({
5262
RowComponent={ExerciseFilesTableRow}
5363
viewOnly={!exercise.permissionHints.update}
5464
downloadArchive={downloadArchive}
65+
linkedFilesIds={createFilesLinksIndex(exerciseFilesLinks)}
5566
{...props}
5667
/>
5768
);
@@ -64,6 +75,7 @@ ExerciseFilesTableContainer.propTypes = {
6475
}).isRequired,
6576
exerciseFiles: ImmutablePropTypes.map,
6677
exerciseFilesStatus: PropTypes.string,
78+
exerciseFilesLinks: PropTypes.array,
6779
usedFiles: PropTypes.instanceOf(Set),
6880
loadFiles: PropTypes.func.isRequired,
6981
addFiles: PropTypes.func.isRequired,
@@ -77,11 +89,17 @@ export default connect(
7789
return {
7890
exerciseFiles: getFilesForExercise(exercise.id)(state),
7991
exerciseFilesStatus: fetchFilesForExerciseStatus(state)(exercise.id),
92+
exerciseFilesLinks: getExerciseFilesLinks(state, exercise.id),
8093
};
8194
},
8295
(dispatch, { exercise }) => ({
83-
loadFiles: () => dispatch(fetchFilesForExercise(exercise.id)),
84-
addFiles: files => dispatch(addExerciseFiles(exercise.id, files)),
96+
loadFiles: () =>
97+
Promise.all([
98+
dispatch(fetchFilesForExercise(exercise.id)),
99+
dispatch(fetchExerciseFileLinksIfNeeded(exercise.id)),
100+
]),
101+
addFiles: files =>
102+
dispatch(addExerciseFiles(exercise.id, files)).then(() => dispatch(fetchExerciseFileLinks(exercise.id))),
85103
removeFile: id => dispatch(removeExerciseFile(exercise.id, id)),
86104
downloadFile: (ev, id) => {
87105
ev.preventDefault();

src/locales/cs.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,6 @@
205205
"app.asyncJobs.list.worker": "Obsluha",
206206
"app.asyncJobs.ping": "Cinkout na obsluhu",
207207
"app.asyncJobs.title": "Úkoly jádra na pozadí",
208-
"app.attachmentFiles.deleteConfirm": "Opravdu chcete smazat tento soubor? Tuto akci není možné vrátit.",
209-
"app.attachmentFilesTable.description": "Přiložené soubory jsou soubory které mohou být použity v popisu úlohy pomocí odkazů poskytnutých pod tímto textem. Přiložené soubory mohou být prohlíženy a stahovány studenty.",
210-
"app.attachmentFilesTable.title": "Přiložené soubory",
211-
"app.attachmentFilesTable.url": "URL",
212208
"app.badge.effectiveRoleDialog.title": "Změnit efektivní roli",
213209
"app.badge.failedLoading": "Nepodařilo se načíst data",
214210
"app.badge.failedLoadingInfo": "Prosim zkontrolujte si své připojení k Internetu.",
@@ -827,8 +823,10 @@
827823
"app.exerciseFileLinkForm.validation.key.required": "Prosíme vyplňte klíč.",
828824
"app.exerciseFileLinkForm.validation.saveName.invalid": "Název souboru nemůže obsahovat nebezpečné znaky používané v cestách.",
829825
"app.exerciseFileLinkForm.validation.saveName.suspicious": "Použité znaky nejsou typické pro běžné názvy souborů. Prosíme ověřte, že se nejedná o chybu.",
830-
"app.exerciseFiles.cannotDeleteExplain": "Soubor nelze smazat protože je použit v konfiguraci.",
826+
"app.exerciseFiles.cannotDeleteLinkedExplain": "Soubor nelze smazat protože má přidružený odkaz ke stažení.",
827+
"app.exerciseFiles.cannotDeleteUsedExplain": "Soubor nelze smazat protože je použit v konfiguraci.",
831828
"app.exerciseFiles.deleteConfirm": "Opravdu si přejete tento soubor smazat? Tato operace nemůže být vrácena.",
829+
"app.exerciseFiles.linkedFile": "Tento soubor je přístupný prostřednictvím odkazu.",
832830
"app.exerciseFilesTable.description": "Soubory úlohy mohou být použity v nastavení úlohy (jako vstupní soubory, vzorové výstupní soubory, přídavné soubory pro kompilaci, vlastní sudí, ...). Na soubory je také možné vytvořit odkazy ke stažení, které lze vkládat do textů úlohy (specifikace pro studenty, popis).",
833831
"app.exerciseFilesTable.title": "Soubory úlohy",
834832
"app.exerciseReferenceSolutions.filters.author": "Autor",
@@ -1019,7 +1017,6 @@
10191017
"app.filesLinksTable.visibleToAll": "Viditelné všem uživatelům (i bez přihlášení)",
10201018
"app.filesTable.downloadArchive": "Stáhnout vše jako ZIP soubor",
10211019
"app.filesTable.empty": "Zatím zde nejsou žádné uložené soubory.",
1022-
"app.filesTable.originalFileName": "Původní jméno",
10231020
"app.filesTable.saveUploadedFilesButton": "Uložit nahrané soubory",
10241021
"app.filterArchiveGroupsForm.searchName": "Vyhledat podle jména",
10251022
"app.filterArchiveGroupsForm.showAll": "Ukázat také nearchivované skupiny",

src/locales/en.json

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,6 @@
205205
"app.asyncJobs.list.worker": "Worker",
206206
"app.asyncJobs.ping": "Ping Worker",
207207
"app.asyncJobs.title": "Core Background Jobs",
208-
"app.attachmentFiles.deleteConfirm": "Are you sure you want to delete the file? This cannot be undone.",
209-
"app.attachmentFilesTable.description": "Attached files are files which can be used within exercise description using links provided below. Attached files can be viewed or downloaded by students.",
210-
"app.attachmentFilesTable.title": "Attached files",
211-
"app.attachmentFilesTable.url": "URL",
212208
"app.badge.effectiveRoleDialog.title": "Change Effective Role",
213209
"app.badge.failedLoading": "Failed to load the data",
214210
"app.badge.failedLoadingInfo": "Please check your Internet connection.",
@@ -827,8 +823,10 @@
827823
"app.exerciseFileLinkForm.validation.key.required": "Please fill the key.",
828824
"app.exerciseFileLinkForm.validation.saveName.invalid": "The file name cannot contain dangerous characters that are used in paths.",
829825
"app.exerciseFileLinkForm.validation.saveName.suspicious": "Used characters are not typical for common file names. Please verify that this is intended.",
830-
"app.exerciseFiles.cannotDeleteExplain": "The file cannot be deleted since it is being used in the configuration.",
826+
"app.exerciseFiles.cannotDeleteLinkedExplain": "The file cannot be deleted since it has an associated download link.",
827+
"app.exerciseFiles.cannotDeleteUsedExplain": "The file cannot be deleted since it is being used in the configuration.",
831828
"app.exerciseFiles.deleteConfirm": "Are you sure you want to delete the file? This cannot be undone.",
829+
"app.exerciseFiles.linkedFile": "This file has been made accessible via a link.",
832830
"app.exerciseFilesTable.description": "Exercise files can be used in exercise configuration (as input files, expected output files, extra compilation files, custom judges, ...). Links to exercise files can also be created and then used in exercise texts (specification for students, description).",
833831
"app.exerciseFilesTable.title": "Exercise Files",
834832
"app.exerciseReferenceSolutions.filters.author": "Author",
@@ -1019,7 +1017,6 @@
10191017
"app.filesLinksTable.visibleToAll": "Visible to all users (without login)",
10201018
"app.filesTable.downloadArchive": "Download all as ZIP archive",
10211019
"app.filesTable.empty": "There are no saved files yet.",
1022-
"app.filesTable.originalFileName": "Original File Name",
10231020
"app.filesTable.saveUploadedFilesButton": "Save Uploaded Files",
10241021
"app.filterArchiveGroupsForm.searchName": "Search by name",
10251022
"app.filterArchiveGroupsForm.showAll": "Show also non-archived groups",
@@ -2170,4 +2167,4 @@
21702167
"recodex-judge-shuffle-all": "Unordered-tokens-and-rows judge",
21712168
"recodex-judge-shuffle-newline": "Unordered-tokens judge (ignoring ends of lines)",
21722169
"recodex-judge-shuffle-rows": "Unordered-rows judge"
2173-
}
2170+
}

src/locales/whitelist_cs.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
[
2-
"app.attachmentFilesTable.url",
32
"app.broker.title",
43
"app.changePasswordForm.email",
54
"app.evaluationProgressStatus.ok",

src/pages/EditExerciseConfig/EditExerciseConfig.js

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,27 @@ import {
3434
fetchExerciseConfigIfNeeded,
3535
setExerciseConfig,
3636
} from '../../redux/modules/exerciseConfigs.js';
37-
import { getExercise } from '../../redux/selectors/exercises.js';
38-
import { exerciseConfigSelector } from '../../redux/selectors/exerciseConfigs.js';
39-
import { getLoggedInUserEffectiveRole } from '../../redux/selectors/users.js';
4037
import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments.js';
41-
import { runtimeEnvironmentsSelector } from '../../redux/selectors/runtimeEnvironments.js';
4238
import { fetchExercisePipelinesVariables } from '../../redux/modules/exercisePipelinesVariables.js';
43-
import { getExercisePipelinesVariablesJS } from '../../redux/selectors/exercisePipelinesVariables.js';
44-
import { getFilesForExercise, fetchFilesForExerciseStatus } from '../../redux/selectors/exerciseFiles.js';
45-
import { isLoadingState } from '../../redux/helpers/resourceManager/status.js';
46-
47-
import withLinks from '../../helpers/withLinks.js';
48-
import { exerciseEnvironmentConfigSelector } from '../../redux/selectors/exerciseEnvironmentConfigs.js';
4939
import {
5040
fetchExerciseEnvironmentConfig,
5141
fetchExerciseEnvironmentConfigIfNeeded,
5242
setExerciseEnvironmentConfig,
5343
} from '../../redux/modules/exerciseEnvironmentConfigs.js';
54-
import { exerciseScoreConfigSelector } from '../../redux/selectors/exerciseScoreConfig.js';
5544
import { fetchScoreConfigIfNeeded, setScoreConfig } from '../../redux/modules/exerciseScoreConfig.js';
5645
import { fetchExerciseTestsIfNeeded, setExerciseTests } from '../../redux/modules/exerciseTests.js';
57-
import { exerciseTestsSelector } from '../../redux/selectors/exerciseTests.js';
5846
import { fetchPipelines } from '../../redux/modules/pipelines.js';
47+
import { exerciseTestsSelector } from '../../redux/selectors/exerciseTests.js';
5948
import { pipelinesSelector, getPipelinesEnvironmentsWhichHasEntryPoint } from '../../redux/selectors/pipelines.js';
49+
import { exerciseScoreConfigSelector } from '../../redux/selectors/exerciseScoreConfig.js';
50+
import { exerciseEnvironmentConfigSelector } from '../../redux/selectors/exerciseEnvironmentConfigs.js';
51+
import { getExercisePipelinesVariablesJS } from '../../redux/selectors/exercisePipelinesVariables.js';
52+
import { getFilesForExercise, fetchFilesForExerciseStatus } from '../../redux/selectors/exerciseFiles.js';
53+
import { runtimeEnvironmentsSelector } from '../../redux/selectors/runtimeEnvironments.js';
54+
import { getExercise } from '../../redux/selectors/exercises.js';
55+
import { exerciseConfigSelector } from '../../redux/selectors/exerciseConfigs.js';
56+
import { getLoggedInUserEffectiveRole } from '../../redux/selectors/users.js';
57+
import { isLoadingState } from '../../redux/helpers/resourceManager/status.js';
6058

6159
import {
6260
getTestsInitValues,
@@ -89,6 +87,7 @@ import {
8987
import { isEmpoweredSupervisorRole } from '../../components/helpers/usersRoles.js';
9088
import { hasPermissions, safeGet } from '../../helpers/common.js';
9189
import withRouter, { withRouterProps } from '../../helpers/withRouter.js';
90+
import withLinks from '../../helpers/withLinks.js';
9291

9392
class EditExerciseConfig extends Component {
9493
componentDidMount() {

0 commit comments

Comments
 (0)