Skip to content

Commit

Permalink
Added custom filtering/ordering to results navigation (#6642)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielDervishi authored Jul 28, 2023
1 parent f4716c3 commit 18ce242
Show file tree
Hide file tree
Showing 25 changed files with 2,225 additions and 30 deletions.
1 change: 1 addition & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- Ensure Jupyter notebook HTML rendering does not require external CDNs (#6656)
- Prevent Jupyter notebook from reloading when an annotation is added (#6656)
- Added a button allowing graders to view a random incomplete submission (#6641)
- Added a filter modal allowing graders to specify filters and order when navigating submissions (#6642)
- Add icons to submission and result grading action buttons (#6666)
- Remove group name maximum length constraint (#6668)
- Fix bug where in some cases flash messages were not being rendered correctly (#6670)
Expand Down
307 changes: 307 additions & 0 deletions app/assets/javascripts/Components/Modals/filter_modal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
import React from "react";
import Modal from "react-modal";
import {MultiSelectDropdown} from "../../DropDownMenu/MultiSelectDropDown";
import {SingleSelectDropDown} from "../../DropDownMenu/SingleSelectDropDown";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";

export class FilterModal extends React.Component {
onToggleOptionTas = user_name => {
const newArray = [...this.props.filterData.tas];
if (newArray.includes(user_name)) {
this.props.updateFilterData({
tas: newArray.filter(item => item !== user_name),
});
} else {
newArray.push(user_name);
this.props.updateFilterData({
tas: newArray,
});
}
};

onClearSelectionTAs = () => {
this.props.updateFilterData({
tas: [],
});
};

onClearSelectionTags = () => {
this.props.updateFilterData({
tags: [],
});
};

onToggleOptionTags = tag => {
const newArray = [...this.props.filterData.tags];
if (newArray.includes(tag)) {
this.props.updateFilterData({
tags: newArray.filter(item => item !== tag),
});
} else {
newArray.push(tag);
this.props.updateFilterData({
tags: newArray,
});
}
};

renderTasDropdown = () => {
if (this.props.role === "Instructor") {
let tas = this.props.tas.map(option => {
return {key: option[0], display: option[0] + " - " + option[1]};
});
return (
<div className={"filter"}>
<p>{I18n.t("activerecord.models.ta.other")}</p>
<MultiSelectDropdown
title={"Tas"}
options={tas}
selected={this.props.filterData.tas}
onToggleOption={this.onToggleOptionTas}
onClearSelection={this.onClearSelectionTAs}
/>
</div>
);
}
};

renderTagsDropdown = () => {
let options = [];
if (this.props.available_tags.length !== 0) {
options = options.concat(
this.props.available_tags.map(item => {
return {key: item.name, display: item.name};
})
);
}
if (this.props.current_tags.length !== 0) {
options = options.concat(
this.props.current_tags.map(item => {
return {key: item.name, display: item.name};
})
);
}
return (
<MultiSelectDropdown
title={"Tags"}
options={options}
selected={this.props.filterData.tags}
onToggleOption={this.onToggleOptionTags}
onClearSelection={this.onClearSelectionTags}
/>
);
};

rangeFilter = (min, max, title, onMinChange, onMaxChange) => {
return (
<div className={"filter"}>
<p>{title}</p>
<div className={"range"} data-testid={title}>
<input
className={"input-min"}
aria-label={title + " - " + I18n.t("min")}
type="number"
step="any"
placeholder={I18n.t("min")}
value={min}
max={max}
onChange={e => onMinChange(e)}
/>
<span>{I18n.t("to")}</span>
<input
className={"input-max"}
aria-label={title + " - " + I18n.t("max")}
type="number"
step="any"
placeholder={I18n.t("max")}
value={max}
min={min}
onChange={e => onMaxChange(e)}
/>
<div className={"hidden"}>
<FontAwesomeIcon icon={"fa-solid fa-circle-exclamation"} />
<span className={"validity"}>{I18n.t("results.filters.invalid_range")}</span>
</div>
</div>
</div>
);
};

onTotalMarkMinChange = e => {
this.props.updateFilterData({
totalMarkRange: {...this.props.filterData.totalMarkRange, min: e.target.value},
});
};

onTotalMarkMaxChange = e => {
this.props.updateFilterData({
totalMarkRange: {...this.props.filterData.totalMarkRange, max: e.target.value},
});
};

onTotalExtraMarkMinChange = e => {
this.props.updateFilterData({
totalExtraMarkRange: {...this.props.filterData.totalExtraMarkRange, min: e.target.value},
});
};

onTotalExtraMarkMaxChange = e => {
this.props.updateFilterData({
totalExtraMarkRange: {...this.props.filterData.totalExtraMarkRange, max: e.target.value},
});
};

componentDidMount() {
Modal.setAppElement("body");
}

onClearFilters = event => {
event.preventDefault();
this.props.clearAllFilters();
};

render() {
if (this.props.loading) {
return "";
}
return (
<Modal
className="react-modal dialog filter-modal"
isOpen={this.props.isOpen}
onRequestClose={() => {
this.props.onRequestClose();
}}
>
<h3 className={"filter-modal-title"}>
<FontAwesomeIcon icon="fa-solid fa-filter" className={"filter-icon-title"} />
{I18n.t("results.filter_submissions")}
</h3>
<form>
<div className={"modal-container-scrollable"}>
<div className={"modal-container-vertical"}>
<div className={"modal-container"}>
<div className={"filter"} data-testid={"order-by"}>
<p>{I18n.t("results.filters.order_by")} </p>
<SingleSelectDropDown
valueToDisplayName={{
group_name: I18n.t("activerecord.attributes.group.group_name"),
submission_date: I18n.t("submissions.commit_date"),
}}
options={["group_name", "submission_date"]}
selected={this.props.filterData.orderBy}
onSelect={selection => {
this.props.updateFilterData({
orderBy: selection,
});
}}
defaultValue={I18n.t("activerecord.attributes.group.group_name")}
/>
<div className={"order"} data-testid={"radio-group"}>
<input
type="radio"
checked={this.props.filterData.ascending}
name="order"
onChange={() => {
this.props.updateFilterData({ascending: true});
}}
id={"Asc"}
data-testid={"ascending"}
/>
<label htmlFor="Asc">{I18n.t("results.filters.ordering.ascending")}</label>
<input
type="radio"
checked={!this.props.filterData.ascending}
name="order"
onChange={() => {
this.props.updateFilterData({ascending: false});
}}
id={"Desc"}
data-testid={"descending"}
/>
<label htmlFor="Desc">{I18n.t("results.filters.ordering.descending")}</label>
</div>
</div>
<div className={"filter"} data-testid={"marking-state"}>
<p>{I18n.t("activerecord.attributes.result.marking_state")}</p>
<SingleSelectDropDown
valueToDisplayName={{
in_progress: I18n.t("submissions.state.in_progress"),
complete: I18n.t("submissions.state.complete"),
released: I18n.t("submissions.state.released"),
remark_requested: I18n.t("submissions.state.remark_requested"),
}}
options={["in_progress", "complete", "released", "remark_requested"]}
selected={this.props.filterData.markingState}
onSelect={selection => {
this.props.updateFilterData({
markingState: selection,
});
}}
/>
</div>
</div>
<div className={"modal-container"}>
<div className={"filter"}>
<p>{I18n.t("activerecord.models.tag.other")}</p>
{this.renderTagsDropdown()}
</div>
<div className={"filter"} data-testid={"section"}>
<p>{I18n.t("activerecord.models.section.one")}</p>
<SingleSelectDropDown
options={this.props.sections.sort()}
selected={this.props.filterData.section}
onSelect={selection => {
this.props.updateFilterData({
section: selection,
});
}}
defaultValue={""}
/>
</div>
</div>
<div className={"modal-container"}>
{this.renderTasDropdown()}
<div className={"annotation-input"}>
<p>{I18n.t("activerecord.models.annotation.one")}</p>
<input
type={"text"}
value={this.props.filterData.annotationText}
onChange={e =>
this.props.updateFilterData({
annotationText: e.target.value,
})
}
placeholder={I18n.t("results.filters.text_box_placeholder")}
/>
</div>
</div>

<div className={"modal-container"}>
{this.rangeFilter(
this.props.filterData.totalMarkRange.min,
this.props.filterData.totalMarkRange.max,
I18n.t("results.total_mark"),
this.onTotalMarkMinChange,
this.onTotalMarkMaxChange
)}
{this.rangeFilter(
this.props.filterData.totalExtraMarkRange.min,
this.props.filterData.totalExtraMarkRange.max,
I18n.t("results.total_extra_marks"),
this.onTotalExtraMarkMinChange,
this.onTotalExtraMarkMaxChange
)}
</div>
</div>
</div>
<div className={"modal-footer"}>
<section className={"modal-container dialog-actions"}>
<button onClick={this.onClearFilters}>{I18n.t("clear_all")}</button>
<button onClick={this.props.onRequestClose}>{I18n.t("close")}</button>
</section>
</div>
</form>
</Modal>
);
}
}
Loading

0 comments on commit 18ce242

Please sign in to comment.