From bd6db735b84464ab1db317c6567831b457e74499 Mon Sep 17 00:00:00 2001 From: Angela Gomba Date: Mon, 20 May 2024 18:01:02 -0400 Subject: [PATCH] Sortable User Course List closes OUT-6339 OUT-6346 flag=none Test Plan: - There are many cases that are handled by the automated and unit tests. The following steps are to ensure the page loads and behaves properly: - Open the User Courses page (/courses) - Observe that each column now has a sorting icon next to the column name - Observe that the Published column is default sorted in ascending order - Click through the different columns and observe that sorting by those columns and in descending order works as expected Change-Id: Ie522e17f161f2a4d0ed8c35f62c5006162de9bd5 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/346737 Tested-by: Service Cloud Jenkins Reviewed-by: Martin Yosifov QA-Review: Martin Yosifov Product-Review: Kyle Rosenbaum --- app/controllers/courses_controller.rb | 47 ++++- app/helpers/courses_helper.rb | 20 ++ app/stylesheets/bundles/course_list.scss | 16 +- app/views/courses/index.html.erb | 116 +++++++++-- spec/controllers/courses_controller_spec.rb | 186 ++++++++++++++--- spec/helpers/courses_helper_spec.rb | 60 ++++++ spec/selenium/courses/course_index_spec.rb | 196 ++++++++++++++++++ .../courses/pages/course_index_page.rb | 32 +++ ui/featureBundles.ts | 1 + ui/features/courses/index.js | 29 +++ ui/features/courses/package.json | 5 + 11 files changed, 655 insertions(+), 53 deletions(-) create mode 100644 ui/features/courses/index.js create mode 100644 ui/features/courses/package.json diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 9949af1e3c1..fcabd1d0d5b 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -567,15 +567,50 @@ class CoursesController < ApplicationController end end - @past_enrollments.sort_by! { |e| [e.course.published? ? 0 : 1, Canvas::ICU.collation_key(e.long_name)] } - [@current_enrollments, @future_enrollments].each do |list| - list.sort_by! do |e| - [e.course.published? ? 0 : 1, e.active? ? 1 : 0, Canvas::ICU.collation_key(e.long_name)] - end - end + @current_enrollments = sort_enrollments(@current_enrollments, "current") + @past_enrollments = sort_enrollments(@past_enrollments, "past") + @future_enrollments = sort_enrollments(@future_enrollments, "future") end helper_method :load_enrollments_for_index + def sort_enrollments(enrollments, type) + sort_column = nil + order = nil + case type + when "current" + sort_column = params[:cc_sort] + order = params[:cc_order] + when "past" + sort_column = params[:pc_sort] + order = params[:pc_order] + when "future" + sort_column = params[:fc_sort] + order = params[:fc_order] + end + sorted_enrollments = enrollments.sort_by! do |e| + case sort_column + when "favorite" + @current_user.courses_with_primary_enrollment(:favorite_courses).map(&:id).include?(e.course_id) ? 0 : 1 + when "course" + e.course.name + when "nickname" + nickname = e.course.nickname_for(@current_user, nil) + [nickname.nil? ? 1 : 0, nickname] + when "term" + [e.course.enrollment_term.default_term? ? 1 : 0, e.course.enrollment_term.name] + when "enrolled_as" + e.readable_role_name + else + if type == "past" + [e.course.published? ? 0 : 1, Canvas::ICU.collation_key(e.long_name)] + else + [e.course.published? ? 0 : 1, e.active? ? 1 : 0, Canvas::ICU.collation_key(e.long_name)] + end + end + end + (order == "desc") ? sorted_enrollments.reverse : sorted_enrollments + end + def enrollments_for_index(type) instance_variable_get(:"@#{type}_enrollments") end diff --git a/app/helpers/courses_helper.rb b/app/helpers/courses_helper.rb index 02d52f51cf9..e52c66b4609 100644 --- a/app/helpers/courses_helper.rb +++ b/app/helpers/courses_helper.rb @@ -181,4 +181,24 @@ module CoursesHelper end end end + + def get_sorting_order(curr_col, sorted_col, order) + "desc" if (sorted_col.nil? && curr_col == "published") || (curr_col == sorted_col && order != "desc") + end + + def get_sorting_icon(curr_col, sorted_col, order) + if (curr_col == sorted_col) || (curr_col == "published" && sorted_col.nil?) + "icon-mini-arrow-#{(order == "desc") ? "down" : "up"}" + else + "icon-mini-arrow-double" + end + end + + def get_courses_params(table, col, params) + params.permit(:cc_sort, :cc_order, :pc_sort, :pc_order, :fc_sort, :fc_order).merge({ + "#{table}_sort": col, + "#{table}_order": get_sorting_order(col, params[:cc_sort], params[:cc_order]), + focus: table + }) + end end diff --git a/app/stylesheets/bundles/course_list.scss b/app/stylesheets/bundles/course_list.scss index 32a89978825..43449d2d5ba 100644 --- a/app/stylesheets/bundles/course_list.scss +++ b/app/stylesheets/bundles/course_list.scss @@ -24,16 +24,28 @@ /* Column widths are based on these cells */ .course-list-star-column { - width: 3%; + width: 10%; &.course-list-favorite-icon { color: $ic-color-icon-disabled; } } + .course-list-sort-icon { + color: #C7CDD1; + &.sorted { + color: $ic-font-color-dark; + } + } + .course-list-column-header { + a { + color: $ic-font-color-dark; + text-decoration: none; + } + } .course-list-term-column, .course-list-enrolled-as-column { width: 15%; } -.course-list-published-column { + .course-list-published-column { width: 7%; } .course-list-nickname-column { diff --git a/app/views/courses/index.html.erb b/app/views/courses/index.html.erb index b63639d4933..414e9d1eba5 100644 --- a/app/views/courses/index.html.erb +++ b/app/views/courses/index.html.erb @@ -16,6 +16,8 @@ # with this program. If not, see . %> +<% js_bundle :courses %> + <% if k5_user? all_courses_title = t('All Subjects') @@ -71,12 +73,38 @@ - <% if @show_star_column %><% end %> - - - - - + <% if @show_star_column %> + + <% end %> + + + + + @@ -95,12 +123,38 @@
<%= t ('Favorites') %><%= course_label %><%= t ('Nickname') %><%= t ('Term') %><%= t ('Enrolled as') %><%= t ('Published') %> + " id="cc_favorite"> + <%= t('Favorite') %> <%= get_sorting_icon("favorite", params[:cc_sort], params[:cc_order]) %>"> + + + " id="cc_course"> + <%= course_label %> <%= get_sorting_icon("course", params[:cc_sort], params[:cc_order]) %>"> + + + " id="cc_nickname"> + <%= t ('Nickname') %> <%= get_sorting_icon("nickname", params[:cc_sort], params[:cc_order]) %>"> + + + " id="cc_term"> + <%= t ('Term') %> <%= get_sorting_icon("term", params[:cc_sort], params[:cc_order]) %>"> + + + " id="cc_enrolled_as"> + <%= t ('Enrolled as') %> <%= get_sorting_icon("enrolled_as", params[:cc_sort], params[:cc_order]) %>"> + + + " id="cc_published"> + <%= t ('Published') %> <%= get_sorting_icon("published", params[:cc_sort], params[:cc_order]) %>"> + +
- <% if @show_star_column %><% end %> - - - - - + <% if @show_star_column %> + + <% end %> + + + + + @@ -119,12 +173,38 @@
<%= t ('Favorites') %><%= course_label %><%= t ('Nickname') %><%= t ('Term') %><%= t ('Enrolled as') %><%= t ('Published') %> + " id="pc_favorite"> + <%= t('Favorite') %> <%= get_sorting_icon("favorite", params[:pc_sort], params[:pc_order]) %>"> + + + " id="pc_course"> + <%= course_label %> <%= get_sorting_icon("course", params[:pc_sort], params[:pc_order]) %>"> + + + " id="pc_nickname"> + <%= t ('Nickname') %> <%= get_sorting_icon("nickname", params[:pc_sort], params[:pc_order]) %>"> + + + " id="pc_term"> + <%= t ('Term') %> <%= get_sorting_icon("term", params[:pc_sort], params[:pc_order]) %>"> + + + " id="pc_enrolled_as"> + <%= t ('Enrolled as') %> <%= get_sorting_icon("enrolled_as", params[:pc_sort], params[:pc_order]) %>"> + + + " id="pc_published"> + <%= t ('Published') %> <%= get_sorting_icon("published", params[:pc_sort], params[:pc_order]) %>"> + +
- <% if @show_star_column %><% end %> - - - - - + <% if @show_star_column %> + + <% end %> + + + + + diff --git a/spec/controllers/courses_controller_spec.rb b/spec/controllers/courses_controller_spec.rb index 4b2cf5fa713..e72e7e9098c 100644 --- a/spec/controllers/courses_controller_spec.rb +++ b/spec/controllers/courses_controller_spec.rb @@ -29,12 +29,12 @@ describe CoursesController do controller.instance_variable_set(:@domain_root_account, Account.default) end - def get_index(user = nil) + def get_index(user: nil, index_params: {}) user_session(user) if user user ||= @user controller.instance_variable_set(:@current_user, user) + get "index", params: index_params controller.load_enrollments_for_index - get "index" end it "forces login" do @@ -75,7 +75,7 @@ describe CoursesController do course_with_student_logged_in toggle_k5_setting(@course.account) - get_index @student + get_index(user: @student) expect(assigns[:js_bundles].flatten).to include :k5_theme expect(assigns[:css_bundles].flatten).to include :k5_theme, :k5_font end @@ -83,7 +83,7 @@ describe CoursesController do it "does not set k5_theme when k5 is off" do course_with_student_logged_in - get_index @student + get_index(user: @student) expect(assigns[:js_bundles].flatten).not_to include :k5_theme expect(assigns[:css_bundles].flatten).not_to include :k5_theme, :k5_font end @@ -93,7 +93,7 @@ describe CoursesController do toggle_k5_setting(@course.account) toggle_classic_font_setting(@course.account) - get_index @student + get_index(user: @student) expect(assigns[:css_bundles].flatten).to include :k5_theme expect(assigns[:css_bundles].flatten).not_to include :k5_font end @@ -139,6 +139,133 @@ describe CoursesController do end end + shared_examples "sorting" do + before do + @course1 = (type == "future") ? Account.default.courses.create!(name: "A", start_at: 1.month.from_now, restrict_enrollments_to_course_dates: true) : Account.default.courses.create!(name: "A") + @course2 = (type == "future") ? Account.default.courses.create!(name: "Z", start_at: 1.month.from_now, restrict_enrollments_to_course_dates: true) : Account.default.courses.create!(name: "Z") + + # user is enrolled as a student in course 1 + enrollment1 = course_with_student user: @student, course: @course1 + enrollment1.invite! + + # publish course 2 + @course2.offer! + # user is enrolled as a ta in course 2 + enrollment2 = course_with_ta course: @course2, user: @student, active_all: true + + term1 = @course1.root_account.enrollment_terms.create!(name: "Term 1") + @course1.enrollment_term = term1 + @course1.save! + + term2 = @course2.root_account.enrollment_terms.create!(name: "Term 2") + @course2.enrollment_term = term2 + @course2.save! + + @student.set_preference(:course_nicknames, @course1.id, "English") + @student.set_preference(:course_nicknames, @course2.id, "Math") + + @student.favorites.create!(context: @course2) + + if type == "past" + [enrollment1, enrollment2].each(&:complete!) + end + end + + context "on published column" do + it "lists unpublished courses after published" do + user_session(@student) + get_index + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id] + end + + it "lists unpublished courses after published when descending order" do + user_session(@student) + get_index(index_params: { sort_column => "published", order_column => "desc" }) + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id] + end + end + + context "on enrolled as column" do + it "lists enrollment type alphabetically" do + user_session(@student) + get_index(index_params: { sort_column => "enrolled_as" }) + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id] + end + + it "lists enrollment type reverse alphabetically when descending order" do + user_session(@student) + get_index(index_params: { sort_column => "enrolled_as", order_column => "desc" }) + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id] + end + end + + context "on term column" do + it "lists terms alphabetically" do + user_session(@student) + get_index(index_params: { sort_column => "term" }) + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id] + end + + it "lists terms reverse alphabetically when descending order" do + user_session(@student) + get_index(index_params: { sort_column => "term", order_column => "desc" }) + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id] + end + end + + context "on nickname column" do + it "lists course nicknames alphabetically" do + user_session(@student) + get_index(index_params: { sort_column => "nickname" }) + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id] + end + + it "lists course nicknames reverse alphabetically when descending order" do + user_session(@student) + get_index(index_params: { sort_column => "nickname", order_column => "desc" }) + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id] + end + end + + context "on course name column" do + it "lists course names alphabetically" do + user_session(@student) + get_index(index_params: { sort_column => "course" }) + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id] + end + + it "lists course names reverse alphabetically when descending order" do + user_session(@student) + get_index(index_params: { sort_column => "course", order_column => "desc" }) + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id] + end + end + + context "on favorites column" do + it "lists favorited courses first" do + user_session(@student) + get_index(index_params: { sort_column => "favorite" }) + if type == "past" + # Only active courses can be favorited. Therefore, we don't expect the sorting to affect the order. + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id] + else + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id] + end + end + + it "lists favorited courses last when descending order" do + user_session(@student) + get_index(index_params: { sort_column => "favorite", order_column => "desc" }) + if type == "past" + # Only active courses can be favorited. Therefore, the list will just be reversed from its ascending order. + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course2.id, @course1.id] + else + expect(assigns["#{type}_enrollments"].map(&:course_id)).to eq [@course1.id, @course2.id] + end + end + end + end + describe "current_enrollments" do it "groups enrollments by course and type" do # enrollments with multiple sections of the same type should be de-duped @@ -246,25 +373,6 @@ describe CoursesController do expect(assigns[:future_enrollments]).to be_empty end - describe "unpublished_courses" do - it "lists unpublished courses after published" do - # unpublished course - course1 = Account.default.courses.create! name: "A" - enrollment1 = course_with_student user: @student, course: course1 - enrollment1.invite! - expect(course1).to be_unpublished - - # published course - course2 = Account.default.courses.create! name: "Z" - course2.offer! - course_with_student course: course2, user: @student, active_all: true - - user_session(@student) - get_index - expect(assigns[:current_enrollments].map(&:course_id)).to eq [course2.id, course1.id] - end - end - context "as enrollment admin" do it "includes courses with no applicable start/end dates" do # no dates at all @@ -293,6 +401,14 @@ describe CoursesController do expect(assigns[:future_enrollments]).to be_empty end end + + describe "sorting" do + include_examples "sorting" do + let(:type) { "current" } + let(:sort_column) { "cc_sort" } + let(:order_column) { "cc_order" } + end + end end describe "past_enrollments" do @@ -502,7 +618,7 @@ describe CoursesController do course1.enrollment_term.update_attribute(:end_at, 1.month.ago) - get_index(@student) + get_index(user: @student) expect(response).to be_successful expect(assigns[:past_enrollments]).to be_empty expect(assigns[:current_enrollments]).to be_empty @@ -510,13 +626,13 @@ describe CoursesController do observer = user_with_pseudonym(active_all: true) add_linked_observer(@student, observer) - get_index(observer) + get_index(user: observer) expect(response).to be_successful expect(assigns[:past_enrollments]).to be_empty expect(assigns[:current_enrollments]).to be_empty expect(assigns[:future_enrollments]).to be_empty - get_index(teacher) + get_index(user: teacher) expect(response).to be_successful expect(assigns[:past_enrollments]).to eq [teacher_enrollment] expect(assigns[:current_enrollments]).to be_empty @@ -565,6 +681,14 @@ describe CoursesController do expect(assigns[:past_enrollments].map(&:course_id)).to eq [course2.id, course1.id] # Z, then A end end + + describe "sorting" do + include_examples "sorting" do + let(:type) { "past" } + let(:sort_column) { "pc_sort" } + let(:order_column) { "pc_order" } + end + end end describe "future_enrollments" do @@ -698,6 +822,14 @@ describe CoursesController do expect(assigns[:future_enrollments].map(&:course_id)).to eq [course2.id, course1.id] # Z, then A end end + + describe "sorting" do + include_examples "sorting" do + let(:type) { "future" } + let(:sort_column) { "fc_sort" } + let(:order_column) { "fc_order" } + end + end end describe "per-assignment permissions" do diff --git a/spec/helpers/courses_helper_spec.rb b/spec/helpers/courses_helper_spec.rb index b3abe2aa0a9..53e59860222 100644 --- a/spec/helpers/courses_helper_spec.rb +++ b/spec/helpers/courses_helper_spec.rb @@ -270,4 +270,64 @@ describe CoursesHelper do expect(format_course_section_date).to eq "(no date)" end end + + describe "sortable user course list helpers" do + context "get_sorting_order" do + it "returns 'desc' if we are sorting on the current col that is in ascending order" do + expect(get_sorting_order("favorite", "favorite", nil)).to eq("desc") + end + + it "returns 'desc' if the default col is being sorted on in ascending order" do + expect(get_sorting_order("published", nil, nil)).to eq("desc") + end + + it "returns nil if we are not sorting on the current col" do + expect(get_sorting_order("enrolled_as", "favorite", nil)).to be_nil + end + + it "returns nil if we are sorting on the current col that is in descending order" do + expect(get_sorting_order("favorite", "favorite", "desc")).to be_nil + end + end + + context "get_sorting_icon" do + it "returns the double arrow icon if we are not sorting on the given column" do + expect(get_sorting_icon("favorite", "published", "desc")).to eq("icon-mini-arrow-double") + end + + it "returns the upward arrow icon if we are sorting on the given column in ascending order" do + expect(get_sorting_icon("favorite", "favorite", nil)).to eq("icon-mini-arrow-up") + end + + it "returns the downward arrow icon if we are sorting on the given column in descending order" do + expect(get_sorting_icon("favorite", "favorite", "desc")).to eq("icon-mini-arrow-down") + end + end + + context "get_courses_params" do + it "returns the correct params for the given table" do + table = "cc" + column = "favorite" + old_params = ActionController::Parameters.new + new_params = ActionController::Parameters.new(cc_sort: column, cc_order: nil, focus: table) + expect(get_courses_params(table, column, old_params)).to eq(new_params.permit(:cc_sort, :cc_order, :focus)) + end + + it "returns the correct params for the given table and params for other tables" do + table = "cc" + column = "favorite" + old_params = ActionController::Parameters.new(pc_sort: "published") + new_params = ActionController::Parameters.new(cc_sort: column, cc_order: nil, focus: table, pc_sort: "published") + expect(get_courses_params(table, column, old_params)).to eq(new_params.permit(:cc_sort, :cc_order, :focus, :pc_sort)) + end + + it "only returns permitted params" do + table = "cc" + column = "favorite" + old_params = ActionController::Parameters.new(foo: "bar") + new_params = ActionController::Parameters.new(cc_sort: column, cc_order: nil, focus: table) + expect(get_courses_params(table, column, old_params)).to eq(new_params.permit(:cc_sort, :cc_order, :focus)) + end + end + end end diff --git a/spec/selenium/courses/course_index_spec.rb b/spec/selenium/courses/course_index_spec.rb index 572981bea1a..cd946cf56a8 100644 --- a/spec/selenium/courses/course_index_spec.rb +++ b/spec/selenium/courses/course_index_spec.rb @@ -177,4 +177,200 @@ describe "course index" do expect(fj('h2:contains("Create Course")')).to be_displayed end end + + context "sorting" do + it "by favorite column lists favorited courses first when ascending and last when descending" do + favorite_course = @current_courses[0] + @user.favorites.create!(context: favorite_course) + get "/courses" + # ascending + favorites_column_header(current_enrollments_selector).click + wait_for_ajaximations + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.first).to eq row_with_text(favorite_course.name) + + # descending + favorites_column_header(current_enrollments_selector).click + wait_for_ajaximations + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.last).to eq row_with_text(favorite_course.name) + end + + it "by course column lists alphabetically when ascending and reverse when descending" do + course_name = "Classic Course (Current)" + + # Selenium is having trouble clicking the nickname column header, even though it + # works fine in the UI. To test sorting, we just load the page with the + # url params to test nickname column sorting. + # Otherwise we would test it the same way as the other headers: + # get "/courses" + # title_column_header(current_enrollments_selector).click + # wait_for_ajaximations + get "/courses?cc_sort=course" + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.first).to eq row_with_text(course_name) + + # descending + get "/courses?cc_order=desc&cc_sort=course" + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.last).to eq row_with_text(course_name) + end + + it "by nickname column lists alphabetically when ascending and reverse when descending" do + course_nickname = "Classic Course Nickname (Current)" + course = @current_courses[0] + @user.set_preference(:course_nicknames, course.id, course_nickname) + + # Selenium is having trouble clicking the term column header, even though it + # works fine in the UI. To test sorting, we just load the page with the + # url params to test term column sorting. + # Otherwise we would test it the same way as the other headers: + # get "/courses" + # nickname_column_header(current_enrollments_selector).click + # wait_for_ajaximations + get "/courses?cc_sort=nickname" + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.first).to eq row_with_text(course_nickname) + + # descending + get "/courses?cc_order=desc&cc_sort=nickname" + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.last).to eq row_with_text(course_nickname) + end + + it "by term column lists alphabetically when ascending and reverse when descending" do + course = @current_courses[0] + term = course.root_account.enrollment_terms.create!(name: "Term 1") + course.enrollment_term = term + course.save! + + # Selenium is having trouble clicking the term column header, even though it + # works fine in the UI. To test sorting, we just load the page with the + # url params to test term column sorting. + # Otherwise we would test it the same way as the other headers: + # get "/courses" + # term_column_header(current_enrollments_selector).click + # wait_for_ajaximations + get "/courses?cc_sort=term" + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.first).to eq row_with_text(course.name) + + # descending + get "/courses?cc_order=desc&cc_sort=term" + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.last).to eq row_with_text(course.name) + end + + it "by enrolled as column lists alphabetically when ascending and reverse when descending" do + @current_courses << course_with_ta(course_name: "Classic Course 2 (Current)", account: @classic_account, user: @user, active_all: true).course + get "/courses" + # ascending + enrolled_as_column_header(current_enrollments_selector).click + wait_for_ajaximations + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.first.text.include?("Student")).to be true + expect(sorted_courses.last.text.include?("TA")).to be true + + # descending + enrolled_as_column_header(current_enrollments_selector).click + wait_for_ajaximations + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.first.text.include?("TA")).to be true + expect(sorted_courses.last.text.include?("Student")).to be true + end + + it "by published column lists published first when ascending and non-published first when descending" do + @current_courses << course_with_student(course_name: "Classic Course 2 (Current)", account: @classic_account, user: @user, active_all: true).course + unpublished_course = @current_courses[2] + unpublished_course.workflow_state = "created" + unpublished_course.save! + + get "/courses" + # sorted by published (ascending) by default + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.last).to eq row_with_text(unpublished_course.name) + + # descending + published_column_header(current_enrollments_selector).click + wait_for_ajaximations + + sorted_courses = table_rows(current_enrollments_selector) + # remove header row + sorted_courses.shift + expect(sorted_courses.first).to eq row_with_text(unpublished_course.name) + end + + it "is independent across tables" do + @current_courses << course_with_student(course_name: "Classic Course 2 (Current)", account: @classic_account, user: @user, active_all: true).course + unpublished_course = @current_courses[2] + unpublished_course.workflow_state = "created" + unpublished_course.save! + + @past_courses << course_with_ta(course_name: "Classic Course 3 (Current)", account: @classic_account, user: @user, active_all: true).course + course_as_ta = @past_courses[2] + course_as_ta.complete! + + favorite_course = @future_courses[0] + @user.favorites.create!(context: favorite_course) + + get "/courses" + + # sort the current courses by published (desc) column + published_column_header(current_enrollments_selector).click + wait_for_ajaximations + + # sort the past courses by enrolled as column + enrolled_as_column_header(past_enrollments_selector).click + wait_for_ajaximations + + # sort the future courses by favorites column + favorites_column_header(future_enrollments_selector).click + wait_for_ajaximations + + sorted_current_courses = table_rows(current_enrollments_selector) + sorted_past_courses = table_rows(past_enrollments_selector) + sorted_future_courses = table_rows(future_enrollments_selector) + + # remove header rows + sorted_current_courses.shift + sorted_past_courses.shift + sorted_future_courses.shift + + expect(sorted_current_courses.first).to eq row_with_text(unpublished_course.name) + expect(sorted_past_courses.last).to eq row_with_text(course_as_ta.name) + expect(sorted_future_courses.first).to eq row_with_text(favorite_course.name) + end + end end diff --git a/spec/selenium/courses/pages/course_index_page.rb b/spec/selenium/courses/pages/course_index_page.rb index 05ff67daaab..34272df6ecc 100644 --- a/spec/selenium/courses/pages/course_index_page.rb +++ b/spec/selenium/courses/pages/course_index_page.rb @@ -77,4 +77,36 @@ module CourseIndexPage def favorite_icon(course_name) f(favorite_icon_selector(course_name)) end + + def favorites_column_header(table) + table_header_row(table)[0] + end + + def title_column_header(table) + table_header_row(table)[1] + end + + def nickname_column_header(table) + table_header_row(table)[2] + end + + def term_column_header(table) + table_header_row(table)[3] + end + + def enrolled_as_column_header(table) + table_header_row(table)[4] + end + + def published_column_header(table) + table_header_row(table)[5] + end + + def table_rows(table) + ff("#{table} tr") + end + + def table_header_row(table) + ff("#{table} tr th") + end end diff --git a/ui/featureBundles.ts b/ui/featureBundles.ts index 3d68f36e74b..4460c04e054 100644 --- a/ui/featureBundles.ts +++ b/ui/featureBundles.ts @@ -82,6 +82,7 @@ const featureBundles: { course_statistics: () => import('./features/course_statistics/index'), course_wizard: () => import('./features/course_wizard/index'), course: () => import('./features/course/index'), + courses: () => import('./features/courses/index'), dashboard: () => import('./features/dashboard/index'), deep_linking_response: () => import('./features/deep_linking_response/index'), developer_keys_v2: () => import('./features/developer_keys_v2/index'), diff --git a/ui/features/courses/index.js b/ui/features/courses/index.js new file mode 100644 index 00000000000..78ba8bb88ec --- /dev/null +++ b/ui/features/courses/index.js @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This file is part of Canvas. + * + * Canvas is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3 of the License. + * + * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License along + * with this program. If not, see . + */ + +import ready from '@instructure/ready' + +ready(() => { + const params = new URLSearchParams(window.location.search) + const sortingTable = params.get('focus') + if (sortingTable) { + const sortingCol = params.get(sortingTable + '_sort') + const focusedHeader = document.querySelector('a#' + sortingTable + '_' + sortingCol) + if (focusedHeader) focusedHeader.focus() + } +}) diff --git a/ui/features/courses/package.json b/ui/features/courses/package.json new file mode 100644 index 00000000000..8bc15546753 --- /dev/null +++ b/ui/features/courses/package.json @@ -0,0 +1,5 @@ +{ + "name": "@canvas-features/courses", + "private": true, + "version": "1.0.0" +} \ No newline at end of file
<%= t ('Favorites') %><%= course_label %><%= t ('Nickname') %><%= t ('Term') %><%= t ('Enrolled as') %><%= t ('Published') %> + " id="fc_favorite"> + <%= t('Favorite') %> <%= get_sorting_icon("favorite", params[:fc_sort], params[:fc_order]) %>"> + + + " id="fc_course"> + <%= course_label %> <%= get_sorting_icon("course", params[:fc_sort], params[:fc_order]) %>"> + + + " id="fc_nickname"> + <%= t ('Nickname') %> <%= get_sorting_icon("nickname", params[:fc_sort], params[:fc_order]) %>"> + + + " id="fc_term"> + <%= t ('Term') %> <%= get_sorting_icon("term", params[:fc_sort], params[:fc_order]) %>"> + + + " id="fc_enrolled_as"> + <%= t ('Enrolled as') %> <%= get_sorting_icon("enrolled_as", params[:fc_sort], params[:fc_order]) %>"> + + + " id="fc_published"> + <%= t ('Published') %> <%= get_sorting_icon("published", params[:fc_sort], params[:fc_order]) %>"> + +