Skip to content

Commit

Permalink
Handling new archiving feature of exercise (setting, adjusting list f…
Browse files Browse the repository at this point in the history
…ilters, and visualization).
  • Loading branch information
krulis-martin committed Aug 21, 2023
1 parent 0d8d920 commit 86083e8
Show file tree
Hide file tree
Showing 23 changed files with 310 additions and 122 deletions.
25 changes: 21 additions & 4 deletions src/components/Exercises/ExerciseCallouts/ExerciseCallouts.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { defaultMemoize } from 'reselect';
import { Link } from 'react-router-dom';

import Explanation from '../../widgets/Explanation';
import { NeedFixingIcon, CheckRequiredIcon, LinkIcon } from '../../icons';
import { ArchiveIcon, NeedFixingIcon, CheckRequiredIcon, LinkIcon } from '../../icons';
import Callout from '../../widgets/Callout';
import DateTime from '../../widgets/DateTime';
import withLinks from '../../../helpers/withLinks';

export const exerciseCalloutsAreVisible = ({ isBroken, hasReferenceSolutions }) => isBroken || !hasReferenceSolutions;
export const exerciseCalloutsAreVisible = ({ archivedAt, isBroken, hasReferenceSolutions }) =>
archivedAt !== null || isBroken || !hasReferenceSolutions;

