Skip to content

Commit

Permalink
Merge pull request #313 from GSA/207_sort_and_filter_component
Browse files Browse the repository at this point in the history
[207] Sort and Filter Component
  • Loading branch information
emmabjj authored Jan 6, 2025
2 parents 9e51cd5 + 81195b4 commit 8955d31
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 37 deletions.
48 changes: 47 additions & 1 deletion app/assets/stylesheets/application.sass.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,52 @@ dialog::backdrop {
background-color: #4d8055;
}

.usa-nav__primary {
button.usa-accordion__button {
&::before,
&::after,
span::after {
display: none !important;
content: none !important;
-webkit-mask: none !important;
mask: none !important;
}

.usa-icon {
width: 1.5rem;
height: 1.5rem;
fill: #005ea2;
vertical-align: middle;
position: relative;
top: -2px;

@media (min-width: 64em) {
margin-left: -16px;
}
}

.icon-up {
display: none;
}

&[aria-expanded="true"] {
.icon-down {
display: none;
}
.icon-up {
display: inline-block;
}
}
}

@media (max-width: 64em) {
a:not(.usa-button):hover {
background-color: #005ea2;
color: white;
}
}
}

.usa-combo-box .border-secondary + input {
border: 1px solid red;
}
}
90 changes: 83 additions & 7 deletions app/controllers/phases_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ def index

def submissions
@submissions = @phase.submissions
@submissions_count = @submissions.count
not_started = @submissions.left_outer_joins(:evaluations).
where({ "evaluations.id" => nil }).count
in_progress = @submissions.joins(:evaluations).
select("submissions.id").where({ "evaluations.completed_at" => nil }).distinct.count
completed = @submissions_count - in_progress - not_started
@submissions_by_status = { not_started:, in_progress:, completed: }

set_submission_counts
set_submission_statuses

apply_filters
apply_sorting
end

private
Expand All @@ -26,4 +25,81 @@ def set_phase
@phase = Phase.where(challenge: current_user.challenge_manager_challenges).find(params[:id])
@challenge = @phase.challenge
end

def set_submission_counts
@submissions_count = @submissions.count
@eligible_count = @submissions.eligible_for_evaluation.count
@selected_count = @submissions.winner.count
end

def set_submission_statuses
@not_started = @submissions.left_joins(evaluator_submission_assignments: :evaluation).
where(evaluations: { id: nil }).distinct
@in_progress = @submissions.joins(evaluator_submission_assignments: :evaluation).
where(evaluations: { completed_at: nil }).distinct
@completed = @submissions.joins(evaluator_submission_assignments: :evaluation).
where.not(evaluations: { completed_at: nil }).
where.not(id: @in_progress.select(:id)).distinct
@submissions_by_status = {
not_started: @not_started.count,
in_progress: @in_progress.count,
completed: @completed.count
}
end

def apply_filters
filter_by_eligibility
filter_by_status
end

def filter_by_eligibility
return unless params[:eligible_for_evaluation] == 'true' ||
params[:selected_to_advance] == 'true'

@submissions = apply_eligibility_filter(@submissions)
end

def filter_by_status
return unless params[:status]

@submissions = apply_status_filter(@submissions)
end

def apply_status_filter(submissions)
case params[:status]
when 'not_started' then @not_started
when 'in_progress' then @in_progress
when 'completed' then @completed
when 'recused' then filter_recused_submissions
else submissions
end
end

def filter_recused_submissions
@submissions.joins(:evaluator_submission_assignments).
where(evaluator_submission_assignments: { status: :recused })
end

def apply_eligibility_filter(submissions)
if params[:selected_to_advance] == 'true'
submissions.where(judging_status: %w[winner])
elsif params[:eligible_for_evaluation] == 'true'
submissions.where(judging_status: %w[selected winner])
else
submissions
end
end

def apply_sorting
case params[:sort]
when 'average_score_high_to_low'
@submissions = @submissions.order_by_average_score(:desc)
when 'average_score_low_to_high'
@submissions = @submissions.order_by_average_score(:asc)
when 'submission_id_high_to_low'
@submissions = @submissions.order(id: :desc)
when 'submission_id_low_to_high'
@submissions = @submissions.order(id: :asc)
end
end
end
12 changes: 6 additions & 6 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ import { application } from "./application";
import DeleteEvaluatorModalController from "./delete_evaluator_modal_controller";
application.register("delete-evaluator-modal", DeleteEvaluatorModalController);

import UnassignEvaluatorSubmissionModalController from "./unassign_evaluator_submission_modal_controller";
application.register(
"unassign-evaluator-submission-modal",
UnassignEvaluatorSubmissionModalController
);

import EvaluationCriteriaController from "./evaluation_criteria_controller";
application.register("evaluation-criteria", EvaluationCriteriaController);

Expand All @@ -27,3 +21,9 @@ application.register("submission-details", SubmissionDetailsController);

import ModalController from "./modal_controller";
application.register("modal", ModalController);

import SortFilterMenuController from "./sort_filter_menu_controller";
application.register("sort-filter-menu", SortFilterMenuController);

import UnassignEvaluatorSubmissionModalController from "./unassign_evaluator_submission_modal_controller";
application.register("unassign-evaluator-submission-modal", UnassignEvaluatorSubmissionModalController);
60 changes: 60 additions & 0 deletions app/javascript/controllers/sort_filter_menu_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// app/javascript/controllers/sort_filter_menu_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["menuItem"]

connect() {
this.updateCheckmarks()
}