const errorTypes = [
'no-texts',
Expand Down Expand Up @@ -181,13 +183,27 @@ const transformErrors = defaultMemoize((errors, exerciseId, links) => {
const ExerciseCallouts = ({
id,
isBroken = false,
archivedAt = null,
hasReferenceSolutions,
validationError = null,
permissionHints = null,
links,
}) => (
<>
{isBroken && (
{archivedAt !== null && (
<Callout variant="info" icon={<ArchiveIcon />}>
<h4>
<FormattedMessage id="app.exercise.archived" defaultMessage="The exercise has been archived." />
</h4>
<FormattedMessage
id="app.exercise.archivedDetailed"
defaultMessage="The exercise was placed into an archived state (at {archivedAt}). Archived exercises are not listed by default, cannot be modified, and cannot be assigned."
values={{ archivedAt: <DateTime unixts={archivedAt} /> }}
/>
</Callout>
)}

{!archivedAt && isBroken && (
<Callout variant="warning" icon={<NeedFixingIcon />}>
<h4>
<FormattedMessage
Expand All @@ -200,7 +216,7 @@ const ExerciseCallouts = ({
</Callout>
)}

{!isBroken && !hasReferenceSolutions && (
{!archivedAt && !isBroken && !hasReferenceSolutions && (
<Callout variant="info" icon={<CheckRequiredIcon />}>
<h4>
<FormattedMessage
Expand Down Expand Up @@ -233,6 +249,7 @@ const ExerciseCallouts = ({
ExerciseCallouts.propTypes = {
id: PropTypes.string.isRequired,
isBroken: PropTypes.bool,
archivedAt: PropTypes.number,
hasReferenceSolutions: PropTypes.bool,
validationError: PropTypes.string,
permissionHints: PropTypes.object,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Exercises/ExerciseGroups/ExerciseGroups.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class ExerciseGroups extends Component {
}
noPadding>
<>
<Table hover>
<Table hover className="mb-1">
<tbody>
{groupsIds.map(groupId => (
<tr key={groupId}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const ExercisesListItem = ({
isPublic,
isLocked,
isBroken,
archivedAt,
hasReferenceSolutions,
permissionHints,
showGroups = false,
Expand All @@ -55,6 +56,7 @@ const ExercisesListItem = ({
isPublic={isPublic}
isLocked={isLocked}
isBroken={isBroken}
archivedAt={archivedAt}
hasReferenceSolutions={hasReferenceSolutions}
/>
</td>
Expand Down Expand Up @@ -200,6 +202,7 @@ ExercisesListItem.propTypes = {
isPublic: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
isBroken: PropTypes.bool.isRequired,
archivedAt: PropTypes.number,
hasReferenceSolutions: PropTypes.bool.isRequired,
localizedTexts: PropTypes.array.isRequired,
permissionHints: PropTypes.object.isRequired,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { ArchiveIcon, LoadingIcon } from '../../icons';
import Button from '../../widgets/TheButton';

const ArchiveExerciseButton = ({ archived, pending, disabled = false, setArchived, ...props }) => (
<Button
{...props}
variant={disabled ? 'secondary' : pending === false ? 'danger' : 'warning'}
onClick={() => setArchived(!archived)}
disabled={pending || disabled}>
{pending ? <LoadingIcon gapRight /> : <ArchiveIcon archived={archived} gapRight />}
{archived ? (
<FormattedMessage id="app.archiveExerciseButton.unset" defaultMessage="Excavate from Archive" />
) : (
<FormattedMessage id="app.archiveExerciseButton.set" defaultMessage="Archive the Exercise" />
)}
</Button>
);

ArchiveExerciseButton.propTypes = {
archived: PropTypes.bool.isRequired,
pending: PropTypes.bool,
disabled: PropTypes.bool,
setArchived: PropTypes.func.isRequired,
};

export default ArchiveExerciseButton;
2 changes: 2 additions & 0 deletions src/components/buttons/ArchiveExerciseButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import ArchiveExerciseButton from './ArchiveExerciseButton';
export default ArchiveExerciseButton;
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { ArchiveGroupIcon, LoadingIcon } from '../../icons';
import { ArchiveIcon, LoadingIcon } from '../../icons';
import Button from '../../widgets/TheButton';

const ArchiveGroupButton = ({ archived, pending, disabled = false, setArchived, shortLabels = false, ...props }) => (
Expand All @@ -10,7 +10,7 @@ const ArchiveGroupButton = ({ archived, pending, disabled = false, setArchived,
variant={disabled ? 'secondary' : 'info'}
onClick={setArchived(!archived)}
disabled={pending || disabled}>
{pending ? <LoadingIcon gapRight /> : <ArchiveGroupIcon archived={archived} gapRight />}
{pending ? <LoadingIcon gapRight /> : <ArchiveIcon archived={archived} gapRight />}
{archived === true ? (
shortLabels ? (
<FormattedMessage id="app.archiveGroupButton.unsetShort" defaultMessage="Excavate" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { loggedInUserIdSelector } from '../../../redux/selectors/auth';
import EditEnvironmentList from '../EditEnvironmentSimpleForm/EditEnvironmentList';
import ResourceRenderer from '../../helpers/ResourceRenderer';
import SubmitButton from '../SubmitButton';
import { TextField, SelectField, TagsSelectorField } from '../Fields';
import { TextField, RadioField, SelectField, TagsSelectorField } from '../Fields';
import { identity, safeGet } from '../../../helpers/common';
import { ExpandCollapseIcon } from '../../icons';
import InsetPanel from '../../widgets/InsetPanel';
Expand All @@ -28,6 +28,36 @@ import Callout from '../../widgets/Callout';

const RTE_PREFIX = 'runtimeEnvironments.';

const ARCHIVED_OPTIONS = [
{
key: 'default',
name: (
<FormattedMessage
id="app.filterExercisesListForm.archivedOptions.default"
defaultMessage="Regular exercises (default)"
/>
),
},
{
key: 'all',
name: (
<FormattedMessage
id="app.filterExercisesListForm.archivedOptions.all"
defaultMessage="All exercises (including archived)"
/>
),
},
{
key: 'only',
name: (
<FormattedMessage
id="app.filterExercisesListForm.archivedOptions.archived"
defaultMessage="Only archived exercises"
/>
),
},
];

const authorsToOptions = defaultMemoize((authors, locale) =>
authors
.filter(identity)
Expand Down Expand Up @@ -184,6 +214,20 @@ class FilterExercisesListForm extends Component {
<>
{tags && tags.length > 0 && (
<>
<Row className="mt-2">
<Col xs={false} sm="auto">
<FormLabel className="mr-2">
<FormattedMessage
id="app.filterExercisesListForm.archived"
defaultMessage="Archived Status:"
/>
</FormLabel>
</Col>
<Col xs={12} sm className="text-muted">
<Field name="archived" component={RadioField} options={ARCHIVED_OPTIONS} />
</Col>
</Row>

<Row>
<Col lg={12}>
<hr />
Expand Down Expand Up @@ -215,7 +259,7 @@ class FilterExercisesListForm extends Component {

<Row>
<Col lg={12}>
<div className="em-margin-bottom">
<div className="mb-3">
<FormLabel>
<FormattedMessage
id="app.filterExercisesListForm.selectedEnvironments"
Expand Down Expand Up @@ -244,7 +288,7 @@ class FilterExercisesListForm extends Component {

<Row>
<Col lg={12}>
<div className="mb-3 text-center">
<div className="mb-2 text-center">
{this.isOpen() ? (
<TheButtonGroup>
<SubmitButton
Expand All @@ -268,7 +312,7 @@ class FilterExercisesListForm extends Component {
</Button>
</TheButtonGroup>
) : (
<span className="small clickable em-padding-horizontal" onClick={this.toggleOpen}>
<span className="small clickable" onClick={this.toggleOpen}>
<ExpandCollapseIcon isOpen={this.isOpen()} gapRight />
<FormattedMessage
id="app.filterExercisesListForm.showAllFilters"
Expand Down
41 changes: 35 additions & 6 deletions src/components/icons/ExercisePrefixIcons.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import Icon from './Icon';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';

import Icon, { ArchiveIcon } from './index';
import DateTime from '../widgets/DateTime';

export const PrivateIcon = props => <Icon {...props} icon={['far', 'eye-slash']} />;
export const NeedFixingIcon = props => <Icon {...props} icon="hammer" />;
export const LockIcon = props => <Icon {...props} icon="lock" />;
export const CheckRequiredIcon = props => <Icon {...props} icon="spell-check" />;

export const ExercisePrefixIcons = ({ id, isPublic, isLocked, isBroken, hasReferenceSolutions, ...props }) => (
export const ExercisePrefixIcons = ({
id,
isPublic,
isLocked,
isBroken,
archivedAt = null,
hasReferenceSolutions,
...props
}) => (
<span>
{!isPublic && (
{archivedAt && (
<span>
<OverlayTrigger
placement="right"
overlay={
<Tooltip id={id}>
<FormattedMessage
id="app.ExercisePrefixIcons.archivedAt"
defaultMessage="Archived at {archivedAt}."
values={{ archivedAt: <DateTime unixts={archivedAt} /> }}
/>
</Tooltip>
}>
<ArchiveIcon {...props} className="text-info" gapRight />
</OverlayTrigger>
</span>
)}

{!archivedAt && !isPublic && (
<span>
<OverlayTrigger
placement="right"
Expand All @@ -28,7 +56,7 @@ export const ExercisePrefixIcons = ({ id, isPublic, isLocked, isBroken, hasRefer
</span>
)}

{isLocked && (
{!archivedAt && isLocked && (
<span>
<OverlayTrigger
placement="right"
Expand All @@ -45,7 +73,7 @@ export const ExercisePrefixIcons = ({ id, isPublic, isLocked, isBroken, hasRefer
</span>
)}

{isBroken && (
{!archivedAt && isBroken && (
<span>
<OverlayTrigger
placement="right"
Expand All @@ -62,7 +90,7 @@ export const ExercisePrefixIcons = ({ id, isPublic, isLocked, isBroken, hasRefer
</span>
)}

{!hasReferenceSolutions && (
{!archivedAt && !isBroken && !hasReferenceSolutions && (
<span>
<OverlayTrigger
placement="right"
Expand All @@ -86,5 +114,6 @@ ExercisePrefixIcons.propTypes = {
isPublic: PropTypes.bool.isRequired,
isLocked: PropTypes.bool.isRequired,
isBroken: PropTypes.bool.isRequired,
archivedAt: PropTypes.number,
hasReferenceSolutions: PropTypes.bool.isRequired,
};
7 changes: 3 additions & 4 deletions src/components/icons/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ export const AddIcon = props => <Icon {...props} icon="plus-circle" />;
export const AdminIcon = props => <Icon {...props} icon="user-tie" />;
export const AdminRoleIcon = props => <Icon {...props} icon="crown" />;
export const AdressIcon = props => <Icon {...props} icon="at" />;
export const ArchiveIcon = props => <Icon {...props} icon="archive" />;
export const ArchiveGroupIcon = ({ archived = false, ...props }) => (
<Icon {...props} icon={archived ? 'dolly' : 'archive'} />
export const ArchiveIcon = ({ archived = false, ...props }) => (
<Icon {...props} icon={archived ? 'dolly' : 'boxes-packing'} />
);
export const AssignmentIcon = props => <Icon {...props} icon="laptop-code" />;
export const AssignmentsIcon = props => <Icon {...props} icon="tasks" />;
Expand Down Expand Up @@ -160,7 +159,7 @@ export const WarningIcon = props => <Icon {...props} icon="exclamation-triangle"
export const WorkingIcon = props => <Icon {...props} spin icon="cog" />;
export const ZipIcon = props => <Icon {...props} icon={['far', 'file-archive']} />;

ArchiveGroupIcon.propTypes = {
ArchiveIcon.propTypes = {
archived: PropTypes.bool,
};

Expand Down
32 changes: 14 additions & 18 deletions src/components/layout/Navigation/ExerciseNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,22 @@ import Navigation from './Navigation';
import withLinks from '../../../helpers/withLinks';
import { createExerciseLinks } from './linkCreators';

const ExerciseNavigation = ({
exerciseId,
canEdit = false,
canViewTests = false,
canViewLimits = false,
canViewAssignments = false,
links,
}) => (
<Navigation
exerciseId={exerciseId}
links={createExerciseLinks(links, exerciseId, canEdit, canViewTests, canViewLimits, canViewAssignments)}
/>
);
const ExerciseNavigation = ({ exercise: { id, permissionHints }, links }) => {
const canEdit = permissionHints.update || permissionHints.archive || permissionHints.remove;
const canViewTests = permissionHints.viewConfig && permissionHints.viewScoreConfig;
const canViewLimits = permissionHints.viewLimits;
const canViewAssignments = permissionHints.viewAssignments;

return (
<Navigation
exerciseId={id}
links={createExerciseLinks(links, id, canEdit, canViewTests, canViewLimits, canViewAssignments)}
/>
);
};

ExerciseNavigation.propTypes = {
exerciseId: PropTypes.string.isRequired,
canEdit: PropTypes.bool,
canViewTests: PropTypes.bool,
canViewLimits: PropTypes.bool,
canViewAssignments: PropTypes.bool,
exercise: PropTypes.object.isRequired,
links: PropTypes.object.isRequired,
};

Expand Down
Loading

0 comments on commit 86083e8

Please sign in to comment.