toggle(event) {
event.preventDefault()
const clickedItem = event.currentTarget
const filterValue = clickedItem.dataset.filterValue

const currentUrl = new URL(window.location.href)
const [param, value] = filterValue.split('=')

if (currentUrl.searchParams.get(param) === value) {
this.clearAllFilters()
} else {
this.applyFilter(filterValue)
}
}

applyFilter(filterValue) {
const currentUrl = new URL(window.location.href)
const [param, value] = filterValue.split('=')

this.removeExistingParams(currentUrl)
currentUrl.searchParams.set(param, value)
window.location.href = currentUrl.toString()
}

clearAllFilters() {
const currentUrl = new URL(window.location.href)
this.removeExistingParams(currentUrl)
window.location.href = currentUrl.toString()
}

removeExistingParams(url) {
const paramsToRemove = ['status', 'eligible_for_evaluation', 'selected_to_advance', 'sort']
paramsToRemove.forEach(key => {
url.searchParams.delete(key)
})
}

updateCheckmarks() {
const currentUrl = new URL(window.location.href)
this.menuItemTargets.forEach(item => {
const checkmark = item.querySelector('.checkmark')
const [param, value] = item.dataset.filterValue.split('=')
if (currentUrl.searchParams.get(param) === value) {
checkmark.style.display = 'inline'
} else {
checkmark.style.display = 'none'
}
})
}
}
21 changes: 21 additions & 0 deletions app/models/submission.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,25 @@ def eligible_for_evaluation?
def selected_to_advance?
winner?
end

def average_score
avg = evaluations.average(:total_score)
avg ? avg.round : 0
end

def self.order_by_average_score(direction)
direction_sql = direction == :desc ? 'DESC' : 'ASC'

joins(
"LEFT JOIN evaluations ON evaluations.submission_id = submissions.id " \
"AND evaluations.completed_at IS NOT NULL"
).
group('submissions.id').
order(
Arel.sql(
"COALESCE(ROUND(AVG(evaluations.total_score)), 0) #{direction_sql}, " \
"submissions.id #{direction_sql}"
)
)
end
end
60 changes: 60 additions & 0 deletions app/views/phases/_sort_and_filter.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<div class="grid-container padding-x-0">
<div class="grid-row">
<div class="grid-col-12 display-flex flex-column flex-align-start padding-top-105">
<nav aria-label="Sort and filter options" class="tablet:usa-nav display-flex" data-controller="sort-filter-menu">
<ul class="usa-nav__primary usa-accordion" style="position: relative;">
<li class="radius-md">
<button
type="button"
class="usa-accordion__button"
aria-expanded="false"
aria-controls="sort-and-filter"
style="background-color: white; border: none; padding: 0;"
>
<span class="text-primary text-bold font-sans-md text-underline" style="text-decoration-thickness: 2px;">Sort & Filter</span>
<%= image_tag "images/usa-icons/arrow_drop_down.svg",
class: "usa-icon icon-down",
alt: "",
aria: { hidden: true } %>
<%= image_tag "images/usa-icons/arrow_drop_up.svg",
class: "usa-icon icon-up",
alt: "",
aria: { hidden: true } %>
</button>
<ul id="sort-and-filter" class="usa-nav__submenu text-no-wrap bg-white font-sans-xs width-auto"
style="position: absolute; top: 100%; left: 0; z-index: 1000; max-height: 80vh; overflow-y: auto;">
<% menu_items = [
{ label: "Evaluation progress: Completed", value: "status=completed" },
{ label: "Evaluation progress: In progress", value: "status=in_progress" },
{ label: "Evaluation progress: Not started", value: "status=not_started" },
{ label: "Evaluation progress: Recused", value: "status=recused" },
{ label: "Eligible for Evaluation", value: "eligible_for_evaluation=true" },
{ label: "Selected to Advance", value: "selected_to_advance=true" },
{ label: "Average score: high to low", value: "sort=average_score_high_to_low" },
{ label: "Average score: low to high", value: "sort=average_score_low_to_high" },
{ label: "Submission ID: high to low", value: "sort=submission_id_high_to_low" },
{ label: "Submission ID: low to high", value: "sort=submission_id_low_to_high" }
] %>
<% menu_items.each do |item| %>
<li class="usa-nav__submenu-item bg-primary-darker margin-y-2px">
<a href="#" class="text-white text-no-underline menu-item display-flex flex-align-center padding-y-05 padding-x-105 width-full"
data-action="click->sort-filter-menu#toggle"
data-sort-filter-menu-target="menuItem"
data-filter-value="<%= item[:value] %>">
<%= image_tag "images/usa-icons/check.svg",
width: 18,
height: 18,
alt: "Selected",
class: "icon-white checkmark margin-left-neg-3 padding-top-2px tablet:padding-top-05 tablet:margin-left-neg-1",
style: "display: none;" %>
<span class="text-white"><%= item[:label] %></span>
</a>
</li>
<% end %>
</ul>
</li>
</ul>
</nav>
</div>
</div>
</div>
4 changes: 2 additions & 2 deletions app/views/phases/_submissions_stats.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
alt: ""
)%>
<span class="text-top">
<span class="text-primary"><%= @submissions.eligible_for_evaluation.count %> of <%= @submissions_count %></span>
<span class="text-primary"><%= @eligible_count %> of <%= @submissions_count %></span>
submissions selected for evaluation
</span>
</div>
Expand All @@ -58,7 +58,7 @@
alt: ""
)%>
<span class="text-top">
<span class="text-primary"><%= @submissions.winner.count %> of <%= @submissions.eligible_for_evaluation.count %></span>
<span class="text-primary"><%= @selected_count %> of <%= @eligible_count %></span>
submissions selected to advance
</span>
</div>
Expand Down
Loading

0 comments on commit 8955d31

Please sign in to comment.