From f69b22298dc3bbf8eee5f482a77d1793411e75c2 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 15:58:47 +0800 Subject: [PATCH 001/173] feat: add CSRF token controller --- app/controllers/csrf_token_controller.rb | 12 ++++++++++++ config/routes.rb | 2 ++ 2 files changed, 14 insertions(+) create mode 100644 app/controllers/csrf_token_controller.rb diff --git a/app/controllers/csrf_token_controller.rb b/app/controllers/csrf_token_controller.rb new file mode 100644 index 00000000000..58026dd4ba6 --- /dev/null +++ b/app/controllers/csrf_token_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class CsrfTokenController < ApplicationController + def csrf_token + render json: { csrfToken: form_authenticity_token } + end + + protected + + def publicly_accessible? + true + end +end diff --git a/config/routes.rb b/config/routes.rb index 163fe94e3d4..b8f0297ebb6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,6 +74,8 @@ masquerades: 'user/masquerades' } + get 'csrf_token' => 'csrf_token#csrf_token' + resources :announcements, only: [:index] do post 'mark_as_read' end From 2f1199b4985c5d372dd472e6e6ba2342759e15df Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 15:57:41 +0800 Subject: [PATCH 002/173] chore(rails): remove webpacker --- Gemfile | 3 -- Gemfile.lock | 7 ----- bin/webpack | 27 ---------------- bin/webpack-dev-server | 70 ------------------------------------------ config/webpacker.yml | 27 ---------------- 5 files changed, 134 deletions(-) delete mode 100755 bin/webpack delete mode 100755 bin/webpack-dev-server delete mode 100644 config/webpacker.yml diff --git a/Gemfile b/Gemfile index e888fe982b7..344687ebb43 100644 --- a/Gemfile +++ b/Gemfile @@ -40,9 +40,6 @@ gem 'sass-rails' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' -# TODO: Check compatibility with webpacker 3.2.0 when it is released. -# https://github.com/rails/webpacker/blob/4f65c5ee58666bbe58b234c48d47ec7d48fab4d8/CHANGELOG.md -gem 'webpacker', '<= 5.4.4' # Internationalisation for JavaScript. gem 'i18n-js', '<= 3.10.0' diff --git a/Gemfile.lock b/Gemfile.lock index f57ffe3da69..44e373a302e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -625,11 +625,6 @@ GEM nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (~> 4.0) - webpacker (5.4.4) - activesupport (>= 5.2) - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) websocket (1.2.9) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) @@ -746,9 +741,7 @@ DEPENDENCIES uglifier (>= 1.3.0) unread validates_hostname - wdm (>= 0.0.3) webdrivers - webpacker (<= 5.4.4) workflow workflow-activerecord (>= 4.1, < 7.0) yard diff --git a/bin/webpack b/bin/webpack deleted file mode 100755 index ae33ffaa0bd..00000000000 --- a/bin/webpack +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env ruby -$stdout.sync = true - -require 'shellwords' - -ENV['RAILS_ENV'] ||= 'development' -RAILS_ENV = ENV['RAILS_ENV'] - -ENV['NODE_ENV'] ||= RAILS_ENV -NODE_ENV = ENV['NODE_ENV'] - -APP_PATH = File.expand_path('../client', __dir__) -NODE_MODULES_PATH = File.join(APP_PATH, 'node_modules') -WEBPACK_CONFIG = File.join(APP_PATH, RAILS_ENV == 'development' ? 'webpack.dev.js' : 'webpack.prod.js') - -unless File.exist?(WEBPACK_CONFIG) - puts 'Webpack configuration not found.' - puts 'Please run bundle exec rails webpacker:install to install webpacker' - exit! -end - -env = { 'NODE_PATH' => NODE_MODULES_PATH.shellescape } -cmd = RAILS_ENV == 'development' ? [ 'yarn build:development' ] : [ 'yarn build:production' ] - -Dir.chdir(APP_PATH) do - exec env, *cmd -end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server deleted file mode 100755 index 3ed8e0d01b4..00000000000 --- a/bin/webpack-dev-server +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env ruby -# TODO: This is the default template and needs to be configured to work with our -# client/ setup. File is retained as webpacker checks the presence of this file. -$stdout.sync = true - -require "shellwords" -require "yaml" -require "socket" - -ENV["RAILS_ENV"] ||= "development" -RAILS_ENV = ENV["RAILS_ENV"] - -ENV["NODE_ENV"] ||= RAILS_ENV -NODE_ENV = ENV["NODE_ENV"] - -APP_PATH = File.expand_path("../client", __dir__) -CONFIG_FILE = File.join(APP_PATH, "../config/webpacker.yml") -NODE_MODULES_PATH = File.join(APP_PATH, "node_modules") -WEBPACK_CONFIG = File.join(APP_PATH, RAILS_ENV == 'development' ? 'webpack.dev.js' : 'webpack.prod.js') - -DEFAULT_LISTEN_HOST_ADDR = NODE_ENV == 'development' ? 'localhost' : '0.0.0.0' - -def args(key) - index = ARGV.index(key) - index ? ARGV[index + 1] : nil -end - -begin - dev_server = YAML.load_file(CONFIG_FILE)[RAILS_ENV]["dev_server"] - - HOSTNAME = args('--host') || dev_server["host"] - PORT = args('--port') || dev_server["port"] - HTTPS = ARGV.include?('--https') || dev_server["https"] - DEV_SERVER_ADDR = "http#{"s" if HTTPS}://#{HOSTNAME}:#{PORT}" - LISTEN_HOST_ADDR = args('--listen-host') || DEFAULT_LISTEN_HOST_ADDR - -rescue Errno::ENOENT, NoMethodError - $stdout.puts "Webpack dev_server configuration not found in #{CONFIG_FILE}." - $stdout.puts "Please run bundle exec rails webpacker:install to install webpacker" - exit! -end - -begin - server = TCPServer.new(LISTEN_HOST_ADDR, PORT) - server.close - -rescue Errno::EADDRINUSE - $stdout.puts "Another program is running on port #{PORT}. Set a new port in #{CONFIG_FILE} for dev_server" - exit! -end - -# Delete supplied host, port and listen-host CLI arguments -["--host", "--port", "--listen-host"].each do |arg| - ARGV.delete(args(arg)) - ARGV.delete(arg) -end - -env = { "NODE_PATH" => NODE_MODULES_PATH.shellescape } - -cmd = [ - "#{NODE_MODULES_PATH}/.bin/webpack-dev-server", "--progress", "--color", - "--config", WEBPACK_CONFIG, - "--host", LISTEN_HOST_ADDR, - "--public", "#{HOSTNAME}:#{PORT}", - "--port", PORT.to_s -] + ARGV - -Dir.chdir(APP_PATH) do - exec env, *cmd -end diff --git a/config/webpacker.yml b/config/webpacker.yml deleted file mode 100644 index 90aca9f51dc..00000000000 --- a/config/webpacker.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Note: You must restart bin/webpack-dev-server for changes to take effect - -default: &default - public_output_path: webpack - - # Reload manifest.json on all requests so we reload latest compiled packs - cache_manifest: false - # We will do precompilation of packs manually. - compile: false - -development: - <<: *default - - dev_server: - host: localhost - port: 8080 - hmr: false - https: false - -test: - <<: *default - -production: - <<: *default - - # Cache manifest.json for performance - cache_manifest: true From a0f5abc16755080f51fb6f44725998a650799bbf Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:00:15 +0800 Subject: [PATCH 003/173] style(rails): remove unused stylesheets --- app/assets/stylesheets/_variables.scss | 35 ---- app/assets/stylesheets/application.scss.erb | 25 --- app/assets/stylesheets/attachments.scss | 8 - .../question_bundle_assignments.scss | 15 -- app/assets/stylesheets/course/layout.scss | 163 ------------------ app/assets/stylesheets/course/statistics.scss | 3 - app/assets/stylesheets/formatters.scss | 7 - app/assets/stylesheets/jobs.scss | 6 - app/assets/stylesheets/layout.scss | 65 ------- app/assets/stylesheets/mixins/_dim.scss | 6 - app/assets/stylesheets/mixins/_image.scss | 12 -- .../stylesheets/mixins/_text_overflow.scss | 5 - app/assets/stylesheets/users.scss | 7 - 13 files changed, 357 deletions(-) delete mode 100644 app/assets/stylesheets/_variables.scss delete mode 100644 app/assets/stylesheets/application.scss.erb delete mode 100644 app/assets/stylesheets/attachments.scss delete mode 100644 app/assets/stylesheets/course/assessment/question_bundle_assignments.scss delete mode 100644 app/assets/stylesheets/course/layout.scss delete mode 100644 app/assets/stylesheets/course/statistics.scss delete mode 100644 app/assets/stylesheets/formatters.scss delete mode 100644 app/assets/stylesheets/jobs.scss delete mode 100644 app/assets/stylesheets/layout.scss delete mode 100644 app/assets/stylesheets/mixins/_dim.scss delete mode 100644 app/assets/stylesheets/mixins/_image.scss delete mode 100644 app/assets/stylesheets/mixins/_text_overflow.scss delete mode 100644 app/assets/stylesheets/users.scss diff --git a/app/assets/stylesheets/_variables.scss b/app/assets/stylesheets/_variables.scss deleted file mode 100644 index 3c020366c5b..00000000000 --- a/app/assets/stylesheets/_variables.scss +++ /dev/null @@ -1,35 +0,0 @@ -// -// Variables -// -------------------------------------------------- - -//== Colors -// -$grey: #808080 !default; -$red: #ff0000 !default; -$white: #ffffff !default; - -//## Variables for colors used in Coursemology. - -//== Fonts -// -//## Code editor typography. -$code-font-family: $font-family-monospace !default; -$code-font-size: $font-size-base !default; - -//== Icons and Logos -// - -//## Common sizes to be used across Coursemology. -$picture-thumb: 25px !default; -$picture-small: 75px !default; -$picture-medium: 100px !default; -$picture-large: 140px !default; - -//## Variables for icons or logos used in Coursemology. -$course-user-badge-achievement-badge: $picture-thumb !default; - -$course-layout-sidebar-logo: $picture-medium !default; - -$user-profile-picture: $picture-large !default; - -//== Others diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb deleted file mode 100644 index e3a35dba596..00000000000 --- a/app/assets/stylesheets/application.scss.erb +++ /dev/null @@ -1,25 +0,0 @@ -@import 'layout'; -@import 'mixins/*'; - -<% -# Import the rest of the files; @import '**/*' will include application.scss multiple times. -# This is not perfect because every time a new file is added all assets need to be cleaned for the -# new set of assets to be generated. -# -# TODO: Use compass-import-once after Compass/compass#1951 is fixed -# TODO: Revert to @import '**/*' after sass/sass#139 is fixed in sass-4.0. -exclude_imports = ['layout', 'mixins', 'application.scss'] -imports = Dir["#{__dir__}/*"] -imports.reject! do |path| - basename = File.basename(path, '.*') - basename.start_with?('_') || exclude_imports.include?(basename) -end -imports.map! do |path| - file_path = File.file?(path) - path = path[(__dir__.length + 1)..] - file_path ? path : "#{path}/**/*" -end - -imports.each do |file| %> -@import '<%= file %>'; -<% end %> diff --git a/app/assets/stylesheets/attachments.scss b/app/assets/stylesheets/attachments.scss deleted file mode 100644 index 74f0c63785d..00000000000 --- a/app/assets/stylesheets/attachments.scss +++ /dev/null @@ -1,8 +0,0 @@ -.attachment { - margin-bottom: 0.5em; - - .delete-attachment, - .uploaded-by { - padding-left: 1em; - } -} diff --git a/app/assets/stylesheets/course/assessment/question_bundle_assignments.scss b/app/assets/stylesheets/course/assessment/question_bundle_assignments.scss deleted file mode 100644 index 70f6f0cf067..00000000000 --- a/app/assets/stylesheets/course/assessment/question_bundle_assignments.scss +++ /dev/null @@ -1,15 +0,0 @@ -.course-assessment-question-bundle-assignments { - .validation-desc { - margin-top: 5px; - } - - .question-group-select { - display: inline-block; - width: 70%; - margin-right: 5px; - } - - .question-group-errors { - display: inline-block; - } -} diff --git a/app/assets/stylesheets/course/layout.scss b/app/assets/stylesheets/course/layout.scss deleted file mode 100644 index 741ddc97a49..00000000000 --- a/app/assets/stylesheets/course/layout.scss +++ /dev/null @@ -1,163 +0,0 @@ -$sidebar-width: 21rem; -$sidebar-margin-side: 1.5rem; - -.course-layout { - display: flex; - - @media (max-width: $screen-sm-min) { - flex-direction: column; - } - - #course-badge-achievement { - margin-bottom: 1em; - margin-left: 0.5em; - - .image > img { - height: $course-user-badge-achievement-badge; - width: $course-user-badge-achievement-badge; - } - - .achievement > a:hover { - text-decoration: none; - } - - .achievement-difference { - margin-left: 6px; - } - } - - #course-badge-level { - margin-left: 0.5em; - - .experience-points { - font-weight: 300; - } - - .level { - font-size: 150%; - margin-right: 0.5em; - } - - .next-level { - font-size: 85%; - margin-bottom: 1em; - } - - .progress { - height: 12px; - margin-bottom: 3px; - margin-top: 6px; - } - } - - #course-sidebar-logo { - .image > img { - height: $course-layout-sidebar-logo; - width: $course-layout-sidebar-logo; - } - } - - #course-navigation-sidebar { - .nav-icons > .fa { - font-size: 1.5em; - height: 0; - line-height: 0; - margin-right: 0.2em; - position: relative; - text-align: center; - top: 0.1em; - width: 1.5em; - } - - .unread { - margin-left: 0.4em; - } - - #admin-header-sidebar { - font-weight: bold; - margin: 30px 15px 10px; - } - } - - #users-container { - display: flex; - flex-wrap: wrap; - } - - .user { - align-items: center; - display: flex; - padding: 1rem $sidebar-margin-side; - - #user-sidebar { - display: flex; - flex-direction: column; - flex-grow: 1; - overflow: hidden; - text-overflow: ellipsis; - } - - #user-link-sidebar { - @include text-overflow; - font-size: 18px; - font-weight: bold; - flex: 1; - } - - #manage-email-subscription-link-sidebar { - font-size: 12px; - } - - .image img { - max-width: inherit; - } - - .image, - #user-link-sidebar { - display: table-cell; - vertical-align: middle; - } - } - - .notification + .notification { - margin-top: 10px; - } - - #hide-sidebar { - border: 0; - } - - #show-sidebar { - border: 0; - position: absolute; - z-index: 999; - } - - #full-sidebar { - margin-right: $sidebar-margin-side; - width: $sidebar-width; - - @media (max-width: $screen-sm-min) { - margin-bottom: $sidebar-margin-side; - margin-right: 0; - width: 100%; - } - } - - // TODO: Revisit this once SPA and see if we can deal with just `width: 100%;` - // For some reasons in /courses/:id/assessments/:id/submissions/:id/edit page, - // `width: 100%` causes `.page-content` to take the width of `.course-layout`. - // This 'patch' calculates the true width of `.page-content` and ensure the - // content never bulges out of `.course-layout`. I am not proud of this patch. - .page-content { - width: calc(100% - #{$sidebar-width} - #{$sidebar-margin-side}); - - @media (max-width: $screen-sm-min) { - width: 100%; - } - } - - .collapse:not(.in) + .page-content { - width: 100%; - } -} diff --git a/app/assets/stylesheets/course/statistics.scss b/app/assets/stylesheets/course/statistics.scss deleted file mode 100644 index 9a8b288ae85..00000000000 --- a/app/assets/stylesheets/course/statistics.scss +++ /dev/null @@ -1,3 +0,0 @@ -.progress { - margin: 0; -} diff --git a/app/assets/stylesheets/formatters.scss b/app/assets/stylesheets/formatters.scss deleted file mode 100644 index a50dd0499c0..00000000000 --- a/app/assets/stylesheets/formatters.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Styles for formatting resources. -.user { - > .image > img { - @include fit-round-img(50px); - margin-right: 0.5em; - } -} diff --git a/app/assets/stylesheets/jobs.scss b/app/assets/stylesheets/jobs.scss deleted file mode 100644 index 726c4349aa6..00000000000 --- a/app/assets/stylesheets/jobs.scss +++ /dev/null @@ -1,6 +0,0 @@ -.jobs.show { - .spinner { - font-size: 500%; - text-align: center; - } -} diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss deleted file mode 100644 index a4b5c02deb7..00000000000 --- a/app/assets/stylesheets/layout.scss +++ /dev/null @@ -1,65 +0,0 @@ -@import 'bootstrap-sprockets'; -@import 'bootstrap'; - -@import 'variables'; -@import 'tokenfield-typeahead'; -@import 'bootstrap-tokenfield'; - -// Fixes for compatibility with Typeahead v0.11 -.tt-menu { - @extend .tt-dropdown-menu; -} - -@import 'font-awesome'; - -// scss-lint:disable SelectorFormat -div.ace_editor, // We disable the linter because this is generated by script. -// scss-lint:enable SelectorFormat -textarea.code { - font-family: $code-font-family; - font-size: $code-font-size; -} - -table.codehilite { - tr { - td { - border: 0; - padding: 0; - - &.line-number { - font-family: $font-family-monospace; - font-size: ($font-size-base - 1); - width: 1%; // Force the line numbers to occupy as little space as possible. - - // Inspired by GitHub to ensure copying the content does not copy line numbers - &::before { - content: attr(data-line-number); - padding-right: $line-height-computed; - } - } - } - } - - pre { - background-color: transparent; - border: 0; - margin: 0; - padding: 0; - } -} - -.sidebar { - padding: 0; -} - -.nav-tabs { - margin-bottom: 1em; -} - -.attachment-uploader { - margin-bottom: 1em; -} - -img { - max-width: 100%; -} diff --git a/app/assets/stylesheets/mixins/_dim.scss b/app/assets/stylesheets/mixins/_dim.scss deleted file mode 100644 index a0f1479271e..00000000000 --- a/app/assets/stylesheets/mixins/_dim.scss +++ /dev/null @@ -1,6 +0,0 @@ -// Dimming style mixin. Indicates the item is currently disabled. -@mixin dim($bg-color) { - background-color: $bg-color; - opacity: 0.9; - padding: 0.8em; -} diff --git a/app/assets/stylesheets/mixins/_image.scss b/app/assets/stylesheets/mixins/_image.scss deleted file mode 100644 index 44d97e5593d..00000000000 --- a/app/assets/stylesheets/mixins/_image.scss +++ /dev/null @@ -1,12 +0,0 @@ -// Keep the aspect ratio of the image. -@mixin no-stretch { - object-fit: cover; - object-position: center; -} - -@mixin fit-round-img($width, $height: $width) { - @include no-stretch; - width: $width; - height: $height; - border-radius: 50%; -} diff --git a/app/assets/stylesheets/mixins/_text_overflow.scss b/app/assets/stylesheets/mixins/_text_overflow.scss deleted file mode 100644 index 62d8cf5f27f..00000000000 --- a/app/assets/stylesheets/mixins/_text_overflow.scss +++ /dev/null @@ -1,5 +0,0 @@ -@mixin text-overflow() { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss deleted file mode 100644 index e4068771a0e..00000000000 --- a/app/assets/stylesheets/users.scss +++ /dev/null @@ -1,7 +0,0 @@ -.users { - &.show { - .image > img { - @include fit-round-img($user-profile-picture); - } - } -} From 8cf6b4846c0e598d5d8b534f0d0b8cbee27fa040 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:00:37 +0800 Subject: [PATCH 004/173] style(rails): remove unused scripts --- app/assets/javascripts/application.js | 22 ------------------- app/assets/javascripts/layout.js | 22 ------------------- .../patches/dropdown_mobile_fix.js | 17 -------------- 3 files changed, 61 deletions(-) delete mode 100644 app/assets/javascripts/application.js delete mode 100644 app/assets/javascripts/layout.js delete mode 100644 app/assets/javascripts/patches/dropdown_mobile_fix.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js deleted file mode 100644 index c805df19169..00000000000 --- a/app/assets/javascripts/application.js +++ /dev/null @@ -1,22 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, -// vendor/assets/javascripts, or vendor/assets/javascripts of plugins, if any, can be referenced -// here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. -// -// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details -// about supported directives. -// -//= require jquery_ujs -//= require i18n/translations -//= require bootstrap-sprockets -//= require twitter/typeahead -//= require bootstrap-tokenfield -//= require simple_form-bootstrap -//= require layout -//= require cocoon -//= require_tree . diff --git a/app/assets/javascripts/layout.js b/app/assets/javascripts/layout.js deleted file mode 100644 index 5b3cc2c468b..00000000000 --- a/app/assets/javascripts/layout.js +++ /dev/null @@ -1,22 +0,0 @@ -(function ($) { - 'use strict'; - - function initializeComponents(element) { - $('[data-toggle="popover"]', element).popover(); - // Tooltips are attached to elements with a title attribute, except for the Facebook button. - // See https://github.com/Coursemology/coursemology-theme/pull/5 - $('[title]', element).not('.fb-like *').tooltip(); - } - - // Queue component initialisation until the script has completely loaded. - // - // This prevents missing definitions for things like Ace themes, which are loaded after the - // application script. - $(function () { - initializeComponents(document); - - $(document).on('nested:fieldAdded', function (e) { - initializeComponents(e.field); - }); - }); -})(jQuery); diff --git a/app/assets/javascripts/patches/dropdown_mobile_fix.js b/app/assets/javascripts/patches/dropdown_mobile_fix.js deleted file mode 100644 index 9ccd566d0af..00000000000 --- a/app/assets/javascripts/patches/dropdown_mobile_fix.js +++ /dev/null @@ -1,17 +0,0 @@ -// Dropdown does not work on iOS devices as only click delegation for a and input are supported -// Event delegation for iOS http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html -// Proposed solution is modified from http://stackoverflow.com/a/22318440 - -(function ($) { - 'use strict'; - - function initializeDropdownEventListener() { - $('[data-toggle=dropdown]').each(function () { - this.addEventListener('click', function () {}, false); - }); - } - - $(function () { - initializeDropdownEventListener(); - }); -})(jQuery); From dce18871a3f9e0ab95297deab7d929108a650453 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:03:31 +0800 Subject: [PATCH 005/173] style(rails): remove instance themes --- Gemfile | 4 -- Gemfile.lock | 9 ---- app/controllers/application_controller.rb | 1 - .../concerns/application_theming_concern.rb | 31 ----------- app/helpers/application_helper.rb | 1 - app/helpers/application_theming_helper.rb | 28 ---------- .../default/assets/images/default/.keep | 0 .../default/assets/javascripts/default/all.js | 8 --- .../assets/stylesheets/default/all.scss.erb | 30 ----------- app/themes/default/locales/.keep | 0 .../default/views/layouts/_topbar.html.slim | 51 ------------------- .../default/views/layouts/default.html.slim | 28 ---------- .../application_controller_spec.rb | 15 ------ .../application_theming_helper_spec.rb | 45 ---------------- 14 files changed, 251 deletions(-) delete mode 100644 app/controllers/concerns/application_theming_concern.rb delete mode 100644 app/helpers/application_theming_helper.rb delete mode 100644 app/themes/default/assets/images/default/.keep delete mode 100644 app/themes/default/assets/javascripts/default/all.js delete mode 100644 app/themes/default/assets/stylesheets/default/all.scss.erb delete mode 100644 app/themes/default/locales/.keep delete mode 100644 app/themes/default/views/layouts/_topbar.html.slim delete mode 100644 app/themes/default/views/layouts/default.html.slim delete mode 100644 spec/helpers/application_theming_helper_spec.rb diff --git a/Gemfile b/Gemfile index 344687ebb43..798921b63e4 100644 --- a/Gemfile +++ b/Gemfile @@ -196,10 +196,6 @@ gem 'cancancan' # We also want stricter sanitization. gem 'rails_utils', git: 'https://github.com/raymondtangsc/rails_utils.git', branch: 'full-sanitize-flash' -# Themes for instances -gem 'themes_on_rails', '>= 0.3.1', git: 'https://github.com/raymondtangsc/themes_on_rails', - branch: 'xtang/rails_6' - # Forms made easy for Rails gem 'simple_form' gem 'simple_form-bootstrap', git: 'https://github.com/purfectliterature/simple_form-bootstrap' diff --git a/Gemfile.lock b/Gemfile.lock index 44e373a302e..6f40b03bb9c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -55,14 +55,6 @@ GIT rails_utils (4.0.0) rails (>= 3.2) -GIT - remote: https://github.com/raymondtangsc/themes_on_rails - revision: 4446d795aad4eaf730f7250646f25bfdb09de799 - branch: xtang/rails_6 - specs: - themes_on_rails (0.4.0) - rails (>= 3.2) - GEM remote: https://rubygems.org/ specs: @@ -734,7 +726,6 @@ DEPENDENCIES spring sprockets (< 4.0.0) stackprof - themes_on_rails (>= 0.3.1)! traceroute twitter-typeahead-rails tzinfo-data diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6761178fee9..3f3db35009d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,7 +10,6 @@ class ApplicationController < ActionController::Base include ApplicationControllerMultitenancyConcern include ApplicationComponentsConcern include ApplicationInternationalizationConcern - include ApplicationThemingConcern include ApplicationUserConcern include ApplicationUserTimeZoneConcern include ApplicationInstanceUserConcern diff --git a/app/controllers/concerns/application_theming_concern.rb b/app/controllers/concerns/application_theming_concern.rb deleted file mode 100644 index d675e06dc40..00000000000 --- a/app/controllers/concerns/application_theming_concern.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true -module ApplicationThemingConcern - extend ActiveSupport::Concern - - included do - theme :deduce_theme - end - - private - - def deduce_theme - priorities = [] - priorities << current_tenant.host if current_tenant - priorities.find(&method(:theme_exists?)) || 'default' - end - - # Checks if the given theme exists. - # - # @param [String] theme_name The name of the theme to check. - # @return [Boolean] True if the theme exists. - def theme_exists?(theme_name) - File.exist?("#{themes_path}/#{theme_name}") - end - - # Gets the path to the themes directory. - # - # @return [String] The path to the themes directory - def themes_path - "#{Rails.root}/app/themes" - end -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index ccd79bdb51f..809538c63cb 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -3,7 +3,6 @@ module ApplicationHelper include FontAwesome::Rails::IconHelper - include ApplicationThemingHelper include ApplicationAnnouncementsHelper include ApplicationJobsHelper include ApplicationWidgetsHelper diff --git a/app/helpers/application_theming_helper.rb b/app/helpers/application_theming_helper.rb deleted file mode 100644 index 80f0e1b9c21..00000000000 --- a/app/helpers/application_theming_helper.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true -module ApplicationThemingHelper - def application_resources - # Used to include external javascript/css files, Currently it's empty - end - - # TODO: Remove this once fully SPA - def page_class - return nil if content_for?(:page_class_specified) - - page_class = super - content_for(:page_class_specified) { page_class } - @page_class = page_class - end - - # TODO: Remove this once fully SPA - def rails_page? - class_identifier = page_class.split.first - - [ - 'high-voltage-pages', - 'user-sessions', - 'user-registrations', - 'devise-passwords', - 'devise-confirmations' - ].include? class_identifier - end -end diff --git a/app/themes/default/assets/images/default/.keep b/app/themes/default/assets/images/default/.keep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/themes/default/assets/javascripts/default/all.js b/app/themes/default/assets/javascripts/default/all.js deleted file mode 100644 index 87af27d8d68..00000000000 --- a/app/themes/default/assets/javascripts/default/all.js +++ /dev/null @@ -1,8 +0,0 @@ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically -// be included in the compiled file accessible from http://example.com/assets/application.js -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// the compiled file. -// -//= require application -//= require_tree . diff --git a/app/themes/default/assets/stylesheets/default/all.scss.erb b/app/themes/default/assets/stylesheets/default/all.scss.erb deleted file mode 100644 index 36b330cdb5e..00000000000 --- a/app/themes/default/assets/stylesheets/default/all.scss.erb +++ /dev/null @@ -1,30 +0,0 @@ -// This is a manifest file that'll automatically include all the stylesheets available in this -// directory and any sub-directories. You're free to add application-wide styles to this file and -// they'll appear at the top of the compiled file, but it's generally better to create a new file -// per style scope. -// -@import 'application'; -@import 'pygments-css/github'; -<% -# Import the rest of the files; @import '**/*' will include application.scss multiple times. -# This is not perfect because every time a new file is added all assets need to be cleaned for the -# new set of assets to be generated. -# This is taken from app/assets/stylesheets/application.css.erb -# -# TODO: Use compass-import-once after Compass/compass#1951 is fixed -# TODO: Revert to @import '**/*' after sass/sass#139 is fixed in sass-4.0. -exclude_imports = ['layout', 'all.scss'] -imports = Dir["#{__dir__}/*"] -imports.reject! do |path| - basename = File.basename(path, '.*') - basename.start_with?('_') || exclude_imports.include?(basename) -end -imports.map! do |path| - file_path = File.file?(path) - path = path[(__dir__.length + 1)..-1] - file_path ? path : "#{path}/**/*" -end - -imports.each do |file| %> -@import '<%= file %>'; -<% end %> diff --git a/app/themes/default/locales/.keep b/app/themes/default/locales/.keep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/themes/default/views/layouts/_topbar.html.slim b/app/themes/default/views/layouts/_topbar.html.slim deleted file mode 100644 index f22ea549f4d..00000000000 --- a/app/themes/default/views/layouts/_topbar.html.slim +++ /dev/null @@ -1,51 +0,0 @@ -nav.navbar.navbar-inverse.navbar-fixed-top role="navigation" - div.container-fluid - div.navbar-header - button.navbar-toggle.collapsed type="button" data-toggle="collapse" data-target="#site-navigation-navbar" aria-expanded="false" aria-controls="navbar" - span.sr-only - = t('layout.navbar.toggle_navigation') - span.icon-bar - span.icon-bar - span.icon-bar - a.navbar-brand href=root_path - = t('layout.coursemology') - div.collapse.navbar-collapse#site-navigation-navbar - ul.nav.navbar-nav.pull-right - li - - my_courses = user_signed_in? && Course.containing_user(current_user).ordered_by_start_at - - if my_courses.present? - a.dropdown-toggle data-toggle="dropdown" - => t('layout.navbar.courses') - span.caret - ul.dropdown-menu.pull-right.courses-menu - - my_courses.each do |course| - li = link_to(format_inline_text(course.title), course_path(course)) - li.divider role='separator' - li = link_to(t('layout.navbar.all_courses'), courses_path) - - else - = link_to(t('layout.navbar.courses'), courses_path) - li - = link_to(t('layout.navbar.help'), '#') - - if user_signed_in? - li - a.dropdown-toggle data-toggle="dropdown" - => current_user.name - span.caret - ul.dropdown-menu - li - = link_to(t('user.admin.navbar.account_settings'), edit_user_profile_path) - - if can?(:manage, :all) - li - = link_to(t('layout.navbar.admin_panel'), admin_path) - - if can?(:manage, current_tenant) - li - = link_to(t('layout.navbar.instance_admin_panel'), admin_instance_admin_path) - li - = link_to(t('layout.navbar.sign_out'), destroy_user_session_path, method: :delete) - - if user_masquerade? - li = link_to t('layout.navbar.stop_masquerading'), back_masquerade_path(current_user) - - else - li - = link_to(t('layout.navbar.register'), new_user_registration_path) - li - = link_to(t('layout.navbar.sign_in'), new_user_session_path) diff --git a/app/themes/default/views/layouts/default.html.slim b/app/themes/default/views/layouts/default.html.slim deleted file mode 100644 index de90559f80e..00000000000 --- a/app/themes/default/views/layouts/default.html.slim +++ /dev/null @@ -1,28 +0,0 @@ -doctype html -html - head - title - = page_title - meta http-equiv="X-UA-Compatible" content="IE=edge" - meta name="status" content=response.status - = server_context_meta_tag - = viewport_meta_tag - = application_resources - = stylesheet_link_tag 'default/all', media: 'all' - = csrf_meta_tags - = webpack_assets_tag - = javascript_include_tag 'default/all' - = header_tags - = render 'layouts/favicon' - - body - - if rails_page? - = render 'layouts/topbar' - div#root.container-fluid - = global_announcements - = flash_messages - div class=@page_class - = yield - - else - div#root - = yield diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 857b6ff3b39..1dd003a9dd7 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -117,21 +117,6 @@ def publicly_accessible? end end - describe ApplicationThemingConcern do - context 'when the instance has a theme' do - it 'uses the theme' do - pending 'an instance with a theme' - raise - end - end - - context 'when the instance does not have a theme' do - it 'uses the default theme' do - expect(controller.send(:deduce_theme)).to eq('default') - end - end - end - describe ApplicationUserConcern do context 'when the action raises a CanCan::AccessDenied' do run_rescue diff --git a/spec/helpers/application_theming_helper_spec.rb b/spec/helpers/application_theming_helper_spec.rb deleted file mode 100644 index 5b9549c9b0f..00000000000 --- a/spec/helpers/application_theming_helper_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.describe ApplicationThemingHelper, type: :helper do - module ApplicationThemingHelper - include RenderWithinLayoutHelper - end - - describe '#page_class' do - subject { helper.page_class } - - context 'when it has never been called before' do - it 'returns the page class' do - expect(subject).not_to be_blank - end - end - - context 'when it has been called before' do - before { helper.page_class } - it { is_expected.to be_blank } - end - - describe 'nested layouts' do - let(:views_directory) do - path = Pathname.new("#{__dir__}/../fixtures/helpers/application_theming_helper") - path.realpath - end - - before { controller.prepend_view_path(views_directory) } - subject { render template: 'page_class_nested_inside', layout: 'layouts/page_class' } - - it 'does not label the root container with the page class' do - expect(subject).not_to have_tag('div.action-view-test-case-test#root') - end - - it 'does not label the nested layout container with the page class' do - expect(subject).not_to have_tag('div.action-view-test-case-test#nested') - end - - it 'labels the deepest-nested container with the page class' do - expect(subject).to have_tag('div.action-view-test-case-test#nested-inside') - end - end - end -end From f84bc8643e32d5f932b7b744c49dba3c086c1c54 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:27:45 +0800 Subject: [PATCH 006/173] style(rails): remove high voltage pages --- Gemfile | 2 -- Gemfile.lock | 2 -- config/initializers/high_voltage.rb | 5 ----- lib/extensions/high_voltage_page_action_class.rb | 2 -- .../high_voltage_page_action_class/action_view.rb | 2 -- .../action_view/base.rb | 12 ------------ spec/libraries/high_voltage_pages_spec.rb | 15 --------------- 7 files changed, 40 deletions(-) delete mode 100644 config/initializers/high_voltage.rb delete mode 100644 lib/extensions/high_voltage_page_action_class.rb delete mode 100644 lib/extensions/high_voltage_page_action_class/action_view.rb delete mode 100644 lib/extensions/high_voltage_page_action_class/action_view/base.rb delete mode 100644 spec/libraries/high_voltage_pages_spec.rb diff --git a/Gemfile b/Gemfile index 798921b63e4..0a3906d76b6 100644 --- a/Gemfile +++ b/Gemfile @@ -62,8 +62,6 @@ gem 'jbuilder' gem 'slim-rails' # ejs for client-side templates gem 'ejs' -# High Voltage for static pages -gem 'high_voltage' # Paginator for Rails gem 'kaminari' # Work with Docker diff --git a/Gemfile.lock b/Gemfile.lock index 6f40b03bb9c..91f6eec2fbc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -264,7 +264,6 @@ GEM raabro (~> 1.4) globalid (1.1.0) activesupport (>= 5.0) - high_voltage (3.1.2) highline (2.0.3) html-pipeline (2.14.3) activesupport (>= 2) @@ -671,7 +670,6 @@ DEPENDENCIES fog-aws (= 3.8.0) font-awesome-rails friendly_id - high_voltage html-pipeline html-pipeline-rouge_filter! http_accept_language diff --git a/config/initializers/high_voltage.rb b/config/initializers/high_voltage.rb deleted file mode 100644 index 2e14e111ac8..00000000000 --- a/config/initializers/high_voltage.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true -HighVoltage.configure do |config| - config.home_page = 'home' - config.layout = nil -end diff --git a/lib/extensions/high_voltage_page_action_class.rb b/lib/extensions/high_voltage_page_action_class.rb deleted file mode 100644 index acb1a65f74f..00000000000 --- a/lib/extensions/high_voltage_page_action_class.rb +++ /dev/null @@ -1,2 +0,0 @@ -# frozen_string_literal: true -module Extensions::HighVoltagePageActionClass; end diff --git a/lib/extensions/high_voltage_page_action_class/action_view.rb b/lib/extensions/high_voltage_page_action_class/action_view.rb deleted file mode 100644 index 87c5f12b660..00000000000 --- a/lib/extensions/high_voltage_page_action_class/action_view.rb +++ /dev/null @@ -1,2 +0,0 @@ -# frozen_string_literal: true -module Extensions::HighVoltagePageActionClass::ActionView; end diff --git a/lib/extensions/high_voltage_page_action_class/action_view/base.rb b/lib/extensions/high_voltage_page_action_class/action_view/base.rb deleted file mode 100644 index 2a3f06d017d..00000000000 --- a/lib/extensions/high_voltage_page_action_class/action_view/base.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true -module Extensions::HighVoltagePageActionClass::ActionView::Base - def page_action_class - if controller.is_a?(HighVoltage::PagesController) - # TODO: Depends on thoughtbot/high_voltage#235 - current_page = controller.send(:current_page) - current_page.sub(/^pages\//, '') - else - super - end - end -end diff --git a/spec/libraries/high_voltage_pages_spec.rb b/spec/libraries/high_voltage_pages_spec.rb deleted file mode 100644 index 60fa9c120bc..00000000000 --- a/spec/libraries/high_voltage_pages_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.describe 'Extension: High Voltage Page Action Class', type: :controller do - controller(HighVoltage::PagesController) do - def action_has_layout? - false - end - end - - it 'gets the correct action class' do - get :show, params: { id: 'home' } - expect(controller.view_context.page_action_class).to eq('home') - end -end From 1863859fe4e9bb9d72e031ddc540718cf7b94d19 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:36:36 +0800 Subject: [PATCH 007/173] style(rails): remove simple_form --- Gemfile | 8 --- Gemfile.lock | 23 -------- app/helpers/application_helper.rb | 2 - app/inputs/array_input.rb | 21 ------- app/inputs/code_input.rb | 10 ---- config/initializers/simple_form.rb | 4 -- lib/templates/slim/scaffold/_form.html.slim | 10 ---- .../helpers/application_cocoon_helper_spec.rb | 55 ------------------- spec/libraries/form_for_resource_spec.rb | 38 ------------- 9 files changed, 171 deletions(-) delete mode 100644 app/inputs/array_input.rb delete mode 100644 app/inputs/code_input.rb delete mode 100644 config/initializers/simple_form.rb delete mode 100644 lib/templates/slim/scaffold/_form.html.slim delete mode 100644 spec/helpers/application_cocoon_helper_spec.rb delete mode 100644 spec/libraries/form_for_resource_spec.rb diff --git a/Gemfile b/Gemfile index 0a3906d76b6..f6813394a24 100644 --- a/Gemfile +++ b/Gemfile @@ -194,14 +194,6 @@ gem 'cancancan' # We also want stricter sanitization. gem 'rails_utils', git: 'https://github.com/raymondtangsc/rails_utils.git', branch: 'full-sanitize-flash' -# Forms made easy for Rails -gem 'simple_form' -gem 'simple_form-bootstrap', git: 'https://github.com/purfectliterature/simple_form-bootstrap' -# Dynamic nested forms -gem 'cocoon' -gem 'bootstrap_tokenfield_rails' -gem 'twitter-typeahead-rails' - # Using CarrierWave for file uploads gem 'carrierwave' # Generate sequential filenames diff --git a/Gemfile.lock b/Gemfile.lock index 91f6eec2fbc..371014da851 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,17 +36,6 @@ GIT specs: rwordnet (2.0.0) -GIT - remote: https://github.com/purfectliterature/simple_form-bootstrap - revision: a7b5759e3a569a65d0e2b575c9ae38a7d5c9e5fc - specs: - simple_form-bootstrap (1.6.0) - actionpack (>= 4.1) - activemodel (>= 4.1) - bootstrap-sass (~> 3) - railties (>= 4.1) - simple_form (>= 3.1.0) - GIT remote: https://github.com/raymondtangsc/rails_utils.git revision: 5c8c0caacf08985ae14c5d007529a09d5f284da0 @@ -554,9 +543,6 @@ GEM fugit (~> 1.8) globalid (>= 1.0.1) sidekiq (>= 6) - simple_form (5.1.0) - actionpack (>= 5.2) - activemodel (>= 5.2) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -595,10 +581,6 @@ GEM timeout (0.4.0) traceroute (0.8.1) rails (>= 3.0.0) - twitter-typeahead-rails (0.11.1) - actionpack (>= 3.1) - jquery-rails - railties (>= 3.1) tzinfo (1.2.11) thread_safe (~> 0.1) uglifier (4.2.0) @@ -652,7 +634,6 @@ DEPENDENCIES capybara-screenshot capybara-selenium carrierwave - cocoon codecov consistency_fail coursemology-polyglot! @@ -716,16 +697,12 @@ DEPENDENCIES shoulda-matchers sidekiq sidekiq-cron - simple_form - simple_form-bootstrap! simplecov sinatra slim-rails spring - sprockets (< 4.0.0) stackprof traceroute - twitter-typeahead-rails tzinfo-data uglifier (>= 1.3.0) unread diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 809538c63cb..88a78d2ec26 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -6,12 +6,10 @@ module ApplicationHelper include ApplicationAnnouncementsHelper include ApplicationJobsHelper include ApplicationWidgetsHelper - include ApplicationCocoonHelper include ApplicationNotificationsHelper include ApplicationFormattersHelper include RouteOverridesHelper - include FormForWithResourceHelper include RenderWithinLayoutHelper # Accesses the header tags specified for the current page diff --git a/app/inputs/array_input.rb b/app/inputs/array_input.rb deleted file mode 100644 index 364ed6c7548..00000000000 --- a/app/inputs/array_input.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true -# https://railsguides.net/simple-form-array-text-input/ -# https://tenforward.consulting/blog/integrating-an-array-column-in-rails-with-simple-form -class ArrayInput < SimpleForm::Inputs::StringInput - def input(_wrapper_options) - input_html_options[:type] ||= input_type - existing_values = Array(object.public_send(attribute_name)).map do |array_el| - @builder.text_field(nil, input_html_options.merge(value: array_el, name: "#{object_name}[#{attribute_name}][]")) - end - if existing_values.empty? - existing_values.push @builder.text_field(nil, - input_html_options.merge(value: nil, - name: "#{object_name}[#{attribute_name}][]")) - end - existing_values.join.html_safe - end - - def input_type - :text - end -end diff --git a/app/inputs/code_input.rb b/app/inputs/code_input.rb deleted file mode 100644 index 74fa4f07968..00000000000 --- a/app/inputs/code_input.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true -class CodeInput < SimpleForm::Inputs::TextInput - private - - def html_options_for(namespace, css_classes) - return super unless namespace == :input - - super.merge(lang: options[:language]) - end -end diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb deleted file mode 100644 index fe7c69c6ed8..00000000000 --- a/config/initializers/simple_form.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true -# Use this setup block to configure all options available in SimpleForm. -SimpleForm.setup do |_| -end diff --git a/lib/templates/slim/scaffold/_form.html.slim b/lib/templates/slim/scaffold/_form.html.slim deleted file mode 100644 index a2ff775acb9..00000000000 --- a/lib/templates/slim/scaffold/_form.html.slim +++ /dev/null @@ -1,10 +0,0 @@ -= simple_form_for(@<%= singular_table_name %>) do |f| - = f.error_notification - - .form-inputs -<%- attributes.each do |attribute| -%> - = f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> -<%- end -%> - - .form-actions - = f.button :submit diff --git a/spec/helpers/application_cocoon_helper_spec.rb b/spec/helpers/application_cocoon_helper_spec.rb deleted file mode 100644 index 011036766d6..00000000000 --- a/spec/helpers/application_cocoon_helper_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.describe ApplicationCocoonHelper, type: :helper do - describe '#link_to_add_association' do - let(:course_group) { Course::Group.new } - let(:form_object) { double(object: course_group, object_name: course_group.class.name) } - let(:html_options) do - { - find_using: 'next', - selector: 'tbody', - insert_using: 'append' - } - end - - let(:expected_options) do - { - 'data-association-insertion-traversal' => 'next', - 'data-association-insertion-node' => 'tbody', - 'data-association-insertion-method' => 'append' - } - end - - before do - allow(view).to receive(:render_association).and_return('form') - end - - context 'when block it not given' do - let(:args) { ['Add User', form_object, :group_users] } - subject { view.link_to_add_association(*args, html_options) } - - it 'generates the correct options and the name' do - expect(subject).to have_tag('a', with: expected_options) do - with_text 'Add User' - end - end - end - - context 'when a block is given' do - let(:block_args) { [form_object, :group_users] } - - subject do - view.link_to_add_association(*block_args, html_options) do - 'New Name' - end - end - - it 'generates the correct options and the name' do - expect(subject).to have_tag('a', with: expected_options) do - with_text 'New Name' - end - end - end - end -end diff --git a/spec/libraries/form_for_resource_spec.rb b/spec/libraries/form_for_resource_spec.rb deleted file mode 100644 index c99c208892a..00000000000 --- a/spec/libraries/form_for_resource_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.describe 'Extension: form_for with resource', type: :view do - let(:instance) { Instance.default } - with_tenant(:instance) do - it 'does not allow :url to be used with :resource' do - expect do - form_for(build(:course), resource: :course, url: courses_path) {} - end.to raise_error(ArgumentError) - end - - it 'requires :resource to be a symbol' do - expect do - form_for(build(:course), resource: 'course') {} - end.to raise_error(ArgumentError) - end - - it 'automatically adds the `path` suffix for route helpers' do - expect(form_for(build(:course), resource: :course) {}).to have_form(courses_path, :post) - end - - context 'when the resource is new' do - subject { form_for(build(:course), resource: :course_path) {} } - it 'generates the plural route' do - expect(subject).to have_form(courses_path, :post) - end - end - - context 'when the resource is persisted' do - let(:course) { create(:course) } - subject { form_for(course, resource: :course_path) {} } - it 'generates the singular route' do - expect(subject).to have_form(course_path(course), :post) - end - end - end -end From b5748653e4d78a1e841e63c34542321bf33f63d8 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:39:13 +0800 Subject: [PATCH 008/173] style(rails): remove ApplicationWidgetsHelper --- app/helpers/application_helper.rb | 1 - app/helpers/application_widgets_helper.rb | 174 ----------------- .../application_widgets_helper_spec.rb | 180 ------------------ 3 files changed, 355 deletions(-) delete mode 100644 app/helpers/application_widgets_helper.rb delete mode 100644 spec/helpers/application_widgets_helper_spec.rb diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 88a78d2ec26..4fb52c73045 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -5,7 +5,6 @@ module ApplicationHelper include ApplicationAnnouncementsHelper include ApplicationJobsHelper - include ApplicationWidgetsHelper include ApplicationNotificationsHelper include ApplicationFormattersHelper diff --git a/app/helpers/application_widgets_helper.rb b/app/helpers/application_widgets_helper.rb deleted file mode 100644 index 0de91c03bab..00000000000 --- a/app/helpers/application_widgets_helper.rb +++ /dev/null @@ -1,174 +0,0 @@ -# frozen_string_literal: true -module ApplicationWidgetsHelper - # Create a +edit+ button. - # - # @return [String] The HTML for the button. - # @overload edit_button(path, html_options = nil, &block) - # Creates a +edit+ button, pointing to the given path and HTML options. This would yield a - # button with an icon, unless a block is provided. - # @param [String] path The path to link to. - # @param [Hash] html_options The HTML options for the button. - # @param [Proc] block The block to use for displaying the link. - # @overload edit_button(resource, html_options = nil, &block) - # Creates a +edit+ button, pointing to the given resource. The URL is resolved using +url_for+. - # This would yield a button with an icon, unless a block is provided. - # @param [Array|Object] path The resource to link to. - # @param [Hash] html_options The HTML options for the button. - # @param [Proc] block The block to use for displaying the link. - # @overload edit_button(body, path, html_options = nil) - # Creates a +edit+ button, pointing to the given path and HTML options. This would create a - # button with the given body. - # @overload edit_button(body, resource, html_options = nil) - # Creates a +edit+ button, pointing to the given resource. The URL is resolved using +url_for+. - # This would create a button with the given body. - def edit_button(name, options = nil, html_options = nil, &block) - name, options, html_options = [nil, name, options] unless html_options - options = [:edit] + Array(options) unless options.is_a?(String) - block ||= proc { fa_icon 'edit' } - resource_button(:edit, 'btn-default', name || block, options, html_options&.dup) - end - - # Create a +delete+ button. - # - # @return [String] The HTML for the button. - # @overload delete_button(path, html_options = nil, &block) - # Creates a +delete+ button, pointing to the given path and HTML options. This would yield a - # button with an icon, unless a block is provided. - # @param [String] path The path to link to. - # @param [Hash] html_options The HTML options for the button. - # @param [Proc] block The block to use for displaying the link. - # @overload delete_button(resource, html_options = nil, &block) - # Creates a +delete+ button, pointing to the given resource. The URL is resolved using - # +url_for+. This would yield a button with an icon, unless a block is provided. - # @param [Array|Object] path The resource to link to. - # @param [Hash] html_options The HTML options for the button. - # @param [Proc] block The block to use for displaying the link. - # @overload delete_button(body, path, html_options = nil) - # Creates a +delete+ button, pointing to the given path and HTML options. This would create a - # button with the given body. - # @overload delete_button(body, resource, html_options = nil) - # Creates a +delete+ button, pointing to the given resource. The URL is resolved using - # +url_for+. This would create a button with the given body. - def delete_button(name, options = nil, html_options = nil, &block) - name, options, html_options = [nil, name, options] unless html_options - block ||= proc { fa_icon 'trash' } - - html_options = html_options&.dup || {} - html_options.reverse_merge!(method: :delete, - data: { confirm: t('helpers.buttons.delete_confirm_message') }) - resource_button(:delete, 'btn-danger', name || block, options, html_options) - end - - # Display a progress_bar with the given percentage and styling. The percentage is assumed to - # be a number ranging from 0-100. In addition, a block can be passed to add custom text. - # - # ActionView::Helpers::CaptureHelper#capture is used to ensure the block is rendered in the - # original view_context, rather than within the view_context of the progress bar layout. - # - # @param [Integer] percentage The percentage to be displayed on the progress bar. - # @param [Hash] opts Options to apply on the progress bar. Supports the following: - # class: css classes of progress bar (defaults to `progress-bar-info`), - # tooltip_text: text to be included in tooltip, - # tooltip_placement: 'left', 'top', 'bottom', or 'right'. - # @yield The HTML text which will be passed to the partial as text to be shown in the bar. - # @return [String] HTML string to render the progress bar. - def display_progress_bar(percentage, opts = {}, &block) - opts[:class] = ['progress-bar-info'] unless opts[:class] - text_in_block = capture(&block) if block_given? - render partial: 'layouts/progress_bar', - locals: { percentage: percentage, opts: opts, progress_bar_text: text_in_block } - end - - private - - # Creates a button for creating, editing, or deleting resources. - # - # @param [Symbol] key The key of the button. This can be +:new+, +:edit+, or +:delete+, and is - # used to look up an appropriate translation. - # @param [String] default_class The default CSS class to be applied to the button. - # @param [String|Proc] body The string to use as the body of the button, or a block which would - # be evaluated to give the body of the button. - # @param [Hash|nil] url_options The options to pass to +url_for+. - # @param [Hash|nil] html_options A hash of mutable options to pass to +link_to+. - def resource_button(key, default_class, body, url_options, html_options) - html_options ||= {} - html_options[:class] = deduce_resource_button_class(key, html_options[:class], default_class) - if !url_options.nil? && !url_options.is_a?(String) - html_options[:title] ||= deduce_resource_button_title(key, url_options) - end - - if body.is_a?(Proc) - link_to(url_options, html_options, &body) - else - link_to(body, url_options, html_options) - end - end - - # Deduce the CSS classes to be applied to the button from the user-specified classes and default - # class. - # - # @param [String|Symbol] key The key of the button, as passed to +resource_button+. - # @param [Array] custom_classes The CSS classes specified by the user. - # @param [String] default_type The default class to use if there is no explicit button class. - # @return [Array] The deduced set of CSS classes to apply. - def deduce_resource_button_class(key, custom_classes, default_type) - custom_classes = Set[*custom_classes] - custom_classes |= ['btn'].freeze - custom_classes << default_type unless resource_button_type_specified?(custom_classes) - custom_classes << key - custom_classes.to_a - end - - # Checks whether the given CSS classes have an explicit button type specified. - # - # @param [Set] css_classes The list of CSS classes specified. - # @return [Boolean] +true+ if the button type is specified. - def resource_button_type_specified?(css_classes) - available_button_types = Set['btn-default', 'btn-primary', 'btn-success', - 'btn-info', 'btn-warning', 'btn-danger'].freeze - css_classes.intersect?(available_button_types) - end - - # Deduces the title to be given to the button given the button type and the URL arguments. - # - # @param [Symbol] button_type The type of the button to generate a title for. - # @param [Symbol|Array|ActiveRecord::Base] resource The resource to deduce the title for. - def deduce_resource_button_title(button_type, resource) - resource = deduce_resource_button_resource(resource) - object_name = deduce_resource_object_name(resource) - resource_name = resource.try(:model_name).try(:human) || object_name.humanize - - keys = [] - keys << :"helpers.buttons.#{object_name}.#{button_type}" if object_name - keys << :"helpers.buttons.#{button_type}" - keys << "#{button_type.to_s.humanize} #{resource_name}" if resource_name - t(keys.shift, model: resource_name, default: keys) - end - - # Given a parameter set for +url_for+, deduce the resource that the parameter references. - # - # @param [Array|String] resource The resource to deduce. This handles arrays, which are - # interpreted as +url_for+ parameters. This also supports hash options to be provided to - # +url_for+. - # @return [Symbol] When an array is given. - # @return [String] When a string is given. - def deduce_resource_button_resource(resource) - return resource unless resource.is_a?(Array) - return resource[-2] if resource.last.is_a?(Hash) - - resource.last - end - - # Deduces the object name of the resource. This is the name used to construct the translation - # string and is also used as a name for sending form data. - # - # @param [Symbol|ActiveRecord::Base] resource The resource to deduce the title for. - # @return [String] The object name of the resource. - def deduce_resource_object_name(resource) - if resource.is_a?(Symbol) - resource.to_s - else - model_name_from_record_or_class(resource).param_key - end - end -end diff --git a/spec/helpers/application_widgets_helper_spec.rb b/spec/helpers/application_widgets_helper_spec.rb deleted file mode 100644 index aa26d370257..00000000000 --- a/spec/helpers/application_widgets_helper_spec.rb +++ /dev/null @@ -1,180 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.describe ApplicationWidgetsHelper, type: :helper do - def stub_resource_button - helper.define_singleton_method(:resource_button) do |_, _, _, url_options, _| - url_options - end - end - - let(:instance) { Instance.default } - with_tenant(:instance) do - describe '#delete_button' do - let(:announcement) { create(:course_announcement) } - subject { helper.delete_button([announcement.course, announcement]) } - it 'defaults to a btn-primary class' do - expect(subject).to have_tag('a.btn.btn-danger') - end - it 'defaults to a file button' do - expect(subject).to have_tag('i.fa-trash') - end - it 'sets the method as delete' do - expect(subject).to have_tag('a', with: { 'data-method' => 'delete' }) - end - end - - describe '#resource_button' do - let(:announcement) { create(:course_announcement) } - before { I18n.backend.store_translations(:en, en: { helpers: { buttons: { new: 'new' } } }) } - after { I18n.backend.store_translations(:en, en: { helpers: { buttons: { new: nil } } }) } - - let(:body) { 'meh' } - subject do - helper.send(:resource_button, :new, 'btn-warning', body, - [announcement.course, announcement], nil) - end - - it 'uses the key to determine the translation' do - expect(subject).to have_tag('a.btn.btn-warning', text: body) - end - - it 'adds the key to the button classes' do - expect(subject).to have_tag('a.btn.new', text: body) - end - - context 'when a block is provided to the body argument' do - let(:text) { 'block!' } - let(:body) { proc { text } } - it 'calls the block to provide the body of the link' do - expect(subject).to have_tag('a.btn.btn-warning', text: text) - end - end - - context 'when a resource is provided to the url_options argument' do - it 'gives the title to the link' do - title = 'helpers.buttons.announcement.new' - expect(subject).to have_tag('a', with: { title: title }) - end - end - end - - describe '#deduce_resource_button_class' do - let(:specified_classes) { [] } - let(:default_class) { 'ignore' } - let(:key) { :new } - subject { helper.send(:deduce_resource_button_class, key, specified_classes, default_class) } - - it 'adds the btn class' do - expect(subject).to include('btn') - end - - it 'adds the key' do - expect(subject).to include(key) - end - - context 'when a button type is specified' do - let(:specified_classes) { ['btn-default'] } - it 'does not add the specified default class' do - expect(subject).not_to include(default_class) - end - end - - context 'when no button type is specified' do - it 'adds the specified default class' do - expect(subject).to include('ignore') - end - end - end - - describe '#deduce_resource_button_title' do - before { helper.define_singleton_method(:t) { |key, hash| [key] + hash[:default] } } - let(:announcement) { build(:course_announcement) } - subject { helper.send(:deduce_resource_button_title, :edit, url_options) } - - context 'when given an array of resources' do - let(:url_options) { [announcement] } - it 'picks the last resource' do - expect(subject).to contain_exactly( - :'helpers.buttons.announcement.edit', - :'helpers.buttons.edit', - 'Edit Announcement' - ) - end - - context 'when given an array with an options hash' do - let(:url_options) { [announcement, test: 'something'] } - it 'picks the last resource' do - expect(subject).to contain_exactly( - :'helpers.buttons.announcement.edit', - :'helpers.buttons.edit', - 'Edit Announcement' - ) - end - end - end - - context 'when given a single resource' do - let(:url_options) { announcement } - it 'looks up the model name' do - expect(subject).to contain_exactly( - :'helpers.buttons.announcement.edit', - :'helpers.buttons.edit', - 'Edit Announcement' - ) - end - end - - context 'when given a symbol' do - let(:url_options) { :announcement } - it 'guesses the human name of the symbol' do - expect(subject).to contain_exactly( - :'helpers.buttons.announcement.edit', - :'helpers.buttons.edit', - 'Edit Announcement' - ) - end - end - end - - describe '#display_progress_bar' do - let(:default_class) { 'progress-bar-info' } - subject { helper.display_progress_bar(50) } - - it 'returns a progress bar' do - expect(subject).to have_tag('div.progress-bar', with: { role: 'progressbar' }) - end - - it 'specifies the correct percentage of the progress bar' do - expect(subject).to have_tag('div.progress-bar', style: 'width: 50%') - end - - it 'defaults to .progress-bar-info' do - expect(subject).to include(default_class) - end - - context 'when opts are specified' do - let(:tooltip_title) { 'Foo' } - let(:opts) { { class: ['progress-bar-striped'], title: tooltip_title } } - subject { helper.display_progress_bar(50, opts) } - - it 'is reflected in the progress bar' do - expect(subject).to have_tag('div.progress-bar.progress-bar-striped') - expect(subject).to have_tag('div.progress-bar', title: tooltip_title) - end - end - - context 'when a block is given' do - it 'appends the text within the progress bar' do - expect(helper.display_progress_bar(50) { '30%' }).to include('30%') - end - - it 'renders the block in the context of the helper' do - message = 'foo' - helper.define_singleton_method(:some_method) { message } - expect(helper.display_progress_bar(50) { helper.some_method }).to include(message) - end - end - end - end -end From c5047d370de20f1b68d667177583ef75b7aa5c5f Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:41:43 +0800 Subject: [PATCH 009/173] style(rails): remove unused slims --- .../course/component_controller.rb | 2 -- app/views/announcements/index.html.slim | 1 - .../achievement/achievements/index.html.slim | 1 - app/views/course/admin/index.html.slim | 1 - .../course/announcements/index.html.slim | 1 - .../assessment/assessments/edit.html.slim | 1 - .../assessment/assessments/index.html.slim | 1 - .../assessments/monitoring.html.slim | 1 - .../assessment/assessments/show.html.slim | 4 --- .../assessments/statistics.html.slim | 1 - .../forum_post_responses/edit.html.slim | 1 - .../forum_post_responses/new.html.slim | 1 - .../multiple_responses/edit.html.slim | 1 - .../question/multiple_responses/new.html.slim | 1 - .../question/programming/edit.html.slim | 1 - .../question/programming/new.html.slim | 1 - .../question/scribing/edit.html.slim | 1 - .../question/scribing/new.html.slim | 1 - .../_comprehension_group_fields.html.slim | 23 ------------- .../_comprehension_point_fields.html.slim | 32 ------------------- .../_comprehension_solution_fields.html.slim | 20 ------------ .../_form_comprehension.html.slim | 29 ----------------- .../question/text_responses/edit.html.slim | 1 - .../question/text_responses/new.html.slim | 1 - .../question/voice_responses/edit.html.slim | 1 - .../question/voice_responses/new.html.slim | 1 - .../edit.html.slim | 2 +- .../index.html.slim | 2 +- .../question_bundle_questions/edit.html.slim | 2 +- .../question_bundle_questions/index.html.slim | 2 +- .../question_bundle_questions/new.html.slim | 2 +- .../question_bundles/edit.html.slim | 2 +- .../question_bundles/index.html.slim | 2 +- .../assessment/question_bundles/new.html.slim | 2 +- .../assessment/question_groups/edit.html.slim | 2 +- .../question_groups/index.html.slim | 2 +- .../assessment/question_groups/new.html.slim | 2 +- .../course/assessment/sessions/new.html.slim | 1 - .../course/assessment/skills/index.html.slim | 1 - .../submission/logs/index.html.slim | 1 - .../submission/submissions/edit.html.slim | 1 - .../submission/submissions/index.html.slim | 1 - .../assessment/submissions/index.html.slim | 1 - app/views/course/courses/index.html.slim | 1 - app/views/course/courses/show.html.slim | 1 - .../course/discussion/topics/index.html.slim | 1 - app/views/course/duplications/show.html.slim | 1 - .../course/enrol_requests/index.html.slim | 1 - .../disbursement/new.html.slim | 1 - .../experience_points_records/index.html.slim | 1 - app/views/course/forum/forums/index.html.slim | 1 - .../group/group_categories/index.html.slim | 1 - .../group/group_categories/show.html.slim | 1 - app/views/course/leaderboards/index.html.slim | 1 - app/views/course/learning_map/index.html.slim | 3 -- .../items/_personal_or_ref_time.html.slim | 11 ------- .../course/lesson_plan/items/index.html.slim | 1 - app/views/course/levels/index.html.slim | 1 - .../course/material/folders/show.html.slim | 1 - .../course/personal_times/index.html.slim | 1 - .../reference_timelines/index.html.slim | 1 - app/views/course/statistics/index.html.slim | 1 - .../course/survey/surveys/index.html.slim | 1 - .../user_email_subscriptions/edit.html.slim | 1 - .../course/user_invitations/index.html.slim | 1 - .../course/user_invitations/new.html.slim | 1 - .../user_registrations/create.html.slim | 1 - app/views/course/users/index.html.slim | 1 - app/views/course/users/staff.html.slim | 1 - app/views/course/users/students.html.slim | 1 - .../submission/submissions/index.html.slim | 1 - app/views/course/video/videos/index.html.slim | 1 - .../course/video_submissions/index.html.slim | 1 - app/views/devise/confirmations/new.html.slim | 13 -------- app/views/devise/passwords/new.html.slim | 12 ------- app/views/layouts/_progress_bar.html.slim | 6 ---- app/views/layouts/_user.html.slim | 4 --- app/views/layouts/course.html.slim | 3 -- app/views/pages/403.html.slim | 3 -- app/views/system/admin/admin/index.html.slim | 1 - .../admin/instance/admin/index.html.slim | 1 - app/views/user/profiles/edit.html.slim | 1 - app/views/user/registrations/new.html.slim | 26 --------------- app/views/user/sessions/new.html.slim | 12 ------- app/views/users/show.html.slim | 1 - 85 files changed, 11 insertions(+), 272 deletions(-) delete mode 100644 app/views/announcements/index.html.slim delete mode 100644 app/views/course/achievement/achievements/index.html.slim delete mode 100644 app/views/course/admin/index.html.slim delete mode 100644 app/views/course/announcements/index.html.slim delete mode 100644 app/views/course/assessment/assessments/edit.html.slim delete mode 100644 app/views/course/assessment/assessments/index.html.slim delete mode 100644 app/views/course/assessment/assessments/monitoring.html.slim delete mode 100644 app/views/course/assessment/assessments/show.html.slim delete mode 100644 app/views/course/assessment/assessments/statistics.html.slim delete mode 100644 app/views/course/assessment/question/forum_post_responses/edit.html.slim delete mode 100644 app/views/course/assessment/question/forum_post_responses/new.html.slim delete mode 100644 app/views/course/assessment/question/multiple_responses/edit.html.slim delete mode 100644 app/views/course/assessment/question/multiple_responses/new.html.slim delete mode 100644 app/views/course/assessment/question/programming/edit.html.slim delete mode 100644 app/views/course/assessment/question/programming/new.html.slim delete mode 100644 app/views/course/assessment/question/scribing/edit.html.slim delete mode 100644 app/views/course/assessment/question/scribing/new.html.slim delete mode 100644 app/views/course/assessment/question/text_responses/_comprehension_group_fields.html.slim delete mode 100644 app/views/course/assessment/question/text_responses/_comprehension_point_fields.html.slim delete mode 100644 app/views/course/assessment/question/text_responses/_comprehension_solution_fields.html.slim delete mode 100644 app/views/course/assessment/question/text_responses/_form_comprehension.html.slim delete mode 100644 app/views/course/assessment/question/text_responses/edit.html.slim delete mode 100644 app/views/course/assessment/question/text_responses/new.html.slim delete mode 100644 app/views/course/assessment/question/voice_responses/edit.html.slim delete mode 100644 app/views/course/assessment/question/voice_responses/new.html.slim delete mode 100644 app/views/course/assessment/sessions/new.html.slim delete mode 100644 app/views/course/assessment/skills/index.html.slim delete mode 100644 app/views/course/assessment/submission/logs/index.html.slim delete mode 100644 app/views/course/assessment/submission/submissions/edit.html.slim delete mode 100644 app/views/course/assessment/submission/submissions/index.html.slim delete mode 100644 app/views/course/assessment/submissions/index.html.slim delete mode 100644 app/views/course/courses/index.html.slim delete mode 100644 app/views/course/courses/show.html.slim delete mode 100644 app/views/course/discussion/topics/index.html.slim delete mode 100644 app/views/course/duplications/show.html.slim delete mode 100644 app/views/course/enrol_requests/index.html.slim delete mode 100644 app/views/course/experience_points/disbursement/new.html.slim delete mode 100644 app/views/course/experience_points_records/index.html.slim delete mode 100644 app/views/course/forum/forums/index.html.slim delete mode 100644 app/views/course/group/group_categories/index.html.slim delete mode 100644 app/views/course/group/group_categories/show.html.slim delete mode 100644 app/views/course/leaderboards/index.html.slim delete mode 100644 app/views/course/learning_map/index.html.slim delete mode 100644 app/views/course/lesson_plan/items/_personal_or_ref_time.html.slim delete mode 100644 app/views/course/lesson_plan/items/index.html.slim delete mode 100644 app/views/course/levels/index.html.slim delete mode 100644 app/views/course/material/folders/show.html.slim delete mode 100644 app/views/course/personal_times/index.html.slim delete mode 100644 app/views/course/reference_timelines/index.html.slim delete mode 100644 app/views/course/statistics/index.html.slim delete mode 100644 app/views/course/survey/surveys/index.html.slim delete mode 100644 app/views/course/user_email_subscriptions/edit.html.slim delete mode 100644 app/views/course/user_invitations/index.html.slim delete mode 100644 app/views/course/user_invitations/new.html.slim delete mode 100644 app/views/course/user_registrations/create.html.slim delete mode 100644 app/views/course/users/index.html.slim delete mode 100644 app/views/course/users/staff.html.slim delete mode 100644 app/views/course/users/students.html.slim delete mode 100644 app/views/course/video/submission/submissions/index.html.slim delete mode 100644 app/views/course/video/videos/index.html.slim delete mode 100644 app/views/course/video_submissions/index.html.slim delete mode 100644 app/views/devise/confirmations/new.html.slim delete mode 100644 app/views/devise/passwords/new.html.slim delete mode 100644 app/views/layouts/_progress_bar.html.slim delete mode 100644 app/views/layouts/_user.html.slim delete mode 100644 app/views/layouts/course.html.slim delete mode 100644 app/views/pages/403.html.slim delete mode 100644 app/views/system/admin/admin/index.html.slim delete mode 100644 app/views/system/admin/instance/admin/index.html.slim delete mode 100644 app/views/user/profiles/edit.html.slim delete mode 100644 app/views/user/registrations/new.html.slim delete mode 100644 app/views/user/sessions/new.html.slim delete mode 100644 app/views/users/show.html.slim diff --git a/app/controllers/course/component_controller.rb b/app/controllers/course/component_controller.rb index 97186790af5..df78741d1da 100644 --- a/app/controllers/course/component_controller.rb +++ b/app/controllers/course/component_controller.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class Course::ComponentController < Course::Controller - layout 'course' - before_action :load_current_component_host before_action :check_component before_action :load_settings diff --git a/app/views/announcements/index.html.slim b/app/views/announcements/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/announcements/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/achievement/achievements/index.html.slim b/app/views/course/achievement/achievements/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/achievement/achievements/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/admin/index.html.slim b/app/views/course/admin/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/admin/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/announcements/index.html.slim b/app/views/course/announcements/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/announcements/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/assessments/edit.html.slim b/app/views/course/assessment/assessments/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/assessments/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/assessments/index.html.slim b/app/views/course/assessment/assessments/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/assessments/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/assessments/monitoring.html.slim b/app/views/course/assessment/assessments/monitoring.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/assessments/monitoring.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/assessments/show.html.slim b/app/views/course/assessment/assessments/show.html.slim deleted file mode 100644 index 72c4657f683..00000000000 --- a/app/views/course/assessment/assessments/show.html.slim +++ /dev/null @@ -1,4 +0,0 @@ -/ Randomized Assessment is temporarily hidden (PR#5406) -/ - if @assessment.randomization.present? -/ = render 'assessment_question_bundle_buttons', assessment: @assessment -div#app-root diff --git a/app/views/course/assessment/assessments/statistics.html.slim b/app/views/course/assessment/assessments/statistics.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/assessments/statistics.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/forum_post_responses/edit.html.slim b/app/views/course/assessment/question/forum_post_responses/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/forum_post_responses/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/forum_post_responses/new.html.slim b/app/views/course/assessment/question/forum_post_responses/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/forum_post_responses/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/multiple_responses/edit.html.slim b/app/views/course/assessment/question/multiple_responses/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/multiple_responses/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/multiple_responses/new.html.slim b/app/views/course/assessment/question/multiple_responses/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/multiple_responses/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/programming/edit.html.slim b/app/views/course/assessment/question/programming/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/programming/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/programming/new.html.slim b/app/views/course/assessment/question/programming/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/programming/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/scribing/edit.html.slim b/app/views/course/assessment/question/scribing/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/scribing/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/scribing/new.html.slim b/app/views/course/assessment/question/scribing/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/scribing/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/text_responses/_comprehension_group_fields.html.slim b/app/views/course/assessment/question/text_responses/_comprehension_group_fields.html.slim deleted file mode 100644 index 749e9ad852c..00000000000 --- a/app/views/course/assessment/question/text_responses/_comprehension_group_fields.html.slim +++ /dev/null @@ -1,23 +0,0 @@ -= content_tag_for(:tr, f.object, class: 'nested-fields') do - td - b = t('.group') - br - = link_to_remove_association t('.remove_group'), f - br - br - = f.input :maximum_group_grade, label: t('.maximum_group_grade'), - input_html: { class: ['text-response-group-maximum-group-grade'] } - - - group_id = f.object_name - - table.table.table-hover - thead - tr - th = link_to_add_association t('.add_point'), f, :points, - partial: 'comprehension_point_fields', - find_selector: 'tbody.tbody-groups.'+group_id, - insert_using: 'append' - / group_id must not be the last class so that it will be correctly substituted by cocoon - tbody class=[group_id, 'tbody-groups'] - = f.simple_fields_for :points do |comprehension_points_form| - = render 'comprehension_point_fields', f: comprehension_points_form diff --git a/app/views/course/assessment/question/text_responses/_comprehension_point_fields.html.slim b/app/views/course/assessment/question/text_responses/_comprehension_point_fields.html.slim deleted file mode 100644 index c0adec30cf0..00000000000 --- a/app/views/course/assessment/question/text_responses/_comprehension_point_fields.html.slim +++ /dev/null @@ -1,32 +0,0 @@ -= content_tag_for(:tr, f.object.group, class: 'nested-fields') do - td - b = t('.point') - br - = link_to_remove_association t('.remove_point'), f - br - br - = f.input :point_grade, label: t('.point_grade'), - input_html: { class: ['text-response-group-point-grade'] } - - - point_id = f.object_name - - .has-error - = f.error :solutions - - table.table.table-striped.table-hover.table-points - thead - tr - th = t('.solution_type') - th = t('.solution') - th - th = t('.information') - th - div.pull-right - = link_to_add_association t('.add_solution'), f, :solutions, - partial: 'comprehension_solution_fields', - find_selector: 'tbody.tbody-points.'+point_id, - insert_using: 'append' - / point_id must not be the last class so that it will be correctly substituted by cocoon - tbody class=[point_id, 'tbody-points'] - = f.simple_fields_for :solutions do |comprehension_solutions_form| - = render 'comprehension_solution_fields', f: comprehension_solutions_form diff --git a/app/views/course/assessment/question/text_responses/_comprehension_solution_fields.html.slim b/app/views/course/assessment/question/text_responses/_comprehension_solution_fields.html.slim deleted file mode 100644 index 3fd0cc77923..00000000000 --- a/app/views/course/assessment/question/text_responses/_comprehension_solution_fields.html.slim +++ /dev/null @@ -1,20 +0,0 @@ -= content_tag_for(:tr, f.object, class: 'nested-fields') do - td = f.input :solution_type, - collection: Course::Assessment::Question::TextResponseComprehensionSolution.solution_types.keys, - label_method: lambda { |key| t(".#{key}") }, - input_html: { class: ['text-response-solution-type'] }, - label: false - / TODO: Fix text to array. - / f.object_name must not be the last class so that it will be correctly substituted by cocoon - td class=[f.object_name, 'td-solution'] - = f.input :solution, as: :array, label: false, required: false, - input_html: { class: ['text-response-solution'] } - .has-error - = f.error :solution_lemma - td.td-solution-button - = link_to 'javascript:void(0)', - class: ['btn', 'btn-default', 'solution-button', f.object_name], - title: t('.add_solution_word') do - = fa_icon 'plus'.freeze - td = f.input :information, label: false, placeholder: t('.information_hint') - td = link_to_remove_association t('.remove'), f diff --git a/app/views/course/assessment/question/text_responses/_form_comprehension.html.slim b/app/views/course/assessment/question/text_responses/_form_comprehension.html.slim deleted file mode 100644 index da5b7846bea..00000000000 --- a/app/views/course/assessment/question/text_responses/_form_comprehension.html.slim +++ /dev/null @@ -1,29 +0,0 @@ -= simple_form_for [current_course, @assessment, @text_response_question] do |f| - = f.error_notification - = render partial: 'course/assessment/questions/form', locals: { f: f, question_assessment: @question_assessment } - = f.hidden_field :hide_text - = f.hidden_field :is_comprehension - = f.hidden_field :allow_attachment - - b = t('.multiline_explanation_comprehension_html') - table.table.table-hover.table-comprehension - thead - tr - th = link_to_add_association t('.add_group'), f, :groups, - partial: 'comprehension_group_fields', - find_selector: 'tbody.tbody-form', insert_using: 'append' - tbody.tbody-form - = f.simple_fields_for :groups do |comprehension_groups_form| - = render 'comprehension_group_fields', f: comprehension_groups_form - - - if @assessment.autograded? - p - b = t('.text_response_autograde') - - - name = t('.comprehension') - - - if f.object.persisted? - - button_text = t('helpers.buttons.update', model: name) - - else - - button_text = t('helpers.buttons.create', model: name) - = f.button :submit, button_text diff --git a/app/views/course/assessment/question/text_responses/edit.html.slim b/app/views/course/assessment/question/text_responses/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/text_responses/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/text_responses/new.html.slim b/app/views/course/assessment/question/text_responses/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/text_responses/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/voice_responses/edit.html.slim b/app/views/course/assessment/question/voice_responses/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/voice_responses/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question/voice_responses/new.html.slim b/app/views/course/assessment/question/voice_responses/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/question/voice_responses/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/question_bundle_assignments/edit.html.slim b/app/views/course/assessment/question_bundle_assignments/edit.html.slim index 1664dfc9a42..db223c4f3ed 100644 --- a/app/views/course/assessment/question_bundle_assignments/edit.html.slim +++ b/app/views/course/assessment/question_bundle_assignments/edit.html.slim @@ -1,4 +1,4 @@ -= page_header 'Edit Question Bundle Assignment' +/ = page_header 'Edit Question Bundle Assignment' - url = course_assessment_question_bundle_assignment_path(current_course, @assessment, @question_bundle_assignment) = simple_form_for @question_bundle_assignment, url: url do |f| diff --git a/app/views/course/assessment/question_bundle_assignments/index.html.slim b/app/views/course/assessment/question_bundle_assignments/index.html.slim index 9461133abf2..b1e8baa7fdd 100644 --- a/app/views/course/assessment/question_bundle_assignments/index.html.slim +++ b/app/views/course/assessment/question_bundle_assignments/index.html.slim @@ -1,4 +1,4 @@ -= page_header +/ = page_header h2 = t('.prepared_bundle_assignments') diff --git a/app/views/course/assessment/question_bundle_questions/edit.html.slim b/app/views/course/assessment/question_bundle_questions/edit.html.slim index df420915434..85b1f77a10e 100644 --- a/app/views/course/assessment/question_bundle_questions/edit.html.slim +++ b/app/views/course/assessment/question_bundle_questions/edit.html.slim @@ -1,4 +1,4 @@ -= page_header 'Edit Question Bundle Question' +/ = page_header 'Edit Question Bundle Question' = simple_form_for @question_bundle_question, url: course_assessment_question_bundle_question_path(current_course, @assessment, @question_bundle_question) do |f| diff --git a/app/views/course/assessment/question_bundle_questions/index.html.slim b/app/views/course/assessment/question_bundle_questions/index.html.slim index dc78efb3db6..cbffb4ea6c9 100644 --- a/app/views/course/assessment/question_bundle_questions/index.html.slim +++ b/app/views/course/assessment/question_bundle_questions/index.html.slim @@ -1,4 +1,4 @@ -= page_header 'Question Bundle Questions' +/ = page_header 'Question Bundle Questions' = link_to 'New Question Bundle Question', new_course_assessment_question_bundle_question_path(current_course, @assessment), class: %w(btn btn-primary) diff --git a/app/views/course/assessment/question_bundle_questions/new.html.slim b/app/views/course/assessment/question_bundle_questions/new.html.slim index 690f550fc64..f0ce85270c9 100644 --- a/app/views/course/assessment/question_bundle_questions/new.html.slim +++ b/app/views/course/assessment/question_bundle_questions/new.html.slim @@ -1,4 +1,4 @@ -= page_header 'New Question Bundle Question' +/ = page_header 'New Question Bundle Question' = simple_form_for @question_bundle_question, url: course_assessment_question_bundle_questions_path(current_course, @assessment) do |f| = render partial: 'form', locals: { f: f } diff --git a/app/views/course/assessment/question_bundles/edit.html.slim b/app/views/course/assessment/question_bundles/edit.html.slim index 0be90c31b66..5ae8b6ce8bb 100644 --- a/app/views/course/assessment/question_bundles/edit.html.slim +++ b/app/views/course/assessment/question_bundles/edit.html.slim @@ -1,4 +1,4 @@ -= page_header 'Edit Question Bundle' +/ = page_header 'Edit Question Bundle' = simple_form_for @question_bundle, url: course_assessment_question_bundle_path(current_course, @assessment, @question_bundle) do |f| diff --git a/app/views/course/assessment/question_bundles/index.html.slim b/app/views/course/assessment/question_bundles/index.html.slim index eeabdb26673..e99f335c1fd 100644 --- a/app/views/course/assessment/question_bundles/index.html.slim +++ b/app/views/course/assessment/question_bundles/index.html.slim @@ -1,4 +1,4 @@ -= page_header 'Question Bundles' +/ = page_header 'Question Bundles' = link_to 'New Question Bundle', new_course_assessment_question_bundle_path(current_course, @assessment), class: %w(btn btn-primary) diff --git a/app/views/course/assessment/question_bundles/new.html.slim b/app/views/course/assessment/question_bundles/new.html.slim index f9928623c0f..1b7fa123062 100644 --- a/app/views/course/assessment/question_bundles/new.html.slim +++ b/app/views/course/assessment/question_bundles/new.html.slim @@ -1,4 +1,4 @@ -= page_header 'New Question Bundle' +/ = page_header 'New Question Bundle' = simple_form_for @question_bundle, url: course_assessment_question_bundles_path(current_course, @assessment) do |f| = render partial: 'form', locals: { f: f } diff --git a/app/views/course/assessment/question_groups/edit.html.slim b/app/views/course/assessment/question_groups/edit.html.slim index b354f188834..28951179082 100644 --- a/app/views/course/assessment/question_groups/edit.html.slim +++ b/app/views/course/assessment/question_groups/edit.html.slim @@ -1,4 +1,4 @@ -= page_header 'Edit Question Group' +/ = page_header 'Edit Question Group' = simple_form_for @question_group, url: course_assessment_question_group_path(current_course, @assessment, @question_group) do |f| diff --git a/app/views/course/assessment/question_groups/index.html.slim b/app/views/course/assessment/question_groups/index.html.slim index 785acc3eed0..34df376ec86 100644 --- a/app/views/course/assessment/question_groups/index.html.slim +++ b/app/views/course/assessment/question_groups/index.html.slim @@ -1,4 +1,4 @@ -= page_header 'Question Groups' +/ = page_header 'Question Groups' = link_to 'New Question Group', new_course_assessment_question_group_path(current_course, @assessment), class: %w(btn btn-primary) diff --git a/app/views/course/assessment/question_groups/new.html.slim b/app/views/course/assessment/question_groups/new.html.slim index 57e486fb60b..06ba422f502 100644 --- a/app/views/course/assessment/question_groups/new.html.slim +++ b/app/views/course/assessment/question_groups/new.html.slim @@ -1,4 +1,4 @@ -= page_header 'New Question Group' +/ = page_header 'New Question Group' = simple_form_for @question_group, url: course_assessment_question_groups_path(current_course, @assessment) do |f| = render partial: 'form', locals: { f: f } diff --git a/app/views/course/assessment/sessions/new.html.slim b/app/views/course/assessment/sessions/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/sessions/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/skills/index.html.slim b/app/views/course/assessment/skills/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/skills/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/submission/logs/index.html.slim b/app/views/course/assessment/submission/logs/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/submission/logs/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/submission/submissions/edit.html.slim b/app/views/course/assessment/submission/submissions/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/submission/submissions/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/submission/submissions/index.html.slim b/app/views/course/assessment/submission/submissions/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/submission/submissions/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/assessment/submissions/index.html.slim b/app/views/course/assessment/submissions/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/assessment/submissions/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/courses/index.html.slim b/app/views/course/courses/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/courses/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/courses/show.html.slim b/app/views/course/courses/show.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/courses/show.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/discussion/topics/index.html.slim b/app/views/course/discussion/topics/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/discussion/topics/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/duplications/show.html.slim b/app/views/course/duplications/show.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/duplications/show.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/enrol_requests/index.html.slim b/app/views/course/enrol_requests/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/enrol_requests/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/experience_points/disbursement/new.html.slim b/app/views/course/experience_points/disbursement/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/experience_points/disbursement/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/experience_points_records/index.html.slim b/app/views/course/experience_points_records/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/experience_points_records/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/forum/forums/index.html.slim b/app/views/course/forum/forums/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/forum/forums/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/group/group_categories/index.html.slim b/app/views/course/group/group_categories/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/group/group_categories/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/group/group_categories/show.html.slim b/app/views/course/group/group_categories/show.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/group/group_categories/show.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/leaderboards/index.html.slim b/app/views/course/leaderboards/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/leaderboards/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/learning_map/index.html.slim b/app/views/course/learning_map/index.html.slim deleted file mode 100644 index 9b17f9c0201..00000000000 --- a/app/views/course/learning_map/index.html.slim +++ /dev/null @@ -1,3 +0,0 @@ -= page_header format_inline_text(@settings.title || t('.header')) - -div#app-root diff --git a/app/views/course/lesson_plan/items/_personal_or_ref_time.html.slim b/app/views/course/lesson_plan/items/_personal_or_ref_time.html.slim deleted file mode 100644 index 52218be18ea..00000000000 --- a/app/views/course/lesson_plan/items/_personal_or_ref_time.html.slim +++ /dev/null @@ -1,11 +0,0 @@ -- effective_time = item.time_for(course_user) -- reference_time = item.reference_time_for(course_user) -- if effective_time.is_a? Course::PersonalTime and effective_time.fixed? - span title=t('course.lesson_plan.items.fixed_desc') - = fa_icon 'lock' -=< format_datetime(effective_time[attribute], datetime_format) if effective_time[attribute] -- if (effective_time[attribute] != reference_time[attribute]) && (effective_time[attribute] != nil) && (reference_time[attribute] != nil) - br - strike - = t('course.lesson_plan.items.ref') - = format_datetime(reference_time[attribute], datetime_format) diff --git a/app/views/course/lesson_plan/items/index.html.slim b/app/views/course/lesson_plan/items/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/lesson_plan/items/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/levels/index.html.slim b/app/views/course/levels/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/levels/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/material/folders/show.html.slim b/app/views/course/material/folders/show.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/material/folders/show.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/personal_times/index.html.slim b/app/views/course/personal_times/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/personal_times/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/reference_timelines/index.html.slim b/app/views/course/reference_timelines/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/reference_timelines/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/statistics/index.html.slim b/app/views/course/statistics/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/statistics/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/survey/surveys/index.html.slim b/app/views/course/survey/surveys/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/survey/surveys/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/user_email_subscriptions/edit.html.slim b/app/views/course/user_email_subscriptions/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/user_email_subscriptions/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/user_invitations/index.html.slim b/app/views/course/user_invitations/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/user_invitations/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/user_invitations/new.html.slim b/app/views/course/user_invitations/new.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/user_invitations/new.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/user_registrations/create.html.slim b/app/views/course/user_registrations/create.html.slim deleted file mode 100644 index 5ac0dff5816..00000000000 --- a/app/views/course/user_registrations/create.html.slim +++ /dev/null @@ -1 +0,0 @@ -= render 'course/courses/show' diff --git a/app/views/course/users/index.html.slim b/app/views/course/users/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/users/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/users/staff.html.slim b/app/views/course/users/staff.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/users/staff.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/users/students.html.slim b/app/views/course/users/students.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/users/students.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/video/submission/submissions/index.html.slim b/app/views/course/video/submission/submissions/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/video/submission/submissions/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/video/videos/index.html.slim b/app/views/course/video/videos/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/video/videos/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/course/video_submissions/index.html.slim b/app/views/course/video_submissions/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/course/video_submissions/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/devise/confirmations/new.html.slim b/app/views/devise/confirmations/new.html.slim deleted file mode 100644 index 64c7702a32e..00000000000 --- a/app/views/devise/confirmations/new.html.slim +++ /dev/null @@ -1,13 +0,0 @@ -= page_header t('.resend') - -= simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| - = f.error_notification - = f.full_error :confirmation_token - - div.form-inputs - = f.input :email, required: true, autofocus: true, value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email), input_html: { autocomplete: "email" } - - div.form-actions - = f.button :submit, t('.resend') - -= render 'devise/shared/links' diff --git a/app/views/devise/passwords/new.html.slim b/app/views/devise/passwords/new.html.slim deleted file mode 100644 index 69b36a375cf..00000000000 --- a/app/views/devise/passwords/new.html.slim +++ /dev/null @@ -1,12 +0,0 @@ -= page_header t('.title') - -= simple_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| - = f.error_notification - - div.form-inputs - = f.input :email, required: true, autofocus: true, input_html: { autocomplete: "email" } - - div.form-actions - = f.button :submit, t('.button') - -= render "devise/shared/links" diff --git a/app/views/layouts/_progress_bar.html.slim b/app/views/layouts/_progress_bar.html.slim deleted file mode 100644 index 2bace588be5..00000000000 --- a/app/views/layouts/_progress_bar.html.slim +++ /dev/null @@ -1,6 +0,0 @@ -div.progress - div.progress-bar [ class=opts[:class] role='progressbar' aria-valuenow="#{percentage}" - aria-valuemin='0' aria-valuemax='100' style="width: #{percentage}%" - title=opts[:tooltip_text] data-placement=opts[:tooltip_placement]] - span.sr-only #{percentage}% Complete - = progress_bar_text diff --git a/app/views/layouts/_user.html.slim b/app/views/layouts/_user.html.slim deleted file mode 100644 index ca55adc8bb4..00000000000 --- a/app/views/layouts/_user.html.slim +++ /dev/null @@ -1,4 +0,0 @@ -= div_for(user) do - = display_user_image(user) - span.name - = display_user(user) diff --git a/app/views/layouts/course.html.slim b/app/views/layouts/course.html.slim deleted file mode 100644 index db67704c57f..00000000000 --- a/app/views/layouts/course.html.slim +++ /dev/null @@ -1,3 +0,0 @@ -- layout = controller.parent_layout(of_layout: 'course') || controller.current_layout -= render within_layout: layout do - div#app-root diff --git a/app/views/pages/403.html.slim b/app/views/pages/403.html.slim deleted file mode 100644 index b72b926f8ba..00000000000 --- a/app/views/pages/403.html.slim +++ /dev/null @@ -1,3 +0,0 @@ -div.page-header - h1 = t('pages.403.header') -p = simple_format(@exception.message) diff --git a/app/views/system/admin/admin/index.html.slim b/app/views/system/admin/admin/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/system/admin/admin/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/system/admin/instance/admin/index.html.slim b/app/views/system/admin/instance/admin/index.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/system/admin/instance/admin/index.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/user/profiles/edit.html.slim b/app/views/user/profiles/edit.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/user/profiles/edit.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root diff --git a/app/views/user/registrations/new.html.slim b/app/views/user/registrations/new.html.slim deleted file mode 100644 index 5145789cff3..00000000000 --- a/app/views/user/registrations/new.html.slim +++ /dev/null @@ -1,26 +0,0 @@ -= page_header - -= simple_format(t('.already_registered_html', - sign_in: link_to(t('layout.navbar.sign_in'), new_user_session_path))) - -= simple_form_for resource, as: resource_name, url: registration_path(resource_name) do |f| - = f.error_notification - .form-inputs - - if @invitation - = hidden_field_tag :invitation, @invitation.invitation_key - = f.input :name, required: true, input_html: { value: @invitation.name }, disabled: true - = f.input :email, required: true, input_html: { value: @invitation.email }, disabled: true - - else - = f.input :name, required: true, autofocus: true - = f.input :email, required: true - = f.input :password, required: true, - hint: (t('.password_hint', length: @minimum_password_length) if @validatable) - = f.input :password_confirmation, required: true - - = recaptcha_tags - br - - .form-actions - = f.button :submit, t('.sign_up') - -= render 'devise/shared/links' diff --git a/app/views/user/sessions/new.html.slim b/app/views/user/sessions/new.html.slim deleted file mode 100644 index d89ea5d3348..00000000000 --- a/app/views/user/sessions/new.html.slim +++ /dev/null @@ -1,12 +0,0 @@ -= page_header - -= simple_form_for resource, as: resource_name, url: session_path(resource_name) do |f| - div.form-inputs - = f.input :email, required: false, autofocus: true - = f.input :password, required: false - = f.input :remember_me, as: :boolean if devise_mapping.rememberable? - - div.form-actions - = f.button :submit, t('.sign_in') - -= render 'devise/shared/links' diff --git a/app/views/users/show.html.slim b/app/views/users/show.html.slim deleted file mode 100644 index a777c8100dc..00000000000 --- a/app/views/users/show.html.slim +++ /dev/null @@ -1 +0,0 @@ -div#app-root From 01ba84208711be6052a91833f7d271d143185eba Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:42:55 +0800 Subject: [PATCH 010/173] style(rails): remove InheritedNestedLayouts --- lib/extensions/inherited_nested_layouts.rb | 2 - .../action_controller.rb | 2 - .../action_controller/base.rb | 71 ------------------- .../inherited_nested_layouts_spec.rb | 69 ------------------ 4 files changed, 144 deletions(-) delete mode 100644 lib/extensions/inherited_nested_layouts.rb delete mode 100644 lib/extensions/inherited_nested_layouts/action_controller.rb delete mode 100644 lib/extensions/inherited_nested_layouts/action_controller/base.rb delete mode 100644 spec/libraries/inherited_nested_layouts_spec.rb diff --git a/lib/extensions/inherited_nested_layouts.rb b/lib/extensions/inherited_nested_layouts.rb deleted file mode 100644 index 6ded661c42d..00000000000 --- a/lib/extensions/inherited_nested_layouts.rb +++ /dev/null @@ -1,2 +0,0 @@ -# frozen_string_literal: true -module Extensions::InheritedNestedLayouts; end diff --git a/lib/extensions/inherited_nested_layouts/action_controller.rb b/lib/extensions/inherited_nested_layouts/action_controller.rb deleted file mode 100644 index 3a4274766c3..00000000000 --- a/lib/extensions/inherited_nested_layouts/action_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -# frozen_string_literal: true -module Extensions::InheritedNestedLayouts::ActionController; end diff --git a/lib/extensions/inherited_nested_layouts/action_controller/base.rb b/lib/extensions/inherited_nested_layouts/action_controller/base.rb deleted file mode 100644 index d352a754497..00000000000 --- a/lib/extensions/inherited_nested_layouts/action_controller/base.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true -module Extensions::InheritedNestedLayouts::ActionController::Base - # Gets the current layout used by this controller. - # - # @return [String] The layout used by the current controller. - def current_layout - _layout(nil, formats) - end - - # Gets the parent layout of the given layout, as specified in the layout hierarchy. - # - # @param [String] of_layout The layout to obtain the parent of. If this is nil, obtains the - # current controller's parent layout. - # @return [String] The parent layout of the given layout. - def parent_layout(of_layout: nil) - layout = of_layout || current_layout - layout_index = layout_hierarchy.find_index(layout) - return nil if layout_index.nil? || layout_index == 0 - - layout_hierarchy[layout_index - 1] - end - - # Gets the layout hierarchy, from the outermost to the innermost layout. - # - # @return [Array] The layout hierarchy for this controller. - def layout_hierarchy - extension_module = Extensions::InheritedNestedLayouts::ActionController::Base - @layout_hierarchy ||= - extension_module.class_hierarchy(self.class). - select { |klass| klass < ActionController::Base }. - map { |klass| extension_module.class_layout(klass, self, formats) }. - reject(&:nil?). - uniq. - reverse! - end - - # Gets the superclass hierarchy for the given class. Object is not part of the returned result. - # - # @param [Class] klass The class to obtain the hierarchy of. - # @return [Array] The superclass hierarchy for the given class. - def self.class_hierarchy(klass) - result = [] - while klass != Object - result << klass - klass = klass.superclass - end - - result - end - - # Gets the layout for objects of the given class. - # - # @param [Class] klass The class to obtain the layout of. This must be a subclass of - # ActionController::Base - # @param [ActionController::Base] self_ The instance to query against the class hierarchy. - # @return [String] The layout to use for instances of +klass+. - def self.class_layout(klass, self_, formats) - layout_method = klass.instance_method(:_layout) - layout = layout_method.bind(self_) - layout.call(nil, formats) - end - - # Overrides {ActionController::Rendering#render} to keep track of the :layout rendering option. - def render(*args) - options = args.extract_options! - layout_hierarchy << options[:layout] if options.try(:key?, :layout) - - args << options - super - end -end diff --git a/spec/libraries/inherited_nested_layouts_spec.rb b/spec/libraries/inherited_nested_layouts_spec.rb deleted file mode 100644 index c1cb3866e00..00000000000 --- a/spec/libraries/inherited_nested_layouts_spec.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.describe 'Extension: Inherited Nested Layouts', type: :controller do - class self::ControllerA < ApplicationController - prepend_view_path File.join(__dir__, '../fixtures/libraries/inherited_nested_layouts') - layout 'testA' - - protected - - def publicly_accessible? - true - end - end - - class self::ControllerB < self::ControllerA - layout :controller_b_layout - - def controller_b_layout - 'testB' - end - end - - class self::ControllerC < self::ControllerB - layout 'testC' - end - - controller(self::ControllerC) do - def index - render template: 'content', layout: 'test_layout' - end - end - - it 'gets the correct current layout' do - expect(controller.current_layout).to eq('testC') - end - - it 'gets the correct parent layout' do - expect(controller.parent_layout).to eq('testB') - end - - it 'gets the correct parent layout of the specified parent' do - expect(controller.parent_layout(of_layout: 'testB')).to eq('testA') - end - - it 'gets the correct layout hierarchy' do - expect(controller.layout_hierarchy).to eq([ - 'default', - 'testA', - 'testB', - 'testC' - ]) - end - - describe '#render' do - context 'when rendering with an explicit :layout' do - it 'gets the correct layout hierarchy' do - get :index - expect(controller.layout_hierarchy).to eq([ - 'default', - 'testA', - 'testB', - 'testC', - 'test_layout' - ]) - end - end - end -end From c644b25bf37ceb4cbe2f5fdee931a813ecc1df23 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:44:12 +0800 Subject: [PATCH 011/173] style(profiles): use `user_time_zone` --- .../strategies/base_personalization_strategy.rb | 4 ++-- app/views/user/profiles/edit.json.jbuilder | 2 +- .../bundles/user/AccountSettings/AccountSettingsForm.tsx | 4 ++-- client/app/bundles/user/operations.ts | 8 ++++---- client/app/types/users.ts | 4 ++-- spec/features/user/profile_edit_spec.rb | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/controllers/concerns/course/lesson_plan/strategies/base_personalization_strategy.rb b/app/controllers/concerns/course/lesson_plan/strategies/base_personalization_strategy.rb index 1bffa34ea3f..c391c7e0326 100644 --- a/app/controllers/concerns/course/lesson_plan/strategies/base_personalization_strategy.rb +++ b/app/controllers/concerns/course/lesson_plan/strategies/base_personalization_strategy.rb @@ -63,10 +63,10 @@ def execute(_course_user, _precomputed_data, _items_to_shift = nil) protected - # Round to "nearest" date in course's timezone, NOT user's timezone. + # Round to "nearest" date in course's time zone, NOT user's time zone. # # @param [ActiveSupport::TimeWithZone] datetime The datetime object to round. - # @param [String] course_tz The timezone of the course. + # @param [String] course_tz The time zone of the course. # @param [Boolean] to_2359 Whether to round off to 2359. This will set the datetime to be 2359 of the date before the # rounded date. def round_to_date(datetime, course_tz, to_2359: false) diff --git a/app/views/user/profiles/edit.json.jbuilder b/app/views/user/profiles/edit.json.jbuilder index 8c2400f5bc2..7154f0567e1 100644 --- a/app/views/user/profiles/edit.json.jbuilder +++ b/app/views/user/profiles/edit.json.jbuilder @@ -2,7 +2,7 @@ json.id current_user.id json.name current_user.name -json.timezone current_user.time_zone +json.timeZone user_time_zone json.locale I18n.locale json.imageUrl user_image(current_user) json.availableLocales I18n.available_locales diff --git a/client/app/bundles/user/AccountSettings/AccountSettingsForm.tsx b/client/app/bundles/user/AccountSettings/AccountSettingsForm.tsx index 1538625a1c1..4684f4ea519 100644 --- a/client/app/bundles/user/AccountSettings/AccountSettingsForm.tsx +++ b/client/app/bundles/user/AccountSettings/AccountSettingsForm.tsx @@ -61,7 +61,7 @@ const AccountSettingsForm = (props: AccountSettingsFormProps): JSX.Element => { object().shape( { name: string().required(t(translations.nameRequired)), - timezone: string().required(t(translations.timeZoneRequired)), + timeZone: string().required(t(translations.timeZoneRequired)), locale: string().required(t(translations.localeRequired)), currentPassword: string() .optional() @@ -171,7 +171,7 @@ const AccountSettingsForm = (props: AccountSettingsFormProps): JSX.Element => { ( => { export const updateProfile = async ( data: Partial, ): Promise | undefined> => { - if (!data.name && !data.timezone && !data.locale) return undefined; + if (!data.name && !data.timeZone && !data.locale) return undefined; const adaptedData: ProfilePostData = { user: { name: data.name, - time_zone: data.timezone, + time_zone: data.timeZone, locale: data.locale, }, }; @@ -54,7 +54,7 @@ export const updateProfile = async ( const response = await GlobalAPI.users.updateProfile(adaptedData); return { name: response.data.name, - timezone: response.data.timezone, + timeZone: response.data.timeZone, locale: response.data.locale, }; } catch (error) { @@ -159,7 +159,7 @@ export const resendConfirmationEmail = async ( url: NonNullable, ): Promise => { try { - await GlobalAPI.users.resendConfirmationEmail(url); + await GlobalAPI.users.resendConfirmationEmailByURL(url); } catch (error) { if (error instanceof AxiosError) throw error.response?.data?.errors; throw error; diff --git a/client/app/types/users.ts b/client/app/types/users.ts index d14919547fc..48488fa1dad 100644 --- a/client/app/types/users.ts +++ b/client/app/types/users.ts @@ -86,7 +86,7 @@ export type Locale = 'en' | 'zh'; export interface ProfileData { id: string; name: string; - timezone: string; + timeZone: string; locale: string; imageUrl: string; availableLocales: Locale[]; @@ -114,7 +114,7 @@ export interface PasswordData { export interface ProfilePostData { user: { name?: ProfileData['name']; - time_zone?: ProfileData['timezone']; + time_zone?: ProfileData['timeZone']; locale?: ProfileData['locale']; profile_photo?: ProfileData['imageUrl']; }; diff --git a/spec/features/user/profile_edit_spec.rb b/spec/features/user/profile_edit_spec.rb index 29952f362ed..abdad4cb647 100644 --- a/spec/features/user/profile_edit_spec.rb +++ b/spec/features/user/profile_edit_spec.rb @@ -17,7 +17,7 @@ time_zone = 'Singapore' fill_in 'name', with: new_name - select time_zone, from: 'timezone' + select time_zone, from: 'timeZone' click_button 'Save changes' wait_for_page From 7b18b074717c454ba008f32fa72dae5dc650e96c Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:46:38 +0800 Subject: [PATCH 012/173] feat(application_controller): `#home` -> root at `#index` --- app/controllers/application_controller.rb | 3 +++ app/controllers/concerns/application_user_concern.rb | 5 ++--- .../home.json.jbuilder => application/index.json.jbuilder} | 0 app/views/pages/home.html.slim | 1 - config/routes.rb | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) rename app/views/{pages/home.json.jbuilder => application/index.json.jbuilder} (100%) delete mode 100644 app/views/pages/home.html.slim diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3f3db35009d..4510af477a8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -20,6 +20,9 @@ class ApplicationController < ActionController::Base rescue_from IllegalStateError, with: :handle_illegal_state_error rescue_from ActionController::InvalidAuthenticityToken, with: :handle_csrf_error + def index + end + protected # Runs the provided block with Bullet disabled. diff --git a/app/controllers/concerns/application_user_concern.rb b/app/controllers/concerns/application_user_concern.rb index 51c0789ae2f..d6c3a50589b 100644 --- a/app/controllers/concerns/application_user_concern.rb +++ b/app/controllers/concerns/application_user_concern.rb @@ -21,11 +21,10 @@ def url_to_user_or_course_user(course, user) protected def publicly_accessible? - is_a?(HighVoltage::PagesController) + action_name.to_sym == :index end def handle_access_denied(exception) - @exception = exception - render 'pages/403', status: :forbidden + render json: { errors: exception.message }, status: :forbidden end end diff --git a/app/views/pages/home.json.jbuilder b/app/views/application/index.json.jbuilder similarity index 100% rename from app/views/pages/home.json.jbuilder rename to app/views/application/index.json.jbuilder diff --git a/app/views/pages/home.html.slim b/app/views/pages/home.html.slim deleted file mode 100644 index d90da6f586b..00000000000 --- a/app/views/pages/home.html.slim +++ /dev/null @@ -1 +0,0 @@ -p Home Page TBD diff --git a/config/routes.rb b/config/routes.rb index b8f0297ebb6..71113187088 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,7 +4,7 @@ # See how all your routes lay out with "rake routes". # You can have the root of your site routed with "root" - # root 'welcome#index' + root 'application#index' # Example of regular route: # get 'products/:id' => 'catalog#view' From a797073b537639b7aeb11cd028683ee1e9f32bce Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:47:57 +0800 Subject: [PATCH 013/173] fix(courses_controller): 403 when viewing unenrolled courses --- app/controllers/course/courses_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/course/courses_controller.rb b/app/controllers/course/courses_controller.rb index f6bea291e2e..bdeabb631d0 100644 --- a/app/controllers/course/courses_controller.rb +++ b/app/controllers/course/courses_controller.rb @@ -39,7 +39,7 @@ def sidebar protected def publicly_accessible? - params[:action] == 'index' + Set[:index, :sidebar].include?(action_name.to_sym) end private From 36ec6d54e84acd4fa898ac9f302f6d4f39310a1b Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:49:37 +0800 Subject: [PATCH 014/173] feat(user_notifications): allow unenrolled course to `fetch` safely --- .../course/user_notifications_controller.rb | 1 + client/app/api/course/UserNotifications.ts | 2 +- .../course/user-notification/operations.ts | 19 ++----------------- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/app/controllers/course/user_notifications_controller.rb b/app/controllers/course/user_notifications_controller.rb index 3cd802f206e..944b6223633 100644 --- a/app/controllers/course/user_notifications_controller.rb +++ b/app/controllers/course/user_notifications_controller.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true class Course::UserNotificationsController < Course::Controller + skip_authorize_resource :course, only: [:fetch] load_and_authorize_resource :user_notification, class: UserNotification.name, only: :mark_as_read def fetch diff --git a/client/app/api/course/UserNotifications.ts b/client/app/api/course/UserNotifications.ts index 2b88f36f79a..21729341b19 100644 --- a/client/app/api/course/UserNotifications.ts +++ b/client/app/api/course/UserNotifications.ts @@ -9,7 +9,7 @@ export default class UserNotificationsAPI extends BaseCourseAPI { return `/courses/${this.courseId}/user_notifications`; } - fetch(): APIResponse { + fetch(): APIResponse { return this.client.get(`${this.#urlPrefix}/fetch`); } diff --git a/client/app/bundles/course/user-notification/operations.ts b/client/app/bundles/course/user-notification/operations.ts index 12a973f3c16..c97cd22d65f 100644 --- a/client/app/bundles/course/user-notification/operations.ts +++ b/client/app/bundles/course/user-notification/operations.ts @@ -1,27 +1,12 @@ -import { AxiosError } from 'axios'; import { UserNotificationData } from 'types/course/userNotifications'; import CourseAPI from 'api/course'; -/** - * Fetches the current user's notifications in the course, if available. - * - * If the current user is not a course user, the network request will fail with - * status code 403. In this case, `undefined` will be returned and no errors will - * be thrown. - */ export const fetchNotifications = async (): Promise< UserNotificationData | undefined > => { - try { - const response = await CourseAPI.userNotifications.fetch(); - return response.data; - } catch (error) { - if (!(error instanceof AxiosError && error.response?.status === 403)) - throw error; - - return undefined; - } + const response = await CourseAPI.userNotifications.fetch(); + return response.data ?? undefined; }; export const markAsRead = async ( From e2a919ba70454b88984a510386a61df475b67e38 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:50:45 +0800 Subject: [PATCH 015/173] chore(foreman): remove foreman configs --- .foreman | 2 -- Procfile.dev | 2 -- Procfile.profile | 2 -- 3 files changed, 6 deletions(-) delete mode 100644 .foreman delete mode 100644 Procfile.dev delete mode 100644 Procfile.profile diff --git a/.foreman b/.foreman deleted file mode 100644 index 2abfe5b3e9a..00000000000 --- a/.foreman +++ /dev/null @@ -1,2 +0,0 @@ -# Defaults for foreman -procfile: Procfile.dev diff --git a/Procfile.dev b/Procfile.dev deleted file mode 100644 index a5b163de63d..00000000000 --- a/Procfile.dev +++ /dev/null @@ -1,2 +0,0 @@ -web: bundle exec rails server -client: cd client && yarn build:development diff --git a/Procfile.profile b/Procfile.profile deleted file mode 100644 index 561a0a80794..00000000000 --- a/Procfile.profile +++ /dev/null @@ -1,2 +0,0 @@ -web: bundle exec rails server -client: cd client && yarn build:profile From c10b302c787799b3f3cb33906e3bb1445d37f51b Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:51:27 +0800 Subject: [PATCH 016/173] chore(hound): do not check javascript/css/scss --- .hound.yml | 8 ++------ .jshintignore | 1 - .jshintrc | 27 --------------------------- .scss-lint.yml | 40 ---------------------------------------- 4 files changed, 2 insertions(+), 74 deletions(-) delete mode 100644 .jshintignore delete mode 100644 .jshintrc delete mode 100644 .scss-lint.yml diff --git a/.hound.yml b/.hound.yml index eef73806924..5b9d8769909 100644 --- a/.hound.yml +++ b/.hound.yml @@ -4,9 +4,5 @@ rubocop: config_file: .rubocop.yml version: 1.22.1 -jshint: - config_file: .jshintrc - ignore_file: .jshintignore - -scss: - config_file: .scss-lint.yml +javascript: + enabled: false diff --git a/.jshintignore b/.jshintignore deleted file mode 100644 index 1a3430af1dc..00000000000 --- a/.jshintignore +++ /dev/null @@ -1 +0,0 @@ -client/** diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index ffd89d07e06..00000000000 --- a/.jshintrc +++ /dev/null @@ -1,27 +0,0 @@ -{ - "boss": false, - "browser": true, - "camelcase": true, - "curly": true, - "eqeqeq": true, - "eqnull": true, - "expr": true, - "forin": true, - "globals": { - "jQuery": true, - "$": true, - "ace": true - }, - "immed": true, - "indent": 4, - "latedef": "nofunc", - "maxlen": 100, - "newcap": true, - "noarg": true, - "quotmark": "single", - "strict": true, - "sub": true, - "trailing": true, - "undef": true, - "unused": true -} diff --git a/.scss-lint.yml b/.scss-lint.yml deleted file mode 100644 index f5ffd4a7ac2..00000000000 --- a/.scss-lint.yml +++ /dev/null @@ -1,40 +0,0 @@ -scss_files: 'app/**/*.scss' - -linters: - # Unhounding. - HexLength: - enabled: true - - StringQuotes: - style: single_quotes - - # Project-specific options - HexLength: - style: long - - IdSelector: - enabled: false - - LeadingZero: - style: include_zero - - PlaceholderInExtend: - enabled: false - - QualifyingElement: - allow_element_with_class: true - - NestingDepth: - max_depth: 5 - - SelectorDepth: - max_depth: 5 - - # CSS Modules requires class selectors to be in camelCase - SelectorFormat: - ignored_types: [class] - - # Allow CSS Modules composition - PropertySpelling: - extra_properties: - - composes From 04e6736c51cca4a32466176cc56488ed2e466144 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:51:53 +0800 Subject: [PATCH 017/173] style(rails): remove pygments-css --- .gitmodules | 3 --- vendor/assets/javascripts/.keep | 0 vendor/assets/stylesheets/.keep | 0 vendor/assets/stylesheets/pygments-css | 1 - 4 files changed, 4 deletions(-) delete mode 100644 vendor/assets/javascripts/.keep delete mode 100644 vendor/assets/stylesheets/.keep delete mode 160000 vendor/assets/stylesheets/pygments-css diff --git a/.gitmodules b/.gitmodules index db92c1cd5ec..6a4f29fbf16 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "vendor/assets/stylesheets/pygments-css"] - path = vendor/assets/stylesheets/pygments-css - url = https://github.com/richleland/pygments-css.git [submodule "vendor/assets/javascripts/recorderjs"] path = client/vendor/recorderjs url = https://github.com/mattdiamond/Recorderjs.git diff --git a/vendor/assets/javascripts/.keep b/vendor/assets/javascripts/.keep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/vendor/assets/stylesheets/.keep b/vendor/assets/stylesheets/.keep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/vendor/assets/stylesheets/pygments-css b/vendor/assets/stylesheets/pygments-css deleted file mode 160000 index 146708f9003..00000000000 --- a/vendor/assets/stylesheets/pygments-css +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 146708f9003299106baf05987abf393eae4424fc From a999e08e835147b06747abc4df20f5abf33458dc Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:52:40 +0800 Subject: [PATCH 018/173] test(capybara): fix flaky `expect_toastify` --- spec/support/capybara.rb | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index f8776528776..1d7794ddee2 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -83,17 +83,9 @@ def fill_in_rails_summernote(selector, text) # to ensure certain changes are made before continuing with the tests. def expect_toastify(message) wait_for_page # To ensure toast is open - found = false - find_all('div.Toastify__toast').each do |toast| - within toast do - toast_text = find('div.Toastify__toast-body', visible: true).text - found = true if toast_text.include? message - find('div.Toastify__toast-body', visible: true).click - break if found - end - end - wait_for_page # To ensure toast is closed - expect(found).to be_truthy + container = find_all('.Toastify').first + expect(container).to have_text(message) + find('p', text: message).click end # Finds a react-beautiful-dnd draggable element From 6130373239aa9acedd3e20d2654407f9df1ed90d Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:54:16 +0800 Subject: [PATCH 019/173] test(capybara): add sign in, sign out performers --- spec/support/authentication_performers.rb | 40 +++++++++++++++++++++++ spec/support/capybara.rb | 5 --- spec/support/devise.rb | 1 - 3 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 spec/support/authentication_performers.rb diff --git a/spec/support/authentication_performers.rb b/spec/support/authentication_performers.rb new file mode 100644 index 00000000000..68bbdcd3d57 --- /dev/null +++ b/spec/support/authentication_performers.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +module AuthenticationPerformersTestHelpers + include Warden::Test::Helpers + + alias_method :warden_logout, :logout + + def login_as(user, _ = {}) + # For some reasons, sometimes new scenarios are automatically logged in as the previous user. + # Clearing cookies isn't enough and subsequent requests still carries over an old, authenticated + # session cookie. We force the server to log out all remaining sessions before logging in. + warden_logout + + visit new_user_session_path + + fill_in 'Email address', with: user.email + fill_in 'Password', with: password_for(user) + click_button 'Sign in' + + wait_for_page + end + + def logout(*_) + # We expect all pages should have a user menu button. + find('div[data-testid="user-menu-button"]').click + find('li', text: 'Sign out').click + + super + wait_for_page + end + + private + + def password_for(user) + user.password || Application::Application.config.x.default_user_password + end +end + +RSpec.configure do |config| + config.include AuthenticationPerformersTestHelpers, type: :feature +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 1d7794ddee2..944617a9ca7 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -140,11 +140,6 @@ def find_sidebar all('aside').first end - def perform_logout_in_course(user_name) - find_sidebar.find_all('div', text: user_name).first.click - find('span', text: 'Sign out').click - wait_for_page - end def confirm_registartion_token_via_email token = ActionMailer::Base.deliveries.last.body.match(/confirmation_token=.*(?=")/) diff --git a/spec/support/devise.rb b/spec/support/devise.rb index cdb9877d6ad..454605c8d44 100644 --- a/spec/support/devise.rb +++ b/spec/support/devise.rb @@ -16,5 +16,4 @@ def requires_login(as: nil) # rubocop:disable Naming/MethodParameterName config.include Devise::Test::ControllerHelpers, type: :controller config.extend DeviseControllerMacros, type: :controller config.include Warden::Test::Helpers, type: :request - config.include Warden::Test::Helpers, type: :feature end From d56dd237a337e4099bb322d2ff3aa473deaab0d6 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:55:06 +0800 Subject: [PATCH 020/173] test(email_management): fix flaky tests --- spec/features/user/email_management_spec.rb | 5 +++-- spec/support/capybara.rb | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/features/user/email_management_spec.rb b/spec/features/user/email_management_spec.rb index c286de4e14a..a7bed04dddf 100644 --- a/spec/features/user/email_management_spec.rb +++ b/spec/features/user/email_management_spec.rb @@ -64,11 +64,12 @@ click_button 'Add email address' fill_in 'newEmail', with: invitation1.email click_button 'Add email address' + wait_for_page expect(page).to have_selector('section', text: invitation1.email) end.to change { user.emails.count }.by(1) expect do - confirm_registartion_token_via_email + confirm_registration_token_via_email end.to change(course1.users, :count).by(1). and change(course2.users, :count).by(1) end @@ -89,7 +90,7 @@ end.to change(user.emails, :count).by(1). and change(other_user.emails, :count).by(-1) - confirm_registartion_token_via_email + confirm_registration_token_via_email expect(user.emails.last.reload.confirmed_at).not_to be_nil end end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 944617a9ca7..a75dd627730 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -141,9 +141,10 @@ def find_sidebar end - def confirm_registartion_token_via_email + def confirm_registration_token_via_email token = ActionMailer::Base.deliveries.last.body.match(/confirmation_token=.*(?=")/) visit "/users/confirmation?#{token}" + wait_for_page end end end From 14b256c201744714c40071f0971c6956454c78b2 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:57:33 +0800 Subject: [PATCH 021/173] test(capybara): set up different client, server ports --- config/environments/test.rb | 7 +++++-- spec/spec_helper.rb | 1 + spec/support/acts_as_tenant.rb | 3 +-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/config/environments/test.rb b/config/environments/test.rb index 01b16e849b7..b6f0666e308 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -43,7 +43,7 @@ config.action_mailer.default_options = { from: 'coursemology@example.org' } # We will assume that we are running on localhost - config.action_mailer.default_url_options = { host: 'localhost' } + config.action_mailer.default_url_options = { host: 'lvh.me:4200' } # Use the threaded background job adapter for replicating the production environment. config.active_job.queue_adapter = ActiveJob::QueueAdapters::BackgroundThreadAdapter.new @@ -54,7 +54,10 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - config.x.default_host = 'coursemology.lvh.me' + config.x.default_host = 'lvh.me' + config.x.client_port = 4200 + config.x.server_port = 6969 + config.x.default_user_password = 'lolololol' # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index de10a293571..512d530225e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -125,4 +125,5 @@ config.server = :puma, { Silent: true } config.default_max_wait_time = 5 config.enable_aria_label = true + config.server_port = Application::Application.config.x.server_port end diff --git a/spec/support/acts_as_tenant.rb b/spec/support/acts_as_tenant.rb index 8690f69f07b..8290d479f18 100644 --- a/spec/support/acts_as_tenant.rb +++ b/spec/support/acts_as_tenant.rb @@ -2,8 +2,7 @@ # Test group helpers for setting the tenant for tests. module ActsAsTenant::TestGroupHelpers def self.build_host(instance) - port = Capybara.current_session&.server&.port - if port + if (port = Application::Application.config.x.client_port) "http://#{instance.host}:#{port}" else "http://#{instance.host}" From c5a8dddd23101f1fa1032719ab0c62b49dbc65f9 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:58:17 +0800 Subject: [PATCH 022/173] chore(rails): set up `lvh.me` as base URL for development --- config/environments/development.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 10ff6dba999..7b9334e8ba2 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -61,9 +61,10 @@ # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::EventedFileUpdateChecker - config.x.default_host = 'localhost:5000' + config.x.default_app_host = 'lvh.me' + config.x.default_host = "#{config.x.default_app_host}:5000" - config.action_mailer.default_url_options = { host: 'localhost:5000' } + config.action_mailer.default_url_options = { host: "#{config.x.default_app_host}:5000" } # Rails 6.0.5.1 security patch # To find out more unpermitted classes and add below then uncomment @@ -78,4 +79,6 @@ config.action_mailer.raise_delivery_errors = false config.action_cable.disable_request_forgery_protection = true + + config.hosts << ".#{config.x.default_app_host}" end From 640b812ac7bf316d0bed3dbc4a2b91fd727846b0 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 16:59:33 +0800 Subject: [PATCH 023/173] feat(rails): add CORS config --- Gemfile | 3 +++ Gemfile.lock | 3 +++ config/environments/development.rb | 7 +++++++ config/environments/production.rb | 7 +++++++ config/environments/test.rb | 7 +++++++ 5 files changed, 27 insertions(+) diff --git a/Gemfile b/Gemfile index f6813394a24..265c5dbd05f 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,9 @@ gem 'rails', '~> 6.0.6.1' # Use PostgreSQL for the backend gem 'pg' +# Enables CORS configuration to allow sharing resources with client on another domain +gem 'rack-cors' + # Instance/Course settings gem 'settings_on_rails' # Manage read/unread status diff --git a/Gemfile.lock b/Gemfile.lock index 371014da851..eb4e2a40648 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -380,6 +380,8 @@ GEM raabro (1.4.0) racc (1.7.1) rack (2.2.7) + rack-cors (2.0.1) + rack (>= 2.0.0) rack-mini-profiler (3.1.0) rack (>= 1.2.0) rack-protection (2.2.3) @@ -671,6 +673,7 @@ DEPENDENCIES parallel_tests pg puma + rack-cors rack-mini-profiler rails (~> 6.0.6.1) rails-controller-testing diff --git a/config/environments/development.rb b/config/environments/development.rb index 7b9334e8ba2..5347a4f65cc 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -81,4 +81,11 @@ config.action_cable.disable_request_forgery_protection = true config.hosts << ".#{config.x.default_app_host}" + + config.middleware.insert_before 0, Rack::Cors do + allow do + origins(/lvh\.me:([0-9]+)/, /(.*?)\.lvh\.me:([0-9]+)/) + resource '*', headers: :any, methods: [:get, :post, :patch, :put], credentials: true + end + end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 7e005ceaa11..877de322745 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -134,4 +134,11 @@ # config.active_record.yaml_column_permitted_classes = [ActiveSupport::HashWithIndifferentAccess, # ActiveSupport::Duration] config.active_record.use_yaml_unsafe_load = true + + config.middleware.insert_before 0, Rack::Cors do + allow do + origins(/coursemology\.org:([0-9]+)/, /(.*?)\.coursemology\.org:([0-9]+)/) + resource '*', headers: :any, methods: [:get, :post, :patch, :put], credentials: true + end + end end diff --git a/config/environments/test.rb b/config/environments/test.rb index b6f0666e308..10df870a6ff 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -69,4 +69,11 @@ # config.active_record.yaml_column_permitted_classes = [ActiveSupport::HashWithIndifferentAccess, # ActiveSupport::Duration] config.active_record.use_yaml_unsafe_load = true + + config.middleware.insert_before 0, Rack::Cors do + allow do + origins(/lvh\.me:([0-9]+)/, /(.*?)\.lvh\.me:([0-9]+)/) + resource '*', headers: :any, methods: [:get, :post, :patch, :put], credentials: true + end + end end From 1100e0a66b7afe25c595c0728fed97df9ad83b81 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:00:47 +0800 Subject: [PATCH 024/173] chore(rails): remove unused gems --- .coveralls.yml | 1 - Gemfile | 24 -------- Gemfile.lock | 47 ---------------- app/helpers/application_helper.rb | 73 +++---------------------- app/helpers/course/controller_helper.rb | 10 ---- config/environments/production.rb | 2 - config/initializers/assets.rb | 13 ----- 7 files changed, 7 insertions(+), 163 deletions(-) delete mode 100644 .coveralls.yml delete mode 100644 config/initializers/assets.rb diff --git a/.coveralls.yml b/.coveralls.yml deleted file mode 100644 index 91600595a1b..00000000000 --- a/.coveralls.yml +++ /dev/null @@ -1 +0,0 @@ -service_name: travis-ci diff --git a/Gemfile b/Gemfile index 265c5dbd05f..333755f6cd8 100644 --- a/Gemfile +++ b/Gemfile @@ -38,22 +38,6 @@ gem 'active_record_upsert', '0.11.1' # Create pretty URLs and work with human-friendly strings gem 'friendly_id' -# Use SCSS for stylesheets -gem 'sass-rails' -# Use Uglifier as compressor for JavaScript assets -gem 'uglifier', '>= 1.3.0' - -# Internationalisation for JavaScript. -gem 'i18n-js', '<= 3.10.0' - -# Use jQuery as the JavaScript library -gem 'jquery-rails' -# Our Coursemology will be themed using Bootstrap -gem 'bootstrap-sass' -gem 'bootstrap-sass-extras', '>= 0.1.0' -gem 'autoprefixer-rails' -# Use font-awesome for icons -gem 'font-awesome-rails' # HTML Pipeline and dependencies gem 'html-pipeline' gem 'sanitize', '>= 4.6.3' @@ -82,10 +66,6 @@ group :development do gem 'spring', platforms: [:ruby] gem 'listen' - # Gems to make development mode faster and less painful - - gem 'wdm', '>= 0.0.3', platforms: [:mswin, :mswin64] - # Helps to prevent database slowdowns gem 'lol_dba', require: false @@ -95,9 +75,6 @@ group :development do # bundle exec yardoc generates the API under doc/. # Use yard stats --list-undoc to find what needs documenting. gem 'yard', group: :doc - - # Gem to generate favicon - gem 'rails_real_favicon' end group :test do @@ -222,6 +199,5 @@ gem 'rwordnet', git: 'https://github.com/makqien/rwordnet' gem 'loofah', '>= 2.2.1' gem 'rails-html-sanitizer', '>= 1.0.4' -gem 'sprockets', '< 4.0.0' gem 'mimemagic', '0.4.3' gem 'ffi', '>= 1.14.2' diff --git a/Gemfile.lock b/Gemfile.lock index eb4e2a40648..a1e34203636 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -116,8 +116,6 @@ GEM activerecord (>= 3.0.0) activesupport (>= 3.0.0) ast (2.4.2) - autoprefixer-rails (10.4.13.0) - execjs (~> 2) aws-eventstream (1.2.0) aws-partitions (1.792.0) aws-sdk-core (3.179.0) @@ -142,12 +140,6 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bootstrap-sass (3.4.1) - autoprefixer-rails (>= 5.2.1) - sassc (>= 2.0.0) - bootstrap-sass-extras (0.1.0) - rails (>= 3.1.0) - bootstrap_tokenfield_rails (0.12.1) builder (3.2.4) bullet (7.0.7) activesupport (>= 3.0.0) @@ -180,7 +172,6 @@ GEM mini_mime (>= 0.1.3) ssrf_filter (~> 1.0) childprocess (4.1.0) - cocoon (1.2.15) codecov (0.6.0) simplecov (>= 0.15, < 0.22) concurrent-ruby (1.2.2) @@ -216,7 +207,6 @@ GEM et-orbi (1.2.7) tzinfo excon (0.92.3) - execjs (2.8.1) exifr (1.3.9) factory_bot (6.2.1) activesupport (>= 5.0.0) @@ -242,8 +232,6 @@ GEM fog-xml (0.1.4) fog-core nokogiri (>= 1.5.11, < 2.0.0) - font-awesome-rails (4.7.0.8) - railties (>= 3.2, < 8.0) formatador (1.1.0) friendly_id (5.5.0) activerecord (>= 4.0.0) @@ -261,8 +249,6 @@ GEM http_accept_language (2.1.1) i18n (1.14.1) concurrent-ruby (~> 1.0) - i18n-js (3.9.2) - i18n (>= 0.6.6) i18n-tasks (1.0.12) activesupport (>= 4.0.2) ast (>= 2.1.0) @@ -294,10 +280,6 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) - jquery-rails (4.5.1) - rails-dom-testing (>= 1, < 3) - railties (>= 4.2.0) - thor (>= 0.14, < 2.0) json (2.6.3) kaminari (1.2.2) activesupport (>= 4.1.0) @@ -386,8 +368,6 @@ GEM rack (>= 1.2.0) rack-protection (2.2.3) rack - rack-proxy (0.7.6) - rack rack-test (2.1.0) rack (>= 1.3) rails (6.0.6.1) @@ -419,10 +399,6 @@ GEM rails-i18n (7.0.5) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - rails_real_favicon (0.1.1) - json (>= 1.7, < 3) - rails - rubyzip (~> 2) railties (6.0.6.1) actionpack (= 6.0.6.1) activesupport (= 6.0.6.1) @@ -516,22 +492,11 @@ GEM sanitize (6.0.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) - sass-rails (6.0.0) - sassc-rails (~> 2.1, >= 2.1.1) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt selenium-webdriver (4.5.0) childprocess (>= 0.5, < 5.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.0.0) settings_on_rails (0.3.1) activerecord (>= 3.1) should_not (1.1.0) @@ -585,8 +550,6 @@ GEM rails (>= 3.0.0) tzinfo (1.2.11) thread_safe (~> 0.1) - uglifier (4.2.0) - execjs (>= 0.3.0, < 3) unicode-display_width (2.4.2) uniform_notifier (1.16.0) unread (0.12.0) @@ -623,11 +586,7 @@ DEPENDENCIES activerecord-userstamp! acts_as_tenant after_commit_action - autoprefixer-rails aws-sdk-s3 - bootstrap-sass - bootstrap-sass-extras (>= 0.1.0) - bootstrap_tokenfield_rails bullet (>= 4.14.9) byebug calculated_attributes @@ -651,16 +610,13 @@ DEPENDENCIES filename flamegraph fog-aws (= 3.8.0) - font-awesome-rails friendly_id html-pipeline html-pipeline-rouge_filter! http_accept_language - i18n-js (<= 3.10.0) i18n-tasks image_optim_rails jbuilder - jquery-rails kaminari listen lograge @@ -678,7 +634,6 @@ DEPENDENCIES rails (~> 6.0.6.1) rails-controller-testing rails-html-sanitizer (>= 1.0.4) - rails_real_favicon rails_utils! recaptcha record_tag_helper @@ -694,7 +649,6 @@ DEPENDENCIES rubyzip rwordnet! sanitize (>= 4.6.3) - sass-rails settings_on_rails should_not shoulda-matchers @@ -707,7 +661,6 @@ DEPENDENCIES stackprof traceroute tzinfo-data - uglifier (>= 1.3.0) unread validates_hostname webdrivers diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4fb52c73045..78422cb43ea 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true module ApplicationHelper - include FontAwesome::Rails::IconHelper - include ApplicationAnnouncementsHelper include ApplicationJobsHelper include ApplicationNotificationsHelper @@ -11,74 +9,17 @@ module ApplicationHelper include RouteOverridesHelper include RenderWithinLayoutHelper - # Accesses the header tags specified for the current page - def header_tags(*args, &proc) - content_for(:header_tags, *args, &proc) - end - - # @!method within_head_tag(&proc) - # Adds the given block to the header tags which will be added to the rendered page. - alias_method :within_head_tag, :header_tags - - # Generates a page header. The title shown will be the +.header+ key in the page - # that calls this helper. - # - # @param [String] header The custom page header string. - # @yield A block in which other helper methods may be called, to place child elements - # on the far right of the header. - # @return [String] - def page_header(header = nil) - content_tag(:div, class: 'page-header') do - content_tag(:h1) do - content_tag(:span, header || t('.header')) + - content_tag(:div, class: 'pull-right') do - yield if block_given? - end - end - end - end - - # Generates all page titles, from the reverse breadcrumb if it's available, - # otherwise checks the +content_for?+ Rails helper. Appends the default - # title to everything. - # @return [String] - def page_title - if content_for?(:page_title) - "#{content_for(:page_title)} - " - elsif !breadcrumb_names.empty? - "#{breadcrumb_names.reverse.join(' - ')} - " - else - '' - end + - t('layout.coursemology') + def user_time_zone + user_signed_in? ? current_user.time_zone : nil end - # Returns a meta tag that has the server side context. Now the context contains following info: - # :controller-name The name of the current controller. - # e.g. 'Course::LessonPlanController' will return 'course/lesson_plan' - # :i18n-locale The locale on the server side. - # - # @return [String] The html meta tag. - def server_context_meta_tag - data = { - name: 'server-context', - 'data-controller-name': controller.class.name.sub(/Controller$/, '').underscore, - 'data-i18n-locale': I18n.locale, - 'data-time-zone': ActiveSupport::TimeZone::MAPPING[user_time_zone] - } - - tag(:meta, data) + def url_to_course_logo(course) + asset_url(course_logo_local_url(course)) end - def user_time_zone - user_signed_in? ? current_user.time_zone : nil - end + private - # This helper will includes all webpack assets - def webpack_assets_tag - capture do - concat javascript_pack_tag('vendors~coursemology') - concat javascript_pack_tag('coursemology') - end + def course_logo_local_url(course) + course.logo.medium.url || 'course_default_logo.svg' end end diff --git a/app/helpers/course/controller_helper.rb b/app/helpers/course/controller_helper.rb index 007bc6341b2..2c93dc9fe13 100644 --- a/app/helpers/course/controller_helper.rb +++ b/app/helpers/course/controller_helper.rb @@ -64,17 +64,7 @@ def link_to_user(user, options = {}, &block) end end - def url_to_course_logo(course) - asset_url(course_logo_local_url(course)) - end - def url_to_material(course, folder, material) course_material_folder_material_path(course, folder, material) end - - private - - def course_logo_local_url(course) - course.logo.medium.url || 'course_default_logo.svg' - end end diff --git a/config/environments/production.rb b/config/environments/production.rb index 877de322745..15b8051d7b3 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -31,8 +31,6 @@ # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? - # Compress JavaScripts and CSS. - config.assets.js_compressor = Uglifier.new(harmony: true) # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb deleted file mode 100644 index f7deda5e20f..00000000000 --- a/config/initializers/assets.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true -# Be sure to restart your server when you modify this file. - -# Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = '1.0' - -# Add additional assets to the asset load path. -# Rails.application.config.assets.paths << Emoji.images_path - -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in the app/assets -# folder are already added. -# Rails.application.config.assets.precompile += %w( admin.js admin.css ) From 3039e301a9203a8feceed6c1c0474fdd1b5456d5 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:01:27 +0800 Subject: [PATCH 025/173] feat(application_multitenancy): support multitenancy on development --- app/controllers/concerns/application_multitenancy.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/controllers/concerns/application_multitenancy.rb b/app/controllers/concerns/application_multitenancy.rb index f9714dd8668..6e9c1fb4a8b 100644 --- a/app/controllers/concerns/application_multitenancy.rb +++ b/app/controllers/concerns/application_multitenancy.rb @@ -13,8 +13,7 @@ def deduce_tenant tenant_host = deduce_tenant_host instance = Instance.find_tenant_by_host_or_default(tenant_host) - if Rails.env.production? && instance && instance.default? && - instance.host.casecmp(tenant_host) != 0 + if Rails.env.production? && instance.default? && instance.host.casecmp(tenant_host) != 0 raise ActionController::RoutingError, 'Instance Not Found' end @@ -25,7 +24,13 @@ def deduce_tenant # @return [String] The host, with www removed. def deduce_tenant_host if Rails.env.development? - 'coursemology.org' + default_app_host = Application::Application.config.x.default_app_host + + if request.host.downcase.ends_with?(default_app_host) + request.host.sub(default_app_host, 'coursemology.org') + else + 'coursemology.org' + end elsif request.host.downcase.start_with?('www.') request.host[4..] else From de4767c491375bbbb0b6afec084d12462dafbc25 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:02:01 +0800 Subject: [PATCH 026/173] test(factory/users): support for custom primary `email` --- spec/factories/users.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/factories/users.rb b/spec/factories/users.rb index c3d245f8b88..4a8d23d98b3 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -7,15 +7,22 @@ factory :user, aliases: [:creator, :updater, :actor] do transient do emails_count { 1 } + email { nil } end name role { :normal } - password { 'lolololol' } + password { Application::Application.config.x.default_user_password } after(:build) do |user, evaluator| emails = build_list(:user_email, evaluator.emails_count, primary: false, user: user) - emails.take(1).each { |user_email| user_email.primary = true } + + if (email = evaluator.email) + user.emails << build(:user_email, email: email, primary: true, user: user) + else + emails.take(1).each { |user_email| user_email.primary = true } + end + user.emails.concat(emails) end From 16af8fc9d7422d541b8be471ad5cc008a6087e3d Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:02:44 +0800 Subject: [PATCH 027/173] test(controllers): add `as: :json`, remove HTML tests --- .../application_controller_spec.rb | 5 --- .../course/admin/admin_controller_spec.rb | 2 +- .../assessment/assessments_controller_spec.rb | 30 +++++++++--------- .../forum_post_responses_controller_spec.rb | 11 +++---- .../text_responses_controller_spec.rb | 11 +++---- .../submission/submissions_controller_spec.rb | 2 +- .../assessment/submissions_controller_spec.rb | 4 +-- .../course/courses_controller_spec.rb | 4 +-- .../discussion/topics_controller_spec.rb | 2 +- .../learning_map_controller_spec.rb | 10 +++--- .../course/personal_times_controller_spec.rb | 2 +- .../reference_timelines_controller_spec.rb | 2 +- .../statistics/statistics_controller_spec.rb | 2 +- .../survey/responses_controller_spec.rb | 6 ---- .../course/survey/surveys_controller_spec.rb | 31 ------------------- ...ser_email_subscriptions_controller_spec.rb | 6 ---- .../course/users_controller_spec.rb | 4 +-- .../video_submissions_controller_spec.rb | 2 +- .../system/admin/admin_controller_spec.rb | 2 +- .../system/admin/courses_controller_spec.rb | 2 +- .../admin/instance/admin_controller_spec.rb | 2 +- .../admin/instance/courses_controller_spec.rb | 2 +- .../user_invitations_controller_spec.rb | 2 +- .../admin/instance/users_controller_spec.rb | 2 +- .../system/admin/users_controller_spec.rb | 2 +- .../user/profiles_controller_spec.rb | 12 ++----- .../user/registration_controller_spec.rb | 4 +-- 27 files changed, 52 insertions(+), 114 deletions(-) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 1dd003a9dd7..e0bf616cd63 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -121,11 +121,6 @@ def publicly_accessible? context 'when the action raises a CanCan::AccessDenied' do run_rescue - it 'renders the access denied page to /pages/403' do - post :create - expect(response).to render_template('pages/403') - end - it 'returns HTTP status 403' do post :create expect(response.status).to eq(403) diff --git a/spec/controllers/course/admin/admin_controller_spec.rb b/spec/controllers/course/admin/admin_controller_spec.rb index d0e63cb3716..be560d104e0 100644 --- a/spec/controllers/course/admin/admin_controller_spec.rb +++ b/spec/controllers/course/admin/admin_controller_spec.rb @@ -8,7 +8,7 @@ before { sign_in(user) } describe '#index' do - subject { get :index, params: { course_id: course } } + subject { get :index, as: :json, params: { course_id: course } } context 'when the user is a Course Manager' do let(:user) { create(:course_manager, course: course).user } diff --git a/spec/controllers/course/assessment/assessments_controller_spec.rb b/spec/controllers/course/assessment/assessments_controller_spec.rb index 6f8e061d651..c8d4b2e2a35 100644 --- a/spec/controllers/course/assessment/assessments_controller_spec.rb +++ b/spec/controllers/course/assessment/assessments_controller_spec.rb @@ -20,27 +20,25 @@ describe '#index' do context 'when a category is given' do before do - post :index, - params: { - course_id: course, - id: immutable_assessment, - assessment: { title: '' }, - category: category - } + post :index, as: :json, params: { + course_id: course, + id: immutable_assessment, + assessment: { title: '' }, + category: category + } end it { expect(controller.instance_variable_get(:@category)).to eq(category) } end context 'when a tab is given' do before do - post :index, - params: { - course_id: course, - id: immutable_assessment, - assessment: { title: '' }, - category: category, - tab: tab - } + post :index, as: :json, params: { + course_id: course, + id: immutable_assessment, + assessment: { title: '' }, + category: category, + tab: tab + } end it { expect(controller.instance_variable_get(:@tab)).to eq(tab) } end @@ -53,7 +51,7 @@ assessment end - subject { get :edit, params: { course_id: course, id: assessment } } + subject { get :edit, as: :json, params: { course_id: course, id: assessment } } context 'when edit page is loaded' do it 'sanitizes the description text' do diff --git a/spec/controllers/course/assessment/question/forum_post_responses_controller_spec.rb b/spec/controllers/course/assessment/question/forum_post_responses_controller_spec.rb index 1faaa15bdac..9eb20c0e12b 100644 --- a/spec/controllers/course/assessment/question/forum_post_responses_controller_spec.rb +++ b/spec/controllers/course/assessment/question/forum_post_responses_controller_spec.rb @@ -51,12 +51,11 @@ end subject do - get :edit, - params: { - course_id: course, - assessment_id: assessment, - id: forum_post_response - } + get :edit, as: :json, params: { + course_id: course, + assessment_id: assessment, + id: forum_post_response + } end context 'when edit page is loaded' do diff --git a/spec/controllers/course/assessment/question/text_responses_controller_spec.rb b/spec/controllers/course/assessment/question/text_responses_controller_spec.rb index ac39d068d9d..37aa04a24f2 100644 --- a/spec/controllers/course/assessment/question/text_responses_controller_spec.rb +++ b/spec/controllers/course/assessment/question/text_responses_controller_spec.rb @@ -51,12 +51,11 @@ end subject do - get :edit, - params: { - course_id: course, - assessment_id: assessment, - id: text_response - } + get :edit, as: :json, params: { + course_id: course, + assessment_id: assessment, + id: text_response + } end context 'when edit page is loaded' do diff --git a/spec/controllers/course/assessment/submission/submissions_controller_spec.rb b/spec/controllers/course/assessment/submission/submissions_controller_spec.rb index 5626e52aa99..e9560fb2246 100644 --- a/spec/controllers/course/assessment/submission/submissions_controller_spec.rb +++ b/spec/controllers/course/assessment/submission/submissions_controller_spec.rb @@ -150,7 +150,7 @@ describe '#extract_instance_variables' do subject do - get :edit, params: { course_id: course, assessment_id: assessment, id: immutable_submission } + get :edit, as: :json, params: { course_id: course, assessment_id: assessment, id: immutable_submission } end it 'extracts instance variables from services' do diff --git a/spec/controllers/course/assessment/submissions_controller_spec.rb b/spec/controllers/course/assessment/submissions_controller_spec.rb index b5a58a3f959..0979b2edd42 100644 --- a/spec/controllers/course/assessment/submissions_controller_spec.rb +++ b/spec/controllers/course/assessment/submissions_controller_spec.rb @@ -13,7 +13,7 @@ describe '#index' do context 'when no category is specified' do - before { get :index, params: { course_id: course } } + before { get :index, as: :json, params: { course_id: course } } it 'sets the category to the first category' do first_category = course.assessment_categories.first @@ -29,7 +29,7 @@ let!(:submission) do create(:submission, :published, creator: student, assessment: assessment) end - before { get :index, params: { course_id: course, category: category } } + before { get :index, as: :json, params: { course_id: course, category: category } } it 'sets the category to be the specified category' do expect(controller.instance_variable_get(:@category)).to eq(category) diff --git a/spec/controllers/course/courses_controller_spec.rb b/spec/controllers/course/courses_controller_spec.rb index 919cf74ca4f..756878bef6d 100644 --- a/spec/controllers/course/courses_controller_spec.rb +++ b/spec/controllers/course/courses_controller_spec.rb @@ -23,7 +23,7 @@ describe '#index' do context 'when there is no user logged in' do it 'allows unauthenticated access' do - get :index + get :index, as: :json expect(response).to be_successful end end @@ -33,7 +33,7 @@ it 'allows access' do sign_in(user) - get :index + get :index, as: :json expect(response).to be_successful end end diff --git a/spec/controllers/course/discussion/topics_controller_spec.rb b/spec/controllers/course/discussion/topics_controller_spec.rb index 001e78ef3d6..f9092d44038 100644 --- a/spec/controllers/course/discussion/topics_controller_spec.rb +++ b/spec/controllers/course/discussion/topics_controller_spec.rb @@ -19,7 +19,7 @@ let(:topics) { controller.instance_variable_get(:@topics) } describe '#index' do - subject { get :index, params: { course_id: course } } + subject { get :index, as: :json, params: { course_id: course } } context 'when a course staff visits the page' do before { sign_in(staff) } diff --git a/spec/controllers/course/learning_map/learning_map_controller_spec.rb b/spec/controllers/course/learning_map/learning_map_controller_spec.rb index d3567d07f00..98b28597e2c 100644 --- a/spec/controllers/course/learning_map/learning_map_controller_spec.rb +++ b/spec/controllers/course/learning_map/learning_map_controller_spec.rb @@ -16,7 +16,7 @@ before do allow(controller).to receive_message_chain('current_component_host.[]').and_return(nil) end - subject { get :index, params: { course_id: course.id } } + subject { get :index, as: :json, params: { course_id: course.id } } it 'raises a component not found error' do expect { subject }.to raise_error(ComponentNotFoundError) end @@ -27,7 +27,7 @@ let!(:achievement2) { create(:course_achievement, course: course) } subject do - post :add_parent_node, params: { + post :add_parent_node, as: :json, params: { course_id: course.id, parent_node_id: "achievement-#{achievement1.id}", node_id: "achievement-#{achievement2.id}" } @@ -52,7 +52,7 @@ end subject do - post :remove_parent_node, params: { + post :remove_parent_node, as: :json, params: { course_id: course.id, parent_node_id: "achievement-#{achievement1.id}", node_id: "achievement-#{achievement2.id}" } @@ -74,7 +74,7 @@ satisfiability_type: :all_conditions) end subject do - post :toggle_satisfiability_type, params: { + post :toggle_satisfiability_type, as: :json, params: { course_id: course.id, node_id: "achievement-#{achievement.id}" } end @@ -94,7 +94,7 @@ satisfiability_type: :at_least_one_condition) end subject do - post :toggle_satisfiability_type, params: { + post :toggle_satisfiability_type, as: :json, params: { course_id: course.id, node_id: "achievement-#{achievement.id}" } end diff --git a/spec/controllers/course/personal_times_controller_spec.rb b/spec/controllers/course/personal_times_controller_spec.rb index e26c0d91e8a..42bf399b050 100644 --- a/spec/controllers/course/personal_times_controller_spec.rb +++ b/spec/controllers/course/personal_times_controller_spec.rb @@ -8,7 +8,7 @@ let(:course_user) { create(:course_user, course: course) } describe '#index' do - subject { get :index, params: { course_id: course, user_id: course_user } } + subject { get :index, as: :json, params: { course_id: course, user_id: course_user } } context 'when a Normal User visits the page' do let(:user) { create(:user) } diff --git a/spec/controllers/course/reference_timelines_controller_spec.rb b/spec/controllers/course/reference_timelines_controller_spec.rb index f04506630b2..b0318215e89 100644 --- a/spec/controllers/course/reference_timelines_controller_spec.rb +++ b/spec/controllers/course/reference_timelines_controller_spec.rb @@ -13,7 +13,7 @@ before { sign_in(user) } describe '#index' do - subject { get :index, params: { course_id: course } } + subject { get :index, as: :json, params: { course_id: course } } context 'when the user is a manager of the course' do let(:user) { create(:course_manager, course: course).user } diff --git a/spec/controllers/course/statistics/statistics_controller_spec.rb b/spec/controllers/course/statistics/statistics_controller_spec.rb index 06e3499b070..0c8ff3be76d 100644 --- a/spec/controllers/course/statistics/statistics_controller_spec.rb +++ b/spec/controllers/course/statistics/statistics_controller_spec.rb @@ -9,7 +9,7 @@ let(:course_user) { create(:course_user, course: course) } describe '#index' do - subject { get :index, params: { course_id: course, user_id: course_user } } + subject { get :index, as: :json, params: { course_id: course, user_id: course_user } } context 'when a Normal User visits the page' do let(:user) { create(:user) } diff --git a/spec/controllers/course/survey/responses_controller_spec.rb b/spec/controllers/course/survey/responses_controller_spec.rb index 4264d5774a7..4293b993b38 100644 --- a/spec/controllers/course/survey/responses_controller_spec.rb +++ b/spec/controllers/course/survey/responses_controller_spec.rb @@ -32,12 +32,6 @@ describe '#index' do let(:user) { create(:administrator) } - context 'when html page is requested' do - subject { get :index, params: { course_id: course.id, survey_id: survey.id } } - - it { is_expected.to render_template('index') } - end - context 'when json data is requested' do render_views subject { get :index, params: { format: :json, course_id: course.id, survey_id: survey.id } } diff --git a/spec/controllers/course/survey/surveys_controller_spec.rb b/spec/controllers/course/survey/surveys_controller_spec.rb index 2d3b402e9e7..dfa7ef62701 100644 --- a/spec/controllers/course/survey/surveys_controller_spec.rb +++ b/spec/controllers/course/survey/surveys_controller_spec.rb @@ -33,24 +33,6 @@ describe '#index' do let!(:published_survey) { create(:survey, :published, course: course) } - context 'when html page is requested' do - let(:user) { student.user } - subject { get :index, params: { course_id: course.id } } - - it { is_expected.to render_template('index') } - - context 'when survey component is disabled' do - before do - allow(controller). - to receive_message_chain('current_component_host.[]').and_return(nil) - end - - it 'raises an component not found error' do - expect { subject }.to raise_error(ComponentNotFoundError) - end - end - end - context 'when json data is requested' do render_views subject { get :index, as: :json, params: { course_id: course.id } } @@ -100,13 +82,6 @@ describe '#show' do let(:survey_traits) { :published } - context 'when html page is requested' do - let(:user) { student.user } - subject { get :show, params: { course_id: course.id, id: survey.id } } - - it { is_expected.to render_template('index') } - end - context 'when json data is requested' do render_views let(:user) { manager.user } @@ -223,12 +198,6 @@ describe '#results' do let(:user) { admin } - context 'when html page is requested' do - subject { get :results, params: { course_id: course.id, id: survey.id } } - - it { is_expected.to render_template('index') } - end - context 'when json data is requested' do render_views let(:response_traits) { :submitted } diff --git a/spec/controllers/course/user_email_subscriptions_controller_spec.rb b/spec/controllers/course/user_email_subscriptions_controller_spec.rb index 604e081212e..b4643228f5d 100644 --- a/spec/controllers/course/user_email_subscriptions_controller_spec.rb +++ b/spec/controllers/course/user_email_subscriptions_controller_spec.rb @@ -11,12 +11,6 @@ let!(:student) { create(:course_student, course: course) } let(:json_response) { JSON.parse(response.body) } - describe '#edit html' do - before { sign_in(staff.user) } - subject { get :edit, params: { course_id: course, user_id: staff } } - it { is_expected.to render_template(:edit) } - end - describe '#edit json' do before { sign_in(student.user) } context 'when an unsubscription link for surveys closing reminder is clicked' do diff --git a/spec/controllers/course/users_controller_spec.rb b/spec/controllers/course/users_controller_spec.rb index 438498be536..39b486869b4 100644 --- a/spec/controllers/course/users_controller_spec.rb +++ b/spec/controllers/course/users_controller_spec.rb @@ -15,7 +15,7 @@ describe '#students' do before { sign_in(user) } - subject { get :students, params: { course_id: course } } + subject { get :students, as: :json, params: { course_id: course } } context 'when a course manager visits the page' do let!(:course_lecturer) { create(:course_manager, course: course, user: user) } @@ -35,7 +35,7 @@ describe '#staff' do before { sign_in(user) } - subject { get :staff, params: { course_id: course } } + subject { get :staff, as: :json, params: { course_id: course } } context 'when a course manager visits the page' do let!(:course_lecturer) { create(:course_manager, course: course, user: user) } diff --git a/spec/controllers/course/video_submissions_controller_spec.rb b/spec/controllers/course/video_submissions_controller_spec.rb index 71d5120080b..8702d60d03e 100644 --- a/spec/controllers/course/video_submissions_controller_spec.rb +++ b/spec/controllers/course/video_submissions_controller_spec.rb @@ -8,7 +8,7 @@ let!(:course_user) { create(:course_user, course: course) } describe '#index' do - subject { get :index, params: { course_id: course, user_id: course_user } } + subject { get :index, as: :json, params: { course_id: course, user_id: course_user } } before { sign_in(user) } context 'when a Normal User visits the page' do diff --git a/spec/controllers/system/admin/admin_controller_spec.rb b/spec/controllers/system/admin/admin_controller_spec.rb index 40c5486646c..044b056abbf 100644 --- a/spec/controllers/system/admin/admin_controller_spec.rb +++ b/spec/controllers/system/admin/admin_controller_spec.rb @@ -9,7 +9,7 @@ before { sign_in(user) } describe '#index' do - before { get :index } + before { get :index, as: :json } it { is_expected.to render_template('index') } end end diff --git a/spec/controllers/system/admin/courses_controller_spec.rb b/spec/controllers/system/admin/courses_controller_spec.rb index f1c6bb73f32..651ab76ef8a 100644 --- a/spec/controllers/system/admin/courses_controller_spec.rb +++ b/spec/controllers/system/admin/courses_controller_spec.rb @@ -10,7 +10,7 @@ let(:normal_user) { create(:user) } describe '#index' do - subject { get :index } + subject { get :index, as: :json } context 'when a system administrator visits the page' do before { sign_in(admin) } diff --git a/spec/controllers/system/admin/instance/admin_controller_spec.rb b/spec/controllers/system/admin/instance/admin_controller_spec.rb index bc7717018ea..90a90303da1 100644 --- a/spec/controllers/system/admin/instance/admin_controller_spec.rb +++ b/spec/controllers/system/admin/instance/admin_controller_spec.rb @@ -9,7 +9,7 @@ before { sign_in(user) } describe '#index' do - subject { get :index } + subject { get :index, as: :json } context 'when a system administrator visits the page' do let(:user) { create(:administrator) } it { is_expected.to render_template(:index) } diff --git a/spec/controllers/system/admin/instance/courses_controller_spec.rb b/spec/controllers/system/admin/instance/courses_controller_spec.rb index 7bbdc7e1851..2a1bf81dba0 100644 --- a/spec/controllers/system/admin/instance/courses_controller_spec.rb +++ b/spec/controllers/system/admin/instance/courses_controller_spec.rb @@ -9,7 +9,7 @@ let(:normal_user) { create(:user) } describe '#index' do - subject { get :index } + subject { get :index, as: :json } context 'when an administrator visits the page' do before { sign_in(instance_admin) } diff --git a/spec/controllers/system/admin/instance/user_invitations_controller_spec.rb b/spec/controllers/system/admin/instance/user_invitations_controller_spec.rb index edcae37efa0..67cb7ea7acd 100644 --- a/spec/controllers/system/admin/instance/user_invitations_controller_spec.rb +++ b/spec/controllers/system/admin/instance/user_invitations_controller_spec.rb @@ -8,7 +8,7 @@ let(:normal_user) { create(:user) } describe '#index' do - subject { get :index } + subject { get :index, as: :json } context 'when a system administrator visits the page' do before { sign_in(instance_admin) } diff --git a/spec/controllers/system/admin/instance/users_controller_spec.rb b/spec/controllers/system/admin/instance/users_controller_spec.rb index 57536349ce7..29c14f43571 100644 --- a/spec/controllers/system/admin/instance/users_controller_spec.rb +++ b/spec/controllers/system/admin/instance/users_controller_spec.rb @@ -9,7 +9,7 @@ let(:normal_user) { create(:user) } describe '#index' do - subject { get :index } + subject { get :index, as: :json } context 'when an instance administrator visits the page' do before { sign_in(instance_admin) } diff --git a/spec/controllers/system/admin/users_controller_spec.rb b/spec/controllers/system/admin/users_controller_spec.rb index 0f6fc09374d..aef5f9f4d2c 100644 --- a/spec/controllers/system/admin/users_controller_spec.rb +++ b/spec/controllers/system/admin/users_controller_spec.rb @@ -10,7 +10,7 @@ let(:normal_user) { create(:user) } describe '#index' do - subject { get :index } + subject { get :index, as: :json } context 'when a system administrator visits the page' do before { sign_in(admin) } diff --git a/spec/controllers/user/profiles_controller_spec.rb b/spec/controllers/user/profiles_controller_spec.rb index 4bd5ffbeedc..4173852b8e3 100644 --- a/spec/controllers/user/profiles_controller_spec.rb +++ b/spec/controllers/user/profiles_controller_spec.rb @@ -8,22 +8,18 @@ let!(:user) { create(:user) } describe '#edit' do - subject { get :edit } + subject { get :edit, as: :json } context 'when user is signed in' do before { sign_in(user) } it { is_expected.to render_template(:edit) } end - - context 'when user is not signed in' do - it { is_expected.to redirect_to(new_user_session_path) } - end end describe '#update' do let(:new_name) { 'New Name' } - subject { patch :update, params: { user: { name: new_name } } } + subject { patch :update, as: :json, params: { user: { name: new_name } } } context 'when user is signed in' do before { sign_in(user) } @@ -33,10 +29,6 @@ expect(user.reload.name).to eq(new_name) end end - - context 'when user is not signed in' do - it { is_expected.to redirect_to(new_user_session_path) } - end end end end diff --git a/spec/controllers/user/registration_controller_spec.rb b/spec/controllers/user/registration_controller_spec.rb index b45ab66829d..92f60b4d3ba 100644 --- a/spec/controllers/user/registration_controller_spec.rb +++ b/spec/controllers/user/registration_controller_spec.rb @@ -34,11 +34,9 @@ @request.env['devise.mapping'] = Devise.mappings[:user] end - it 'flashes error message and no new user is registered' do + it 'does not register any new users' do allow(controller).to receive(:verify_recaptcha).and_return(false) expect { subject }.to change { User.count }.by(0) - expect(flash[:alert]).to be_present - expect(response).to render_template(:new) end end end From 1932d61e43db0176e76d3c221fb084a1d6e899c1 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:03:28 +0800 Subject: [PATCH 028/173] test(forum_post_response_answer): fix `submission` empty --- .../assessment/answer/forum_post_response_answer_spec.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spec/features/course/assessment/answer/forum_post_response_answer_spec.rb b/spec/features/course/assessment/answer/forum_post_response_answer_spec.rb index 01b8335a9cd..f249f6ba8bd 100644 --- a/spec/features/course/assessment/answer/forum_post_response_answer_spec.rb +++ b/spec/features/course/assessment/answer/forum_post_response_answer_spec.rb @@ -8,12 +8,9 @@ with_tenant(:instance) do let(:course) { create(:course) } let(:assessment) { create(:assessment, :published_with_forum_post_response_question, course: course) } - before { login_as(user, scope: :user) } + let(:submission) { create(:submission, :attempting, assessment: assessment, creator: user) } - let(:submission) do - create(:submission, *submission_traits, assessment: assessment, creator: user) - end - let(:submission_traits) { nil } + before { login_as(user, scope: :user) } context 'As a Course Student' do let(:user) { create(:course_student, course: course).user } From fbd2319ce56ce986ccf6c395e7b04b398b00f746 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:04:09 +0800 Subject: [PATCH 029/173] test(programming_answer): rewrite `pending` tests --- .../answer/programming_answer_spec.rb | 110 ++++++------------ 1 file changed, 34 insertions(+), 76 deletions(-) diff --git a/spec/features/course/assessment/answer/programming_answer_spec.rb b/spec/features/course/assessment/answer/programming_answer_spec.rb index ff9e1fa14a8..63389efccc8 100644 --- a/spec/features/course/assessment/answer/programming_answer_spec.rb +++ b/spec/features/course/assessment/answer/programming_answer_spec.rb @@ -1,119 +1,77 @@ # frozen_string_literal: true require 'rails_helper' -RSpec.describe 'Course: Assessments: Submissions: Programming Answers' do +RSpec.describe 'Course: Assessments: Submissions: Programming Answers', js: true do let(:instance) { Instance.default } with_tenant(:instance) do let(:course) { create(:course) } let(:assessment) { create(:assessment, :published_with_programming_question, course: course) } - let(:assessment2) { create(:assessment, :published_with_programming_question, course: course) } - let(:submission) do - create(:submission, *submission_traits, assessment: assessment, creator: user) - end - let(:submission2) do - create(:submission, *submission_traits2, assessment: assessment2, creator: user) - end + let(:submission) { create(:submission, *submission_traits, assessment: assessment, creator: user) } let(:submission_traits) { nil } - let(:submission_traits2) { nil } before { login_as(user, scope: :user) } context 'As a Course Student' do let(:user) { create(:course_student, course: course).user } + let(:submission_traits) { :attempting } + let(:answer_code) { 'this is a testing code whatever lol' } - scenario 'I can save my submission', js: true do - pending 'Removed add/delete file links for CS1010S' + scenario 'I can save my submission' do visit edit_course_assessment_submission_path(course, assessment, submission) - # Fill in every single successive item - within find(content_tag_selector(submission.answers.first)) do - all('div.files div.nested-fields').each_with_index do |file, i| - within file do - fill_in 'filename', with: "test #{i}.py" - end - end - end - - click_button I18n.t('course.assessment.submission.submissions.buttons.save') - expect(current_path).to eq( - edit_course_assessment_submission_path(course, assessment, submission) - ) + find('div', class: 'ace_editor').click + send_keys answer_code + click_button 'Save Draft' + wait_for_page - submission.answers.first.specific.files.reload.each_with_index do |_, i| - expect(page).to have_field('filename', with: "test #{i}.py") - end - - # Add a new file - click_link(I18n.t('course.assessment.answer.programming.programming.add_file')) - new_file_name = 'new_file.py' - expected_files = [new_file_name] + submission.answers.first.specific.files.map(&:filename) - within find(content_tag_selector(submission.answers.first)) do - within all('div.files div.nested-fields').first do - fill_in 'filename', with: new_file_name - end - end - click_button I18n.t('course.assessment.submission.submissions.buttons.save') - - expected_files.each do |file| - expect(page).to have_field('filename', with: file) - end - - # Delete files - within find(content_tag_selector(submission.answers.first)) do - all(:link, I18n.t('course.assessment.answer.programming.file_fields.delete')). - each(&:click) - end - click_button I18n.t('course.assessment.submission.submissions.buttons.save') - - expect(page).not_to have_field('filename') + file = submission.answers.first.specific.files.reload.first + expect(file.content).to eq(answer_code) end - pending 'I can only see public test cases but cannot update my finalized submission ' do + scenario 'I can only see public test cases but cannot update my finalized submission ' do create(:course_assessment_question_programming, assessment: assessment, test_case_count: 1, private_test_case_count: 1, evaluation_test_case_count: 1) visit edit_course_assessment_submission_path(course, assessment, submission) - expect(page).to have_selector('.code') - click_button I18n.t('course.assessment.submission.submissions.buttons.finalise') + expect(page).to have_selector('.ace_editor') - within find(content_tag_selector(submission.answers.first)) do - expect(page).not_to have_selector('.code') - end + click_button 'Finalise Submission' + click_button 'Continue' + + expect(page).not_to have_selector('.ace_editor') - # Check that student can only see public but not private and evalution test cases. - expect(page). - to have_text(I18n.t('course.assessment.answer.programming.test_cases.public')) - expect(page). - not_to have_text(I18n.t('course.assessment.answer.programming.test_cases.private')) - expect(page). - not_to have_text(I18n.t('course.assessment.answer.programming.test_cases.evaluation')) + expect(page).to have_text('Public Test Cases') + expect(page).not_to have_text('Private Test Cases') + expect(page).not_to have_text('Evaluation Test Cases') end end context 'As Course Staff' do let(:user) { create(:course_teaching_assistant, course: course).user } - let(:submission_traits) { :submitted } - let(:submission_traits2) { :attempting } - pending 'I can view the test cases' do - # View test cases for submitted submission - visit edit_course_assessment_submission_path(course, assessment, submission) + context 'when submission is submitted' do + let(:submission_traits) { :submitted } + + scenario 'I can view the test cases' do + visit edit_course_assessment_submission_path(course, assessment, submission) - within find(content_tag_selector(submission.answers.first)) do assessment.questions.first.actable.test_cases.each do |test_case| - expect(page).to have_content_tag_for(test_case) + expect(page).to have_text(test_case.identifier) end end + end - # View test cases for attempting submission - visit edit_course_assessment_submission_path(course, assessment_2, submission2) + context 'when submission is attempting' do + let(:submission) { create(:submission, :attempting, assessment: assessment, creator: user) } - within find(content_tag_selector(submission2.answers.first)) do - assessment_2.questions.first.actable.test_cases.each do |solution| - expect(page).to have_content_tag_for(solution) + scenario 'I can view the test cases' do + visit edit_course_assessment_submission_path(course, assessment, submission) + + assessment.questions.first.actable.test_cases.each do |test_case| + expect(page).to have_text(test_case.identifier) end end end From 8be353eed6e3c65f6165c9a4df62a20e518a89d3 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:04:43 +0800 Subject: [PATCH 030/173] test(text_response_answer): fix `submission` empty --- .../answer/text_response_answer_spec.rb | 77 +++++++++---------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/spec/features/course/assessment/answer/text_response_answer_spec.rb b/spec/features/course/assessment/answer/text_response_answer_spec.rb index a534a227e10..f0913f4a316 100644 --- a/spec/features/course/assessment/answer/text_response_answer_spec.rb +++ b/spec/features/course/assessment/answer/text_response_answer_spec.rb @@ -6,60 +6,59 @@ with_tenant(:instance) do let(:course) { create(:course) } - let(:assessment) { create(:assessment, :published_with_text_response_question, course: course) } - before { login_as(user, scope: :user) } + let(:user) { create(:course_student, course: course).user } + let(:submission) { create(:submission, :attempting, assessment: assessment, creator: user) } - let(:submission) do - create(:submission, *submission_traits, assessment: assessment, creator: user) - end - let(:submission_traits) { nil } + before { login_as(user, scope: :user) } context 'As a Course Student' do - let(:user) { create(:course_student, course: course).user } - let(:file_path) do - File.join(Rails.root, '/spec/fixtures/files/text.txt') - end + let(:file_path) { File.join(Rails.root, '/spec/fixtures/files/text.txt') } - scenario 'I cannot update my submission after finalising' do - visit edit_course_assessment_submission_path(course, assessment, submission) + context 'when it is a file upload question' do + let(:assessment) { create(:assessment, :published_with_text_response_question, course: course) } - answer_id = submission.answers.first.id - find_field(name: "#{answer_id}.answer_text").set('Test') - click_button('Finalise Submission') - accept_confirm_dialog do - wait_for_job + scenario 'I cannot update my submission after finalising' do + visit edit_course_assessment_submission_path(course, assessment, submission) + + answer_id = submission.answers.first.id + find_field(name: "#{answer_id}.answer_text").set('Test') + click_button('Finalise Submission') + accept_confirm_dialog do + wait_for_job + end + expect(page).not_to have_field(name: "#{answer_id}[answer_text]") end - expect(page).not_to have_field(name: "#{answer_id}[answer_text]") - end - scenario 'I upload an attachment to the answer' do - visit edit_course_assessment_submission_path(course, assessment, submission) - answer = submission.answers.last - file_view = find('strong', text: 'Uploaded Files:').find(:xpath, '..') - dropzone = find('.dropzone-input') - file_input = dropzone.find('input', visible: false) + scenario 'I upload an attachment to the answer' do + visit edit_course_assessment_submission_path(course, assessment, submission) + answer = submission.answers.last + file_view = all('div', text: 'Uploaded Files').last + dropzone = find('.dropzone-input') + file_input = dropzone.find('input', visible: false) - file_input.set(file_path) + file_input.set(file_path) - # The file should show in the dropzone - expect(dropzone).to have_css('span', text: 'text.txt') + # The file should show in the dropzone + expect(dropzone).to have_css('span', text: 'text.txt') - click_button('Save Draft') + click_button('Save Draft') - expect(dropzone).to have_no_css('span', text: 'text.txt') - expect(file_view).to have_css('span', text: 'text.txt') - expect(file_view).to have_css('span', count: 2) - expect(answer.specific.attachments).not_to be_empty + expect(dropzone).to have_no_css('span', text: 'text.txt') + expect(file_view).to have_css('span', text: 'text.txt') + expect(file_view).to have_css('span', count: 2) + expect(answer.specific.attachments).not_to be_empty + end end - scenario 'I cannot see the text box for a file upload question' do - assessment = create(:assessment, :published_with_file_upload_question, course: course) - submission = create(:submission, assessment: assessment, creator: user) + context 'when it is a text response question' do + let(:assessment) { create(:assessment, :published_with_file_upload_question, course: course) } - visit edit_course_assessment_submission_path(course, assessment, submission) + scenario 'I cannot see the text box for a file upload question' do + visit edit_course_assessment_submission_path(course, assessment, submission) - file_upload_answer = submission.answers.first - expect(page).not_to have_field(name: "#{file_upload_answer.id}[answer_text]") + file_upload_answer = submission.answers.first + expect(page).not_to have_field(name: "#{file_upload_answer.id}[answer_text]") + end end end end From dd56d75bbb80d4ecfcb16e252a8990dac3bd35c5 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:05:03 +0800 Subject: [PATCH 031/173] test(capybara): add `expect_forbidden` --- spec/support/capybara.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index a75dd627730..cf5fa6b3c09 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -140,6 +140,9 @@ def find_sidebar all('aside').first end + def expect_forbidden + expect(page).to have_content("You don't have permission to access") + end def confirm_registration_token_via_email token = ActionMailer::Base.deliveries.last.body.match(/confirmation_token=.*(?=")/) From 68cdd16929197a782ac7893ffa74fd4aa4f54856 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:07:43 +0800 Subject: [PATCH 032/173] test: replace 403 expectations with `expect_forbidden` --- .../question/forum_post_response_management_spec.rb | 4 ++-- .../question/multiple_response_management_spec.rb | 4 ++-- .../assessment/question/programming_management_spec.rb | 4 ++-- .../assessment/question/text_response_management_spec.rb | 4 ++-- .../assessment/question/voice_response_management_spec.rb | 4 ++-- spec/features/course/duplication_spec.rb | 4 ++-- spec/features/course/staff_management_spec.rb | 5 +++-- spec/features/course/students_statistics_spec.rb | 6 ++++++ spec/features/system/admin/masquerades_spec.rb | 6 ++---- 9 files changed, 23 insertions(+), 18 deletions(-) diff --git a/spec/features/course/assessment/question/forum_post_response_management_spec.rb b/spec/features/course/assessment/question/forum_post_response_management_spec.rb index 97434ca9f30..b8d72ccfd95 100644 --- a/spec/features/course/assessment/question/forum_post_response_management_spec.rb +++ b/spec/features/course/assessment/question/forum_post_response_management_spec.rb @@ -106,10 +106,10 @@ context 'As a Student' do let(:user) { create(:course_student, course: course).user } - scenario 'I cannot add questions', js: false do + scenario 'I cannot add questions' do visit new_course_assessment_question_forum_post_response_path(course, assessment) - expect(page.status_code).to eq(403) + expect_forbidden end end end diff --git a/spec/features/course/assessment/question/multiple_response_management_spec.rb b/spec/features/course/assessment/question/multiple_response_management_spec.rb index 585db80e1ad..4e1e705a938 100644 --- a/spec/features/course/assessment/question/multiple_response_management_spec.rb +++ b/spec/features/course/assessment/question/multiple_response_management_spec.rb @@ -212,10 +212,10 @@ context 'As a Student' do let(:user) { create(:course_student, course: course).user } - scenario 'I cannot add questions', js: false do + scenario 'I cannot add questions' do visit new_course_assessment_question_multiple_response_path(course, assessment) - expect(page.status_code).to eq(403) + expect_forbidden end end end diff --git a/spec/features/course/assessment/question/programming_management_spec.rb b/spec/features/course/assessment/question/programming_management_spec.rb index d398d5bae91..036ae690e3c 100644 --- a/spec/features/course/assessment/question/programming_management_spec.rb +++ b/spec/features/course/assessment/question/programming_management_spec.rb @@ -269,10 +269,10 @@ context 'As a Student' do let(:user) { create(:course_student, course: course).user } - scenario 'I cannot add questions', js: false do + scenario 'I cannot add questions' do visit new_course_assessment_question_programming_path(course, assessment) - expect(page.status_code).to eq(403) + expect_forbidden end end end diff --git a/spec/features/course/assessment/question/text_response_management_spec.rb b/spec/features/course/assessment/question/text_response_management_spec.rb index 905044dbbdd..8808f6db3e3 100644 --- a/spec/features/course/assessment/question/text_response_management_spec.rb +++ b/spec/features/course/assessment/question/text_response_management_spec.rb @@ -163,10 +163,10 @@ context 'As a Student' do let(:user) { create(:course_student, course: course).user } - scenario 'I cannot add questions', js: false do + scenario 'I cannot add questions' do visit new_course_assessment_question_text_response_path(course, assessment) - expect(page.status_code).to eq(403) + expect_forbidden end end end diff --git a/spec/features/course/assessment/question/voice_response_management_spec.rb b/spec/features/course/assessment/question/voice_response_management_spec.rb index 611e7f094b9..e46f7c68442 100644 --- a/spec/features/course/assessment/question/voice_response_management_spec.rb +++ b/spec/features/course/assessment/question/voice_response_management_spec.rb @@ -81,10 +81,10 @@ context 'As a Student' do let(:user) { create(:course_student, course: course).user } - scenario 'I cannot add questions', js: false do + scenario 'I cannot add questions' do visit new_course_assessment_question_voice_response_path(course, assessment) - expect(page.status_code).to eq(403) + expect_forbidden end end end diff --git a/spec/features/course/duplication_spec.rb b/spec/features/course/duplication_spec.rb index 6d1462f75fd..9f28f8aa0e1 100644 --- a/spec/features/course/duplication_spec.rb +++ b/spec/features/course/duplication_spec.rb @@ -114,10 +114,10 @@ expect(find_sidebar).not_to have_text(I18n.t('layouts.duplication.title')) end - scenario 'I cannot access the duplication page', js: false do + scenario 'I cannot access the duplication page' do visit course_duplication_path(course) - expect(page.status_code).to eq(403) + expect_forbidden end end end diff --git a/spec/features/course/staff_management_spec.rb b/spec/features/course/staff_management_spec.rb index 23ee632374f..4f9daf754c7 100644 --- a/spec/features/course/staff_management_spec.rb +++ b/spec/features/course/staff_management_spec.rb @@ -30,9 +30,10 @@ expect(find_sidebar).to have_text(I18n.t('layouts.course_users.title')) end - scenario 'I cannot access the staff list', js: false do + scenario 'I cannot access the staff list' do visit course_users_staff_path(course) - expect(page.status_code).to eq(403) + + expect_forbidden end end diff --git a/spec/features/course/students_statistics_spec.rb b/spec/features/course/students_statistics_spec.rb index 6cf2cd77089..ba8ae24e71f 100644 --- a/spec/features/course/students_statistics_spec.rb +++ b/spec/features/course/students_statistics_spec.rb @@ -64,6 +64,12 @@ expect(page).not_to have_selector('li', text: I18n.t('course.statistics.header')) end + + scenario 'I cannot access the statistics page' do + visit course_statistics_path(course) + + expect_forbidden + end end end end diff --git a/spec/features/system/admin/masquerades_spec.rb b/spec/features/system/admin/masquerades_spec.rb index ec84113692d..ff592e9690f 100644 --- a/spec/features/system/admin/masquerades_spec.rb +++ b/spec/features/system/admin/masquerades_spec.rb @@ -28,8 +28,7 @@ scenario 'I cannot masquerade a user' do visit masquerade_path(user_to_masquerade) - expect(page).not_to have_selector('li', text: user_to_masquerade.name) - expect(page).to have_selector('div', text: 'pages.403.header') + expect_forbidden end end @@ -39,8 +38,7 @@ scenario 'I cannot masquerade a user' do visit masquerade_path(user_to_masquerade) - expect(page).not_to have_selector('li', text: user_to_masquerade.name) - expect(page).to have_selector('div', text: 'pages.403.header') + expect_forbidden end end end From aa318d34c9cba6d95ca4832a66c0bdde75f94e13 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:08:26 +0800 Subject: [PATCH 033/173] feat(multiple_response_answer): adapt to new UI --- .../course/assessment/answer/multiple_response_answer_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/course/assessment/answer/multiple_response_answer_spec.rb b/spec/features/course/assessment/answer/multiple_response_answer_spec.rb index bb0a35346a7..b2b18393fa5 100644 --- a/spec/features/course/assessment/answer/multiple_response_answer_spec.rb +++ b/spec/features/course/assessment/answer/multiple_response_answer_spec.rb @@ -75,7 +75,7 @@ visit edit_course_assessment_submission_path(course, assessment, submission) option = assessment.questions.first.actable.options.first - element = find('b', text: option.option).find('div') + element = find('p', text: option.option) expect(element.style('background-color')['background-color']).to eq('rgba(232, 245, 233, 1)') end From 66a2d112369236a2dcc85aa4fe35e595dda87524 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:09:34 +0800 Subject: [PATCH 034/173] test(password_protected): use new authentication performers --- spec/features/course/assessment/submission/log_spec.rb | 6 +++--- .../password_protected_and_delayed_publishing_spec.rb | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/spec/features/course/assessment/submission/log_spec.rb b/spec/features/course/assessment/submission/log_spec.rb index 32cba111692..613de002975 100644 --- a/spec/features/course/assessment/submission/log_spec.rb +++ b/spec/features/course/assessment/submission/log_spec.rb @@ -44,19 +44,19 @@ # Logout and login again and visit the same submission click_on 'OK' - perform_logout_in_course CourseUser.for_user(user).first.name - + logout login_as(user) - wait_for_page expect do visit edit_course_assessment_submission_path(course, protected_assessment, protected_submission) + wait_for_page end.to change { protected_submission.logs.count }.by(1) expect(protected_submission.logs.last.valid_attempt?).to be(false) expect do fill_in 'password', with: protected_assessment.session_password click_button('Submit') + wait_for_page end.to change { protected_submission.logs.count }.by(1) wait_for_page diff --git a/spec/features/course/assessment/submission/password_protected_and_delayed_publishing_spec.rb b/spec/features/course/assessment/submission/password_protected_and_delayed_publishing_spec.rb index 3578fecee49..a4cc5443d36 100644 --- a/spec/features/course/assessment/submission/password_protected_and_delayed_publishing_spec.rb +++ b/spec/features/course/assessment/submission/password_protected_and_delayed_publishing_spec.rb @@ -12,9 +12,7 @@ end let(:mrq_questions) { assessment.reload.questions.map(&:specific) } let(:student) { create(:course_student, course: course).user } - let(:submission) do - create(:submission, assessment: assessment, creator: student) - end + let(:submission) { create(:submission, :attempting, assessment: assessment, creator: student) } before { login_as(user, scope: :user) } @@ -33,11 +31,11 @@ # The user should be redirect to submission edit page wait_for_page expect(current_path).to eq(edit_course_assessment_submission_path(course, assessment, last_submission)) + click_button 'OK' # Logout and login again and visit the same submission logout login_as(user) - wait_for_page visit edit_course_assessment_submission_path(course, assessment, last_submission) From 9d43ae3d4c0dd102140704b57c4507f8dbfade6a Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:10:18 +0800 Subject: [PATCH 035/173] feat(manually_graded): adapt to new UI --- .../course/assessment/submission/manually_graded_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/course/assessment/submission/manually_graded_spec.rb b/spec/features/course/assessment/submission/manually_graded_spec.rb index 77e053cd232..22083d5fdf1 100644 --- a/spec/features/course/assessment/submission/manually_graded_spec.rb +++ b/spec/features/course/assessment/submission/manually_graded_spec.rb @@ -173,7 +173,7 @@ # Refresh and check for the late submission warning. visit edit_course_assessment_submission_path(course, assessment, submission) - expect(page).to have_css('#late-submission', text: late_submission_text) + expect(page).to have_text(late_submission_text) # Create an extra question after submission is submitted, user should still be able to # grade the submission in this case. From 54da869ee00856424cfc91dd805dbba85a516ff7 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:10:27 +0800 Subject: [PATCH 036/173] feat(assessment_attempt): adapt to new UI --- spec/features/course/assessment/assessment_attempt_spec.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/features/course/assessment/assessment_attempt_spec.rb b/spec/features/course/assessment/assessment_attempt_spec.rb index 8d0dfb0e51b..60d0dcee01e 100644 --- a/spec/features/course/assessment/assessment_attempt_spec.rb +++ b/spec/features/course/assessment/assessment_attempt_spec.rb @@ -213,8 +213,7 @@ expect(submission.answers.map(&:reload).all?(&:evaluated?)).to be(true) # This field should be filled when page loads - correct_exp = (assessment.base_exp * submission.grade / - assessment.questions.map(&:maximum_grade).sum).to_i + correct_exp = (assessment.base_exp * submission.grade / assessment.questions.map(&:maximum_grade).sum).to_i expect(find_field('submission_draft_points_awarded').value).to eq(correct_exp.to_s) submission_maximum_grade = 0 @@ -253,7 +252,7 @@ visit edit_course_assessment_submission_path(course, assessment, submission) - click_button I18n.t('course.assessment.submission.submissions.buttons.unsubmit') + click_button 'Unsubmit Submission' expect(submission.reload.attempting?).to be_truthy expect(submission.points_awarded).to be_nil expect(submission.reload.latest_answers.all?(&:attempting?)).to be_truthy @@ -265,7 +264,7 @@ visit edit_course_assessment_submission_path(course, assessment, submission) - click_button I18n.t('course.assessment.submission.submissions.buttons.unsubmit') + click_button 'Unsubmit Submission' expect(submission.reload.attempting?).to be_truthy expect(submission.points_awarded).to be_nil expect(submission.latest_answers.all?(&:attempting?)).to be_truthy From 43ec634bfdd560bdf7bac44ef645dd511fc02f58 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:10:34 +0800 Subject: [PATCH 037/173] feat(assessment_viewing): adapt to new UI --- spec/features/course/assessment/assessment_viewing_spec.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spec/features/course/assessment/assessment_viewing_spec.rb b/spec/features/course/assessment/assessment_viewing_spec.rb index 4b2c1be486c..22d42c06403 100644 --- a/spec/features/course/assessment/assessment_viewing_spec.rb +++ b/spec/features/course/assessment/assessment_viewing_spec.rb @@ -53,10 +53,7 @@ create(:submission, assessment: assessment, creator: student_user) visit course_assessment_path(course, assessment) - expect(page).to have_link( - 'Attempt', - href: course_assessment_path(course, assessment) - ) + expect(page).to have_link('Attempt', href: course_assessment_attempt_path(course, assessment)) end end From 878b87e19ddd48a1bf602d808dd709a4d8e0f79e Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:11:00 +0800 Subject: [PATCH 038/173] test(homepage): enable `js: true` --- spec/features/course/homepage_spec.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/features/course/homepage_spec.rb b/spec/features/course/homepage_spec.rb index cab4ca9b92a..e2336a01fd1 100644 --- a/spec/features/course/homepage_spec.rb +++ b/spec/features/course/homepage_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rails_helper' -RSpec.feature 'Course: Homepage' do +RSpec.feature 'Course: Homepage', js: true do let(:instance) { Instance.default } with_tenant(:instance) do @@ -109,13 +109,13 @@ expect(course_user.reload.last_active_at).to be_within(1.hour).of(Time.zone.now) end - scenario 'I am able to see announcements in course homepage', js: true do + scenario 'I am able to see announcements in course homepage' do valid_announcement = create(:course_announcement, course: course) visit course_path(course) expect(page).to have_selector("#announcement-#{valid_announcement.id}") end - scenario 'I am able to see the activity feed in course homepage', js: true do + scenario 'I am able to see the activity feed in course homepage' do feed_notifications visit course_path(course) @@ -124,7 +124,7 @@ end end - scenario 'I am unable to see activities with deleted objects in my course homepage', js: true do + scenario 'I am unable to see activities with deleted objects in my course homepage' do feed_notifications.each do |notification| notification.activity.object.delete end @@ -135,7 +135,7 @@ end end - scenario 'I can view and ignore the relevant todos in my homepage', js: true do + scenario 'I can view and ignore the relevant todos in my homepage' do assessment_todos video_todo survey_todo @@ -158,7 +158,7 @@ end find("#todo-ignore-button-#{assessment_todos[:in_progress].id}").click - expect(page).to have_selector('div.Toastify__toast-body', text: 'Pending task successfully ignored') + expect_toastify 'Pending task successfully ignored' # Reload page to load other todos visit course_path(course) @@ -174,13 +174,13 @@ context 'As a user not registered for the course' do let(:user) { create(:user) } - scenario 'I am not able to see announcements in course homepage', js: true do + scenario 'I am not able to see announcements in course homepage' do valid_announcement = create(:course_announcement, course: course) visit course_path(course) expect(page).to_not have_selector("#announcement-#{valid_announcement.id}") end - scenario 'I am not able to see the activity feed in course homepage', js: true do + scenario 'I am not able to see the activity feed in course homepage' do feed_notifications visit course_path(course) @@ -189,7 +189,7 @@ end end - scenario 'I am able to see owner and managers in instructors list', js: true do + scenario 'I am able to see owner and managers in instructors list' do manager = create(:course_manager, course: course) teaching_assistant = create(:course_teaching_assistant, course: course) visit course_path(course) @@ -200,7 +200,7 @@ expect(page).not_to have_selector("#instructor-#{teaching_assistant.user_id}") end - scenario 'I am able to see the course description', js: true do + scenario 'I am able to see the course description' do visit course_path(course) expect(page).to have_text('Description') expect(page).to have_text(course.description) From 8bf205fa67e0e472b1ba4e341723f7a5912051c6 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:11:14 +0800 Subject: [PATCH 039/173] feat(staff_statistics): adapt to new UI --- spec/features/course/staff_statistics_spec.rb | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/spec/features/course/staff_statistics_spec.rb b/spec/features/course/staff_statistics_spec.rb index 826ee9964fc..02d8ced8b6a 100644 --- a/spec/features/course/staff_statistics_spec.rb +++ b/spec/features/course/staff_statistics_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rails_helper' -RSpec.feature 'Course: Statistics: Staff' do +RSpec.feature 'Course: Statistics: Staff', js: true do subject { page } let!(:instance) { Instance.default } @@ -79,34 +79,32 @@ end scenario 'I can view staff summary' do - pending 'Migrated staff statistics to React-side' - visit course_statistics_staff_path(course) - - expect(page).to have_selector('li', text: I18n.t('course.statistics.staff.header')) - - within find(content_tag_selector(tutor1)) do - expect(page).to have_selector('td', text: '1') # S/N - expect(page).to have_selector('td', text: tutor1.name) - expect(page).to have_selector('td', text: tutor1_submissions.size) - expect(page).to have_selector('td', text: "1 #{I18n.t('time.day')} 01:01:01") + visit course_statistics_path(course) + click_button 'Staff' + + within find('tr', text: tutor1.name) do |row| + expect(row).to have_selector('td', text: '1') # S/N + expect(row).to have_selector('td', text: tutor1.name) + expect(row).to have_selector('td', text: tutor1_submissions.size) + expect(row).to have_selector('td', text: "1 #{I18n.t('time.day')} 01:01:01") end - within find(content_tag_selector(tutor2)) do - expect(page).to have_selector('td', text: '2') - expect(page).to have_selector('td', text: tutor2.name) - expect(page).to have_selector('td', text: tutor2_submissions.size) - expect(page).to have_selector('td', text: "2 #{I18n.t('time.day')} 00:00:00") + within find('tr', text: tutor2.name) do |row| + expect(row).to have_selector('td', text: '2') + expect(row).to have_selector('td', text: tutor2.name) + expect(row).to have_selector('td', text: tutor2_submissions.size) + expect(row).to have_selector('td', text: "2 #{I18n.t('time.day')} 00:00:00") end # Do not reflect staff submissions as part of staff statistics. - within find(content_tag_selector(tutor3)) do - expect(page).to have_selector('td', text: '3') - expect(page).to have_selector('td', text: tutor3.name) - expect(page).to have_selector('td', text: '1') - expect(page).to have_selector('td', text: "3 #{I18n.t('time.day')} 00:00:00") + within find('tr', text: tutor3.name) do |row| + expect(row).to have_selector('td', text: '3') + expect(row).to have_selector('td', text: tutor3.name) + expect(row).to have_selector('td', text: '1') + expect(row).to have_selector('td', text: "3 #{I18n.t('time.day')} 00:00:00") end - expect(page).not_to have_content_tag_for(tutor4) + expect(page).not_to have_text(tutor4.name) end end @@ -116,7 +114,7 @@ scenario 'I cannot see the sidebar item' do visit course_path(course) - expect(page).not_to have_selector('li', text: I18n.t('course.statistics.header')) + expect(find_sidebar).not_to have_text(I18n.t('course.statistics.header')) end end end From 98fe97c72d0706ec29170c0ae41dee70be0192ad Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:11:31 +0800 Subject: [PATCH 040/173] feat(student_statistics): adapt to new UI --- .../course/students_statistics_spec.rb | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/spec/features/course/students_statistics_spec.rb b/spec/features/course/students_statistics_spec.rb index ba8ae24e71f..164cbe4fe30 100644 --- a/spec/features/course/students_statistics_spec.rb +++ b/spec/features/course/students_statistics_spec.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true require 'rails_helper' -RSpec.feature 'Course: Student Statistics' do +RSpec.feature 'Course: Student Statistics', js: true do subject { page } let!(:instance) { Instance.default } with_tenant(:instance) do let(:course) { create(:course) } - let(:students) { create_list(:course_student, 2, course: course) } + let!(:students) { create_list(:course_student, 2, course: course) } before do login_as(user, scope: :user) @@ -27,42 +27,32 @@ create(:course_group_user, group: other_group, course_user: students.last) end - scenario 'I can only view all student statistics when I am not a group manager', js: true do - pending 'Migrated students statistics to React-side' - students - visit course_statistics_all_students_path(course) - - expect(page).to have_link(I18n.t('course.statistics.student.header'), - href: course_statistics_all_students_path(course)) - - students.each do |student| - expect(page).to have_content_tag_for(student) - end + scenario 'I can only view all student statistics when I am not a group manager' do + visit course_statistics_path(course) - expect(page).not_to have_text(I18n.t('course.statistics.tabs.my_students_tab')) - expect(page). - not_to have_text(I18n.t('course.statistics.course_student_statistics.phantom_students')) + students.each { |student| expect(page).to have_text(student.name) } + expect(page).not_to have_text('Show My Students Only') + expect(page).not_to have_text('Phantom') # Test that phantom students are rendered only if they exist phantom_student = students.first phantom_student.phantom = true phantom_student.save - visit course_statistics_all_students_path(course) - students.each do |student| - expect(page).to have_content_tag_for(student) - end - expect(page). - to have_text(I18n.t('course.statistics.course_student_statistics.phantom_students')) + + visit course_statistics_path(course) + + students.each { |student| expect(page).to have_text(student.name) } + expect(page).to have_text('Phantom') end end context 'As a Course Student' do let(:user) { create(:course_student, course: course).user } - scenario 'I cannot see the sidebar item' do + scenario 'I cannot see the statistics sidebar item' do visit course_path(course) - expect(page).not_to have_selector('li', text: I18n.t('course.statistics.header')) + expect(find_sidebar).not_to have_text(I18n.t('course.statistics.header')) end scenario 'I cannot access the statistics page' do From 2605bd3a06c9ff4b93b949c09d943efac52435a1 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:11:48 +0800 Subject: [PATCH 041/173] feat(masquerades): adapt to new UI --- spec/features/system/admin/masquerades_spec.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/features/system/admin/masquerades_spec.rb b/spec/features/system/admin/masquerades_spec.rb index ff592e9690f..bd240303ee3 100644 --- a/spec/features/system/admin/masquerades_spec.rb +++ b/spec/features/system/admin/masquerades_spec.rb @@ -17,8 +17,11 @@ scenario 'I can masquerade a user' do visit admin_users_path + find(".user-masquerade-#{user_to_masquerade.id}").click - expect(page).to have_selector('li', text: user_to_masquerade.name) + wait_for_page + + expect(page).to have_text("Masquerading as #{user_to_masquerade.name}") end end From d87c356f3f91f2fbe7761bdc63f11eba66c0eb77 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:12:00 +0800 Subject: [PATCH 042/173] test(course_management): enable `js: true` --- spec/features/course_management_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/features/course_management_spec.rb b/spec/features/course_management_spec.rb index ca8e3fc90ea..2a39a3c4be8 100644 --- a/spec/features/course_management_spec.rb +++ b/spec/features/course_management_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rails_helper' -RSpec.feature 'Courses' do +RSpec.feature 'Courses', js: true do subject { page } let(:instance) { create(:instance) } @@ -9,7 +9,7 @@ let(:user) { create(:instance_user, :instructor).user } before { login_as(user, scope: :user) } - scenario 'Users can see a list of published courses', js: true do + scenario 'Users can see a list of published courses' do unpublished_course = create(:course) published_course = create(:course, :published) @@ -30,7 +30,7 @@ expect(page).not_to have_link(other_course.title, href: course_path(other_course)) end - scenario 'Users can create a new course', js: true do + scenario 'Users can create a new course' do visit courses_path find('button.new-course-button').click From 7343f70728270163979a2f370c84f455791b7adb Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:12:19 +0800 Subject: [PATCH 043/173] test(instance_user_role_requests_management): enable `js: true` --- .../instance_user_role_requests_management_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/features/instance_user_role_requests_management_spec.rb b/spec/features/instance_user_role_requests_management_spec.rb index c87f20671fa..e667b210f21 100644 --- a/spec/features/instance_user_role_requests_management_spec.rb +++ b/spec/features/instance_user_role_requests_management_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'rails_helper' -RSpec.feature 'Instance::UserRoleRequests' do +RSpec.feature 'Instance::UserRoleRequests', js: true do subject { page } let(:instance) { create(:instance) } @@ -10,7 +10,7 @@ before { login_as(user, scope: :user) } context 'As a normal instance user' do - scenario 'I can create a new role request', type: :mailer, js: true do + scenario 'I can create a new role request', type: :mailer do visit courses_path find('#role-request-button').click @@ -33,7 +33,7 @@ expect(request_created.reason).to eq(request.reason) end - scenario 'I can edit my existing role request', js: true do + scenario 'I can edit my existing role request' do request = create(:role_request, user: user, instance: instance) visit courses_path find('#role-request-button').click @@ -46,7 +46,7 @@ end end - context 'As an instance admin', js: true do + context 'As an instance admin' do let(:user) { create(:instance_administrator).user } let!(:requests) { create_list(:role_request, 2, instance: instance) } From f8663ccaee4b4986ea7ec990611a73620f24ac6e Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:12:46 +0800 Subject: [PATCH 044/173] test(instance_announcement_management): create `user` before `login` --- .../instance/instance_announcement_management_spec.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spec/features/system/admin/instance/instance_announcement_management_spec.rb b/spec/features/system/admin/instance/instance_announcement_management_spec.rb index 2a8e4f01e66..294d8023763 100644 --- a/spec/features/system/admin/instance/instance_announcement_management_spec.rb +++ b/spec/features/system/admin/instance/instance_announcement_management_spec.rb @@ -7,13 +7,10 @@ let!(:instance) { create(:instance) } with_tenant(:instance) do - before do - login_as(user, scope: :user) - end + let(:user) { create(:instance_administrator, instance: instance).user } + before { login_as(user, scope: :user) } context 'As an Instance Administrator' do - let(:user) { create(:instance_administrator).user } - scenario 'I can create instance announcements' do visit admin_instance_announcements_path From ea18058c9595018d018ff4cee07ccaa7ac2f406c Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:13:55 +0800 Subject: [PATCH 045/173] test(authentication): remove incompatible RSpec tests --- spec/features/course/registration_spec.rb | 64 -------- .../features/user/password_management_spec.rb | 16 -- spec/features/user_sign_in_spec.rb | 50 ------- spec/features/user_sign_up_spec.rb | 141 ------------------ 4 files changed, 271 deletions(-) delete mode 100644 spec/features/course/registration_spec.rb delete mode 100644 spec/features/user/password_management_spec.rb delete mode 100644 spec/features/user_sign_in_spec.rb delete mode 100644 spec/features/user_sign_up_spec.rb diff --git a/spec/features/course/registration_spec.rb b/spec/features/course/registration_spec.rb deleted file mode 100644 index 9772859290d..00000000000 --- a/spec/features/course/registration_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.feature 'Courses: Registration' do - let!(:instance) { Instance.default } - - with_tenant(:instance) do - let(:course) { create(:course) } - let(:user) { create(:user) } - before { login_as(user, scope: :user) } - - context 'when the course has unconfirmed invitations' do - let!(:invitation) { create(:course_user_invitation, course: course) } - - scenario 'Users can register course using invitation code', js: true do - visit course_path(course) - - # No code input - find('#register-button').click - expect(page). - to have_selector('div.Toastify__toast-body', text: 'Please enter an invitation code') - - # Enter wrong registration code - fill_in 'registration-code', with: 'defintielyTheWrongCode' - find('#register-button').click - expect(page). - to have_selector('div.Toastify__toast-body', text: 'Your code is incorrect') - - # Correct code - fill_in 'registration-code', with: invitation.invitation_key - find('#register-button').click - end - end - - context 'when the course allows enrol requests' do - let(:course) { create(:course, :enrollable) } - - scenario 'Users can create and cancel enrol requests', js: true do - visit course_path(course) - - expect(page).to have_text(course.description) - - expect(ActionMailer::Base.deliveries.count).to eq(0) - find('#submit-enrol-request-button').click - expect(page).to have_selector('div.Toastify__toast-body', text: 'Your enrol request has been submitted.') - expect(ActionMailer::Base.deliveries.count).not_to eq(0) - - # Cancel request - find('#cancel-enrol-request-button').click - expect(page).to have_selector('div.Toastify__toast-body', text: 'Your enrol request has been cancelled.') - end - - context 'when the user has been enrolled' do - let!(:enrolled_student) { create(:course_student, course: course, user: user) } - - scenario 'user cannot de-register or re-register for the course', js: true do - visit course_path(course) - expect(page).not_to have_selector('#submit-enrol-request-button') - expect(page).not_to have_selector('#cancel-enrol-request-button') - end - end - end - end -end diff --git a/spec/features/user/password_management_spec.rb b/spec/features/user/password_management_spec.rb deleted file mode 100644 index 9e613af50b5..00000000000 --- a/spec/features/user/password_management_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.feature 'Users: Change password' do - let(:instance) { Instance.default } - with_tenant(:instance) do - let!(:user) { create(:user) } - before { login_as(user, scope: :user) } - - context 'As a User' do - scenario 'I am able to change my password' do - visit edit_user_profile_path - end - end - end -end diff --git a/spec/features/user_sign_in_spec.rb b/spec/features/user_sign_in_spec.rb deleted file mode 100644 index 6a6af255650..00000000000 --- a/spec/features/user_sign_in_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.feature 'Users: Sign In' do - let(:instance) { Instance.default } - let(:other_instance) { create(:instance) } - let(:password) { '12345678' } - - with_tenant(:instance) do - context 'As a user from another instance' do - let(:user) do - user = nil - ActsAsTenant.with_tenant(other_instance) do - user = create(:user, password: password) - end - user - end - - scenario 'I can sign in to current instance' do - visit new_user_session_path - fill_in 'user_email', with: user.email - fill_in 'user_password', with: user.password - - expect do - click_button I18n.t('user.sessions.new.sign_in') - end.to change { instance.instance_users.exists?(user: user) }.from(false).to(true) - end - - context 'As a system administrator' do - let(:user) do - user = nil - ActsAsTenant.with_tenant(other_instance) do - user = create(:administrator, password: password) - end - user - end - - scenario 'I can sign in to current instance' do - visit new_user_session_path - fill_in 'user_email', with: user.email - fill_in 'user_password', with: password - - click_button I18n.t('user.sessions.new.sign_in') - expect(page).to have_selector('div.alert', text: I18n.t('user.signed_in')) - expect(instance.instance_users.exists?(user: user)).to be_falsy - end - end - end - end -end diff --git a/spec/features/user_sign_up_spec.rb b/spec/features/user_sign_up_spec.rb deleted file mode 100644 index 010c3273123..00000000000 --- a/spec/features/user_sign_up_spec.rb +++ /dev/null @@ -1,141 +0,0 @@ -# frozen_string_literal: true -require 'rails_helper' - -RSpec.feature 'Users: Sign Up' do - let(:instance) { Instance.default } - with_tenant(:instance) do - context 'As an unregistered user' do - scenario 'I can register for an account' do - visit new_user_registration_path - - expect do - click_button I18n.t('user.registrations.new.sign_up') - end.not_to change(User, :count) - expect(page).to have_selector('div.has-error') - - valid_user = attributes_for(:user).reverse_merge(email: generate(:email)) - fill_in 'user_name', with: valid_user[:name] - fill_in 'user_email', with: valid_user[:email] - fill_in 'user_password', with: valid_user[:password] - fill_in 'user_password_confirmation', with: valid_user[:password] - - expect do - click_button I18n.t('user.registrations.new.sign_up') - end.to change(User, :count).by(1) - user = User::Email.find_by!(email: valid_user[:email]).user_id - expect(instance.users.exists?(user)).to be_truthy - end - end - - context 'As a user invited by course staffs' do - let(:course) { create(:course) } - let(:invitation) { create(:course_user_invitation, :phantom, course: course) } - let(:invited_email) { invitation.email } - - scenario 'I can register for an account' do - visit new_user_registration_path(invitation: invitation.invitation_key) - - invited_user = attributes_for(:user) - fill_in 'user_password', with: invited_user[:password] - fill_in 'user_password_confirmation', with: invited_user[:password] - - expect do - click_button I18n.t('user.registrations.new.sign_up') - end.to change(course.users, :count).by(1) - - email = User::Email.find_by(email: invited_email) - user = email.user - course_user = CourseUser.where(user: user, course: course).first - expect(email).to be_primary - expect(email).to be_confirmed - expect(invitation.reload).to be_confirmed - expect(invitation.confirmer).to eq(email.user) - expect(course_user).to be_phantom - end - - context 'when the invitation code is confirmed' do - let(:invitation) { create(:course_user_invitation, :confirmed, course: course) } - - scenario 'I am redirected with an error' do - visit new_user_registration_path(invitation: invitation.invitation_key) - - expect(current_path).to eq(root_path) - expect(page).to have_selector('div.alert', text: I18n.t('user.registrations.new.used_with_email')) - end - end - end - - context 'As a user invited by course staffs to multiple courses' do - let(:course1) { create(:course) } - let(:course2) { create(:course) } - let!(:invitation1) { create(:course_user_invitation, :phantom, name: 'course1_user', course: course1) } - let!(:invitation2) do - create(:course_user_invitation, name: 'course2_user', email: invitation1.email, course: course2) - end - let(:invited_email) { invitation1.email } - - scenario 'I can register for an account via the registration links' do - visit new_user_registration_path(invitation: invitation1.invitation_key) - - invited_user = attributes_for(:user) - fill_in 'user_password', with: invited_user[:password] - fill_in 'user_password_confirmation', with: invited_user[:password] - - expect do - click_button I18n.t('user.registrations.new.sign_up') - end.to change(course1.users, :count).by(1). - and change(course2.users, :count).by(1) - - email = User::Email.find_by(email: invited_email) - user = email.user - first_course_user = CourseUser.where(user: user, course: course1).first - second_course_user = CourseUser.where(user: user, course: course2).first - - expect(email).to be_primary - expect(email).to be_confirmed - expect(invitation1.reload).to be_confirmed - expect(invitation1.confirmer).to eq(email.user) - expect(first_course_user.name).to eq(invitation1.name) - expect(first_course_user).to be_phantom - - expect(invitation2.reload).to be_confirmed - expect(invitation2.confirmer).to eq(email.user) - expect(second_course_user.name).to eq(invitation2.name) - expect(second_course_user).not_to be_phantom - end - - scenario 'I can register for an account without using the registration link', type: :notifier do - visit new_user_registration_path - - valid_user = attributes_for(:user).reverse_merge(email: invitation1.email) - fill_in 'user_name', with: valid_user[:name] - fill_in 'user_email', with: valid_user[:email] - fill_in 'user_password', with: valid_user[:password] - fill_in 'user_password_confirmation', with: valid_user[:password] - - expect do - click_button I18n.t('user.registrations.new.sign_up') - confirm_registartion_token_via_email - end.to change(course1.users, :count).by(1). - and change(course2.users, :count).by(1) - - email = User::Email.find_by(email: invited_email) - user = email.user - first_course_user = CourseUser.where(user: user, course: course1).first - second_course_user = CourseUser.where(user: user, course: course2).first - - expect(email).to be_primary - expect(email).to be_confirmed - expect(invitation1.reload).to be_confirmed - expect(invitation1.confirmer).to eq(email.user) - expect(first_course_user.name).to eq(invitation1.name) - expect(first_course_user).to be_phantom - - expect(invitation2.reload).to be_confirmed - expect(invitation2.confirmer).to eq(email.user) - expect(second_course_user.name).to eq(invitation2.name) - expect(second_course_user).not_to be_phantom - end - end - end -end From 5476d2eca2e9fedfe93819025975e57f4d3bd025 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:14:08 +0800 Subject: [PATCH 046/173] test(authentication): add tests with Playwright --- .gitignore | 5 + tests/helpers.ts | 173 ++++++++++++++++++ tests/package.json | 16 ++ tests/playwright.config.ts | 75 ++++++++ tests/tests/courses/registration.spec.ts | 91 +++++++++ tests/tests/users/password-management.spec.ts | 27 +++ tests/tests/users/sign-in.spec.ts | 11 ++ tests/tests/users/sign-up.spec.ts | 152 +++++++++++++++ tests/yarn.lock | 28 +++ 9 files changed, 578 insertions(+) create mode 100644 tests/helpers.ts create mode 100644 tests/package.json create mode 100644 tests/playwright.config.ts create mode 100644 tests/tests/courses/registration.spec.ts create mode 100644 tests/tests/users/password-management.spec.ts create mode 100644 tests/tests/users/sign-in.spec.ts create mode 100644 tests/tests/users/sign-up.spec.ts create mode 100644 tests/yarn.lock diff --git a/.gitignore b/.gitignore index 6859e686c65..8903069669b 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,8 @@ node_modules /.env.* /client/.env /client/.env.* + +# Ignore Playwright results +test-results/ +playwright-report/ +playwright/.cache/ diff --git a/tests/helpers.ts b/tests/helpers.ts new file mode 100644 index 00000000000..8d3f53fcdd9 --- /dev/null +++ b/tests/helpers.ts @@ -0,0 +1,173 @@ +import { + APIRequestContext, + Locator, + Page as BasePage, + test as base, +} from '@playwright/test'; + +import packageJSON from './package.json'; + +interface User { + id: number; + name: string; + email: string; + password: string; + role: string; +} + +interface Page extends BasePage { + getReCAPTCHA: () => Locator; + getUserMenuButton: () => Locator; +} + +interface SignInPage extends Page { + originalPage: Page; + getEmailField: () => Locator; + getPasswordField: () => Locator; + getSignInButton: () => Locator; + manufactureUser: () => Promise; +} + +interface SignUpPage extends Page { + getNameField: () => Locator; + getEmailField: () => Locator; + getPasswordField: () => Locator; + getConfirmPasswordField: () => Locator; + getSignUpButton: () => Locator; + gotoSignUpPage: () => ReturnType; + gotoInvitation: (token: string) => ReturnType; + getFieldMocks: () => Pick; +} + +interface AuthenticatedPage extends Page { + user: User; + signOut: () => Promise; +} + +interface TestFixtures { + page: Page; + signInPage: SignInPage; + signUpPage: SignUpPage; + authedPage: AuthenticatedPage; +} + +let apiContext: APIRequestContext; + +const getEmail = (index: number) => `${Date.now()}+${index}@example.org`; + +const extend = ( + use: (r: T) => Promise, + page: Page, + extension: Omit, +) => use(Object.assign(page, extension) as T); + +export const test = base.extend({ + page: async ({ page }, use) => { + await extend(use, page, { + getReCAPTCHA: () => + page.frameLocator('[title="reCAPTCHA"]').getByLabel("I'm not a robot"), + getUserMenuButton: () => page.getByTestId('user-menu-button'), + } satisfies Omit); + }, + signInPage: async ({ page }, use, testInfo) => { + await page.goto('/users/sign_in'); + + await extend(use, page, { + originalPage: page, + getEmailField: () => page.getByLabel('Email address'), + getPasswordField: () => page.getByLabel('Password'), + getSignInButton: () => page.getByRole('button', { name: 'Sign in' }), + manufactureUser: async () => { + const email = getEmail(testInfo.workerIndex); + const password = 'lolololol'; + + const { id, name, role } = await manufacture({ + user: { emails_count: 0, email, password }, + }); + + return { email, password, id, name, role }; + }, + }); + }, + signUpPage: async ({ page }, use, testInfo) => { + await extend(use, page, { + getNameField: () => page.getByLabel('Name'), + getEmailField: () => page.getByLabel('Email address'), + getPasswordField: () => page.getByLabel('Password *', { exact: true }), + getConfirmPasswordField: () => page.getByLabel('Confirm password'), + getSignUpButton: () => page.getByRole('button', { name: 'Sign up' }), + gotoSignUpPage: () => page.goto('/users/sign_up'), + gotoInvitation: (token: string) => + page.goto(`/users/sign_up?invitation=${token}`), + getFieldMocks: () => ({ + name: 'John Doe', + email: getEmail(testInfo.workerIndex), + password: 'lolololol', + }), + }); + }, + authedPage: async ({ signInPage: page }, use) => { + const user = await page.manufactureUser(); + + await page.getEmailField().fill(user.email); + await page.getPasswordField().fill(user.password); + await page.getSignInButton().click(); + await page.waitForURL('/'); + + await extend(use, page.originalPage, { + user, + signOut: async () => { + await page.getUserMenuButton().click(); + await page.getByRole('button', { name: 'Sign out' }).click(); + await page.waitForURL('/users/sign_in'); + }, + }); + }, +}); + +test.beforeAll(async ({ playwright }) => { + apiContext = await playwright.request.newContext({ + baseURL: packageJSON.servers.serverURL, + }); +}); + +test.afterAll(async () => { + apiContext.dispose(); +}); + +type FactoryPayload = Record< + string, + Record & { traits?: string[] } +>; + +export const manufacture = async (payload: FactoryPayload) => { + const response = await apiContext.post('/test/create', { + data: { factory: payload }, + }); + + return await response.json(); +}; + +interface EmailPayload { + sender: string; + recipient: string; + subject: string; + body: string; +} + +export const getLastSentEmail = async (): Promise => { + const response = await apiContext.get('/test/last_sent_email'); + const payload = await response.json(); + if (!payload) return null; + + return { + sender: payload.header[1].unparsed_value, + recipient: payload.header[2].unparsed_value, + subject: payload.header[4].unparsed_value, + body: payload.body.raw_source, + }; +}; + +export const clearEmails = () => apiContext.delete('/test/clear_emails'); + +export { expect } from '@playwright/test'; diff --git a/tests/package.json b/tests/package.json new file mode 100644 index 00000000000..4c76fdf4ff6 --- /dev/null +++ b/tests/package.json @@ -0,0 +1,16 @@ +{ + "name": "coursemology2-tests", + "version": "1.0.0", + "repository": "https://github.com/Coursemology/coursemology2.git", + "devDependencies": { + "@playwright/test": "^1.36.1" + }, + "scripts": { + "prepare": "npx playwright install --with-deps", + "test": "npx playwright test" + }, + "servers": { + "clientURL": "http://lvh.me:4200", + "serverURL": "http://lvh.me:6969" + } +} diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts new file mode 100644 index 00000000000..025da366cbd --- /dev/null +++ b/tests/playwright.config.ts @@ -0,0 +1,75 @@ +import { defineConfig, devices } from '@playwright/test'; +import packageJSON from './package.json'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + baseURL: packageJSON.servers.clientURL, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/tests/tests/courses/registration.spec.ts b/tests/tests/courses/registration.spec.ts new file mode 100644 index 00000000000..157cd8815d0 --- /dev/null +++ b/tests/tests/courses/registration.spec.ts @@ -0,0 +1,91 @@ +import { + test, + expect, + manufacture, + getLastSentEmail, + clearEmails, +} from '../../helpers'; + +test.describe('with an unconfirmed invitation', () => { + let course: { id: number }; + let invitation; + + test.beforeEach(async () => { + course = await manufacture({ course: {} }); + invitation = await manufacture({ + course_user_invitation: { course_id: course.id }, + }); + }); + + test('can register with invitation code', async ({ authedPage: page }) => { + await page.goto(`/courses/${course.id}`); + + await expect(page.getByText(invitation.name)).not.toBeVisible(); + + const registerButton = page.getByRole('button', { name: 'Register' }); + await registerButton.click(); + + await expect(page.getByText('enter an invitation code')).toBeVisible(); + + const invitationCodeField = page.getByLabel('Invitation code'); + await invitationCodeField.fill('definitelyTheWrongCode'); + await registerButton.click(); + + await expect(page.getByText('code is incorrect')).toBeVisible(); + + await invitationCodeField.fill(invitation.invitation_key); + await registerButton.click(); + + await expect(page.getByText(invitation.name)).toBeVisible(); + }); +}); + +test.describe('allows enrol requests', () => { + let course: { id: number }; + + test.beforeEach(async () => { + course = await manufacture({ course: { traits: ['enrollable'] } }); + + await clearEmails(); + }); + + test('can request', async ({ authedPage: page }) => { + await page.goto(`/courses/${course.id}`); + + await expect(page.getByText('Description')).toBeVisible(); + await expect(page.getByText('Instructors', { exact: true })).toBeVisible(); + + page.getByRole('button', { name: 'Request to enrol' }).click(); + + await expect( + page.getByText('enrol request has been submitted'), + ).toBeVisible(); + + const notificationEmail = await getLastSentEmail(); + expect(notificationEmail?.subject).toContain('Enrol Request'); + + page.getByRole('button', { name: 'Cancel request' }).click(); + + await expect( + page.getByText('enrol request has been cancelled'), + ).toBeVisible(); + }); + + test('cannot request if already enrolled', async ({ authedPage: page }) => { + const student = await manufacture({ + course_student: { course_id: course.id, user_id: page.user.id }, + }); + + await page.goto(`/courses/${course.id}`); + + await expect(page.getByText(student.name)).toBeVisible(); + + await expect( + page.getByRole('button', { name: 'Request to enrol' }), + ).not.toBeVisible(); + + await expect( + page.getByRole('button', { name: 'Cancel request' }), + ).not.toBeVisible(); + }); +}); diff --git a/tests/tests/users/password-management.spec.ts b/tests/tests/users/password-management.spec.ts new file mode 100644 index 00000000000..041ebf0ac41 --- /dev/null +++ b/tests/tests/users/password-management.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from '../../helpers'; + +test('can change password', async ({ authedPage: page }) => { + const newPassword = 'newpassword'; + + await page.goto('/user/profile/edit'); + + await page.getByLabel('Current password').fill(page.user.password); + await page.getByLabel('New password', { exact: true }).fill(newPassword); + await page.getByLabel('Confirm new password').fill(newPassword); + + await page.getByRole('button', { name: 'Save changes' }).click(); + await expect(page.getByText('Your changes have been saved')).toBeVisible(); + + await page.signOut(); + + await page.getByLabel('Email address').fill(page.user.email); + await page.getByLabel('Password').fill(page.user.password); + await page.getByRole('button', { name: 'Sign in' }).click(); + + await expect(page.getByText('invalid email or password')).toBeVisible(); + + await page.getByLabel('Password').fill(newPassword); + await page.getByRole('button', { name: 'Sign in' }).click(); + + await expect(page.getUserMenuButton()).toBeVisible(); +}); diff --git a/tests/tests/users/sign-in.spec.ts b/tests/tests/users/sign-in.spec.ts new file mode 100644 index 00000000000..69efc87de90 --- /dev/null +++ b/tests/tests/users/sign-in.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from '../../helpers'; + +test('can sign in', async ({ signInPage: page }) => { + const { email, password } = await page.manufactureUser(); + + await page.getEmailField().fill(email); + await page.getPasswordField().fill(password); + await page.getSignInButton().click(); + + await expect(page.getUserMenuButton()).toBeVisible(); +}); diff --git a/tests/tests/users/sign-up.spec.ts b/tests/tests/users/sign-up.spec.ts new file mode 100644 index 00000000000..e0c129ab1b9 --- /dev/null +++ b/tests/tests/users/sign-up.spec.ts @@ -0,0 +1,152 @@ +import { expect, getLastSentEmail, manufacture, test } from '../../helpers'; + +const getHrefURLFromString = (string: string): string | undefined => + string.match(/href="(.*(?="))/)?.[1]; + +test.describe('unregistered user', () => { + test('can sign up', async ({ signUpPage: page }) => { + const { name, email, password } = page.getFieldMocks(); + + await page.gotoSignUpPage(); + + await page.getNameField().fill(name); + await page.getEmailField().fill(email); + await page.getPasswordField().fill(password); + await page.getConfirmPasswordField().fill(password); + await page.getReCAPTCHA().click(); + await page.getSignUpButton().click(); + + await expect.soft(page.getByText(email)).toBeVisible(); + await expect(page.getByText('check your email')).toBeVisible(); + + const confirmationEmail = await getLastSentEmail(); + expect(confirmationEmail).not.toBeNull(); + + expect.soft(confirmationEmail!.recipient).toEqual(email); + expect(confirmationEmail!.body).toContain('confirmation_token'); + + const confirmationURL = getHrefURLFromString(confirmationEmail!.body); + expect(confirmationURL).toBeTruthy(); + + await page.goto(confirmationURL!); + + await expect.soft(page.getByText(email)).toBeVisible(); + await expect(page.getByText('confirmed')).toBeVisible(); + }); +}); + +test.describe('user invited to a course', () => { + test('can sign up with invitation', async ({ signUpPage: page }) => { + const { password } = page.getFieldMocks(); + + const course = await manufacture({ course: {} }); + const invitation = await manufacture({ + course_user_invitation: { course_id: course.id, traits: ['phantom'] }, + }); + + await page.gotoInvitation(invitation.invitation_key); + + await expect(page.getNameField()).toHaveValue(invitation.name); + await expect(page.getEmailField()).toHaveValue(invitation.email); + + await page.getPasswordField().fill(password); + await page.getConfirmPasswordField().fill(password); + await page.getReCAPTCHA().click(); + + await page.getSignUpButton().click(); + + await expect(page).toHaveURL(new RegExp(course.id)); + }); + + test('cannot sign up with used invitation', async ({ signUpPage: page }) => { + const invitation = await manufacture({ + course_user_invitation: { traits: ['confirmed'] }, + }); + + await page.gotoInvitation(invitation.invitation_key); + + await expect(page.getByText('has been claimed')).toBeVisible(); + await expect(page.getNameField()).toBeEmpty(); + await expect(page.getEmailField()).toBeEmpty(); + }); +}); + +test.describe('user invited to 2 courses', () => { + test('can sign up with first invitation', async ({ signUpPage: page }) => { + const { password } = page.getFieldMocks(); + + const course1 = await manufacture({ course: {} }); + const invitation1 = await manufacture({ + course_user_invitation: { course_id: course1.id }, + }); + + const course2 = await manufacture({ course: {} }); + const invitation2 = await manufacture({ + course_user_invitation: { + email: invitation1.email, + course_id: course2.id, + }, + }); + + await page.gotoInvitation(invitation1.invitation_key); + + await page.getPasswordField().fill(password); + await page.getConfirmPasswordField().fill(password); + await page.getReCAPTCHA().click(); + await page.getSignUpButton().click(); + await expect(page.getByText('Welcome')).toBeVisible(); + + await page.goto(`/courses/${course1.id}`); + await expect(page).toHaveURL(new RegExp(course1.id)); + await expect(page.getByText(invitation1.name)).toBeVisible(); + + await page.goto(`/courses/${course2.id}`); + await expect(page).toHaveURL(new RegExp(course2.id)); + await expect(page.getByText(invitation2.name)).toBeVisible(); + }); + + test('can sign up without invitation', async ({ signUpPage: page }) => { + const { name, email, password } = page.getFieldMocks(); + + const course = await manufacture({ course: {} }); + const invitation = await manufacture({ + course_user_invitation: { email, course_id: course.id }, + }); + + await page.gotoSignUpPage(); + + await page.getNameField().fill(name); + await page.getEmailField().fill(email); + await page.getPasswordField().fill(password); + await page.getConfirmPasswordField().fill(password); + await page.getReCAPTCHA().click(); + await page.getSignUpButton().click(); + + await expect.soft(page.getByText(email)).toBeVisible(); + await expect(page.getByText('check your email')).toBeVisible(); + + const confirmationEmail = await getLastSentEmail(); + expect(confirmationEmail).not.toBeNull(); + + expect.soft(confirmationEmail!.recipient).toEqual(email); + expect(confirmationEmail!.body).toContain('confirmation_token'); + + const confirmationURL = getHrefURLFromString(confirmationEmail!.body); + expect(confirmationURL).toBeTruthy(); + + await page.goto(confirmationURL!); + + await expect.soft(page.getByText(email)).toBeVisible(); + await expect(page.getByText('confirmed')).toBeVisible(); + + await page.goto('/users/sign_in'); + await page.getByLabel('Email address').fill(email); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: 'Sign in' }).click(); + await page.waitForURL('/'); + + await page.goto(`/courses/${course.id}`); + await expect(page).toHaveURL(new RegExp(course.id)); + await expect(page.getByText(invitation.name)).toBeVisible(); + }); +}); diff --git a/tests/yarn.lock b/tests/yarn.lock new file mode 100644 index 00000000000..42c7b7b2350 --- /dev/null +++ b/tests/yarn.lock @@ -0,0 +1,28 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@playwright/test@^1.36.1": + version "1.36.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.36.1.tgz#0b1247d279f142ac0876ce25e7daf15439d5367b" + integrity sha512-YK7yGWK0N3C2QInPU6iaf/L3N95dlGdbsezLya4n0ZCh3IL7VgPGxC6Gnznh9ApWdOmkJeleT2kMTcWPRZvzqg== + dependencies: + "@types/node" "*" + playwright-core "1.36.1" + optionalDependencies: + fsevents "2.3.2" + +"@types/node@*": + version "20.4.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.4.tgz#c79c7cc22c9d0e97a7944954c9e663bcbd92b0cb" + integrity sha512-CukZhumInROvLq3+b5gLev+vgpsIqC2D0deQr/yS1WnxvmYLlJXZpaQrQiseMY+6xusl79E04UjWoqyr+t1/Ew== + +fsevents@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +playwright-core@1.36.1: + version "1.36.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.36.1.tgz#f5f275d70548768ca892583519c89b237a381c77" + integrity sha512-7+tmPuMcEW4xeCL9cp9KxmYpQYHKkyjwoXRnoeTowaeNat8PoBMk/HwCYhqkH2fRkshfKEOiVus/IhID2Pg8kg== From 533b1d6831ef0057938e497a7e03f403bc257e42 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:15:47 +0800 Subject: [PATCH 047/173] test: add server test helpers for Playwright --- app/controllers/test/controller.rb | 10 ++++++ app/controllers/test/factories_controller.rb | 35 ++++++++++++++++++++ app/controllers/test/mailer_controller.rb | 12 +++++++ config/routes.rb | 8 +++++ 4 files changed, 65 insertions(+) create mode 100644 app/controllers/test/controller.rb create mode 100644 app/controllers/test/factories_controller.rb create mode 100644 app/controllers/test/mailer_controller.rb diff --git a/app/controllers/test/controller.rb b/app/controllers/test/controller.rb new file mode 100644 index 00000000000..1c50dff49c1 --- /dev/null +++ b/app/controllers/test/controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +class Test::Controller < ActionController::Base + before_action :restrict_to_test + + private + + def restrict_to_test + head :not_found unless Rails.env.test? + end +end diff --git a/app/controllers/test/factories_controller.rb b/app/controllers/test/factories_controller.rb new file mode 100644 index 00000000000..11eee7e2ef4 --- /dev/null +++ b/app/controllers/test/factories_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +class Test::FactoriesController < Test::Controller + before_action :set_user_stamper, only: [:create] + + def create + models = {} + + ActsAsTenant.with_tenant(Instance.default) do + create_params.each do |factory_name, attributes| + traits = traits_from(attributes) + model = FactoryBot.create(factory_name, *traits, attributes) + models[factory_name] = model.as_json + rescue SystemStackError + models[factory_name] = { id: model.id } + end + end + + result = (models.size <= 1) ? models.values.first : models + render json: result, status: :created + end + + private + + def create_params + params.permit(factory: {}).to_h['factory'] + end + + def set_user_stamper + User.stamper = User.human_users.first + end + + def traits_from(attributes) + attributes.extract!('traits')[:traits]&.map(&:to_sym) + end +end diff --git a/app/controllers/test/mailer_controller.rb b/app/controllers/test/mailer_controller.rb new file mode 100644 index 00000000000..352eb45b494 --- /dev/null +++ b/app/controllers/test/mailer_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Test::MailerController < Test::Controller + def last_sent + render json: ActionMailer::Base.deliveries.last + end + + def clear + ActionMailer::Base.deliveries.clear + + head :ok + end +end diff --git a/config/routes.rb b/config/routes.rb index 71113187088..61216cbba65 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -462,4 +462,12 @@ end resources :attachment_references, path: 'attachments', only: [:create, :show, :destroy] + + if Rails.env.test? + namespace :test do + post 'create' => 'factories#create' + delete 'clear_emails' => 'mailer#clear' + get 'last_sent_email' => 'mailer#last_sent' + end + end end From 294866da1289890cb01f3a06dbe5903424838382 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:16:18 +0800 Subject: [PATCH 048/173] feat(passwords_controller): responds to JSON --- app/controllers/user/passwords_controller.rb | 20 ++++++++++++++++++++ config/routes.rb | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 app/controllers/user/passwords_controller.rb diff --git a/app/controllers/user/passwords_controller.rb b/app/controllers/user/passwords_controller.rb new file mode 100644 index 00000000000..9f0eb5e2347 --- /dev/null +++ b/app/controllers/user/passwords_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +class User::PasswordsController < Devise::PasswordsController + respond_to :json + + def edit + super + + if (user = User.find_by(reset_password_token: hash_reset_password_token(params[:reset_password_token]))) + render json: { email: user.email } + else + render json: { error: 'Invalid token' }, status: :bad_request + end + end + + private + + def hash_reset_password_token(token) + Devise.token_generator.digest(self, :reset_password_token, token) + end +end diff --git a/config/routes.rb b/config/routes.rb index 61216cbba65..ce0f413c46e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -71,7 +71,8 @@ devise_for :users, controllers: { registrations: 'user/registrations', sessions: 'user/sessions', - masquerades: 'user/masquerades' + masquerades: 'user/masquerades', + passwords: 'user/passwords', } get 'csrf_token' => 'csrf_token#csrf_token' From 86cc87ca66b05359e9647092d25c550bec51574f Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:16:47 +0800 Subject: [PATCH 049/173] feat(registrations_controller): responds to JSON --- .../user/registrations_controller.rb | 21 ++++++++++++++----- .../user/registrations/create.json.jbuilder | 3 +++ 2 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 app/views/user/registrations/create.json.jbuilder diff --git a/app/controllers/user/registrations_controller.rb b/app/controllers/user/registrations_controller.rb index c9e850daaa4..6e1a50671a2 100644 --- a/app/controllers/user/registrations_controller.rb +++ b/app/controllers/user/registrations_controller.rb @@ -8,9 +8,18 @@ class User::RegistrationsController < Devise::RegistrationsController def new if @invitation&.confirmed? message = @invitation.confirmer ? t('.used_with_email', email: @invitation.confirmer.email) : t('.used') - redirect_to root_path, danger: message + render json: { message: message }, status: :conflict and return + elsif @invitation + course = @invitation.course + + render json: { + name: @invitation.name, + email: @invitation.email, + courseTitle: course.title, + courseId: course.id + } else - super + head :no_content end end @@ -18,13 +27,15 @@ def new def create unless verify_recaptcha build_resource(sign_up_params) - flash.now[:alert] = t('user.registrations.create.verify_recaptcha_alert') - flash.delete :recaptcha_error - return render :new + render json: { errors: t('user.registrations.create.verify_recaptcha_alert') }, status: :unprocessable_entity + return end + User.transaction do super + @invitation.confirm!(confirmer: resource) if @invitation && !@invitation.confirmed? && resource.persisted? + @user = resource end end diff --git a/app/views/user/registrations/create.json.jbuilder b/app/views/user/registrations/create.json.jbuilder new file mode 100644 index 00000000000..51f3133daef --- /dev/null +++ b/app/views/user/registrations/create.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true +json.id @user.id +json.confirmed @user.confirmed? From 8edcb29efbb37ce0c407367450ca8db53f6dc86c Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:16:58 +0800 Subject: [PATCH 050/173] feat(confirmations_controller): responds to JSON --- app/controllers/user/confirmations_controller.rb | 16 ++++++++++++++++ config/routes.rb | 1 + 2 files changed, 17 insertions(+) create mode 100644 app/controllers/user/confirmations_controller.rb diff --git a/app/controllers/user/confirmations_controller.rb b/app/controllers/user/confirmations_controller.rb new file mode 100644 index 00000000000..83d62d851b7 --- /dev/null +++ b/app/controllers/user/confirmations_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +class User::ConfirmationsController < Devise::ConfirmationsController + respond_to :json + + def show + super do |email| + if email.persisted? && email.confirmed? + render json: { email: email.email } + else + render json: { error: 'Invalid token' }, status: :bad_request + end + + return + end + end +end diff --git a/config/routes.rb b/config/routes.rb index ce0f413c46e..7ced1146b24 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -73,6 +73,7 @@ sessions: 'user/sessions', masquerades: 'user/masquerades', passwords: 'user/passwords', + confirmations: 'user/confirmations' } get 'csrf_token' => 'csrf_token#csrf_token' From c51fc8c205c0ac637245399f982364d0dbb8c1b3 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:17:10 +0800 Subject: [PATCH 051/173] feat(sessions_controller): responds to JSON --- app/controllers/user/sessions_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/user/sessions_controller.rb b/app/controllers/user/sessions_controller.rb index f4763dac4fe..c776cdcf50b 100644 --- a/app/controllers/user/sessions_controller.rb +++ b/app/controllers/user/sessions_controller.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true class User::SessionsController < Devise::SessionsController + respond_to :json + # before_filter :configure_sign_in_params, only: [:create] # GET /resource/sign_in From 1e37f478db31a6ba0ed78cb1b19a6b9f1d5913ad Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:17:22 +0800 Subject: [PATCH 052/173] style(locales): remove unused translations --- .../assessment/question/text_response.yml | 30 ------------------- .../locales/en/course/lesson_plan/items.yml | 3 -- config/locales/en/layout.yml | 11 ------- config/locales/en/user/admin.yml | 5 ---- config/locales/en/user/registrations.yml | 4 --- config/locales/en/user/sessions.yml | 2 +- .../assessment/question/text_response.yml | 30 ------------------- .../locales/zh/course/lesson_plan/items.yml | 3 -- config/locales/zh/layout.yml | 11 ------- config/locales/zh/user/admin.yml | 5 ---- config/locales/zh/user/registrations.yml | 4 --- config/locales/zh/user/sessions.yml | 2 +- 12 files changed, 2 insertions(+), 108 deletions(-) delete mode 100644 config/locales/en/course/assessment/question/text_response.yml delete mode 100644 config/locales/en/user/admin.yml delete mode 100644 config/locales/zh/course/assessment/question/text_response.yml delete mode 100644 config/locales/zh/user/admin.yml diff --git a/config/locales/en/course/assessment/question/text_response.yml b/config/locales/en/course/assessment/question/text_response.yml deleted file mode 100644 index 59ace807af5..00000000000 --- a/config/locales/en/course/assessment/question/text_response.yml +++ /dev/null @@ -1,30 +0,0 @@ -en: - course: - assessment: - question: - text_responses: - form_comprehension: - multiline_explanation_comprehension_html: > - Adding solutions allows the question to be autograded. Students can only input plaintext. - add_group: 'Add Group' - comprehension: 'Comprehension Question' - text_response_autograde: 'Note: If no solutions are provided, the autograder will always award the maximum grade.' - comprehension_group_fields: - group: 'Group' - remove_group: 'Remove Group' - maximum_group_grade: 'Maximum Grade for this Group' - add_point: 'Add Point' - comprehension_point_fields: - point: 'Point' - remove_point: 'Remove Point' - point_grade: 'Grade for this Point' - add_solution: 'Add Solution' - solution_type: 'Type' - solution: 'Solution' - information: 'Word from Text Passage' - comprehension_solution_fields: - information_hint: 'The exact word from the text passage.' - add_solution_word: 'Add Solution Word' - remove: 'Remove' - compre_keyword: 'Keyword' - compre_lifted_word: 'Lifted Word' diff --git a/config/locales/en/course/lesson_plan/items.yml b/config/locales/en/course/lesson_plan/items.yml index 4b50e3e045c..3e4ce95a8f9 100644 --- a/config/locales/en/course/lesson_plan/items.yml +++ b/config/locales/en/course/lesson_plan/items.yml @@ -5,9 +5,6 @@ en: sidebar_title: :'course.lesson_plan.items.index.header' index: header: 'Lesson Plan' - ref: 'Ref: ' - fixed_desc: > - The personalized timeline for this item has been fixed. It will no longer be automatically modified. activerecord: attributes: course/lesson_plan/item: diff --git a/config/locales/en/layout.yml b/config/locales/en/layout.yml index 227ec2d48d4..5b90d1fe3ac 100644 --- a/config/locales/en/layout.yml +++ b/config/locales/en/layout.yml @@ -1,17 +1,6 @@ en: layout: coursemology: 'Coursemology' - navbar: - toggle_navigation: 'Toggle Navigation' - courses: 'Courses' - all_courses: 'All Courses' - help: 'Help' - register: 'Register' - sign_in: 'Sign In' - sign_out: 'Sign Out' - admin_panel: 'Administration Panel' - instance_admin_panel: 'Instance Administration Panel' - stop_masquerading: '(Stop Masquerading)' layouts: course_admin: title: 'Course Settings' diff --git a/config/locales/en/user/admin.yml b/config/locales/en/user/admin.yml deleted file mode 100644 index 011062284ca..00000000000 --- a/config/locales/en/user/admin.yml +++ /dev/null @@ -1,5 +0,0 @@ -en: - user: - admin: - navbar: - account_settings: :'user.registrations.edit.header' diff --git a/config/locales/en/user/registrations.yml b/config/locales/en/user/registrations.yml index 845e2c6fd45..15c10c5443a 100644 --- a/config/locales/en/user/registrations.yml +++ b/config/locales/en/user/registrations.yml @@ -3,10 +3,6 @@ en: registrations: new: header: :'layout.navbar.register' - already_registered_html: > - Already registered? If you have an existing Coursemology account, you can %{sign_in}. - password_hint: '#{length} characters minimum' - sign_up: :'user.registrations.new.header' used: > The provided invitation code has been claimed by another account, please use that account to log in instead. used_with_email: > diff --git a/config/locales/en/user/sessions.yml b/config/locales/en/user/sessions.yml index a95637a5321..89ca98ec80c 100644 --- a/config/locales/en/user/sessions.yml +++ b/config/locales/en/user/sessions.yml @@ -3,4 +3,4 @@ en: sessions: new: header: :'layout.navbar.sign_in' - sign_in: :'user.sessions.new.header' + diff --git a/config/locales/zh/course/assessment/question/text_response.yml b/config/locales/zh/course/assessment/question/text_response.yml deleted file mode 100644 index f09fe96cf54..00000000000 --- a/config/locales/zh/course/assessment/question/text_response.yml +++ /dev/null @@ -1,30 +0,0 @@ -zh: - course: - assessment: - question: - text_responses: - form_comprehension: - multiline_explanation_comprehension_html: > - 添加解决方案可以使问题自动评分,学生只能输入纯文本 - add_group: '添加组' - comprehension: '理解题' - text_response_autograde: '注意:如果没有提供答案,自动评分器将始终授予最高分数。' - comprehension_group_fields: - group: '组' - remove_group: '移除组' - maximum_group_grade: '该组的最高分' - add_point: '添加要点' - comprehension_point_fields: - point: '要点' - remove_point: '移除要点' - point_grade: '该要点的得分' - add_solution: '添加答案' - solution_type: '类型' - solution: '答案' - information: '段落中的文本' - comprehension_solution_fields: - information_hint: '文字段落中的确切字句。' - add_solution_word: '添加答案文本' - remove: '移除' - compre_keyword: '关键词' - compre_lifted_word: '要点词' diff --git a/config/locales/zh/course/lesson_plan/items.yml b/config/locales/zh/course/lesson_plan/items.yml index 627efaca038..27e7d218ea7 100644 --- a/config/locales/zh/course/lesson_plan/items.yml +++ b/config/locales/zh/course/lesson_plan/items.yml @@ -5,9 +5,6 @@ zh: sidebar_title: :'course.lesson_plan.items.index.header' index: header: '课程计划' - ref: 'Ref: ' - fixed_desc: > - 这个项目的定制化时间线已被修复,它将不再被自动修改。 activerecord: attributes: course/lesson_plan/item: diff --git a/config/locales/zh/layout.yml b/config/locales/zh/layout.yml index 580407cb2c6..57bdc3d7e3d 100644 --- a/config/locales/zh/layout.yml +++ b/config/locales/zh/layout.yml @@ -1,17 +1,6 @@ zh: layout: coursemology: 'Coursemology' - navbar: - toggle_navigation: '切换导航' - courses: '课程' - all_courses: '全部课程' - help: '帮助' - register: '注册' - sign_in: '登录' - sign_out: '登出' - admin_panel: '管理小组' - instance_admin_panel: '实例管理小组' - stop_masquerading: '(停止伪装)' layouts: course_admin: title: '课程设置' diff --git a/config/locales/zh/user/admin.yml b/config/locales/zh/user/admin.yml deleted file mode 100644 index da6af697d51..00000000000 --- a/config/locales/zh/user/admin.yml +++ /dev/null @@ -1,5 +0,0 @@ -zh: - user: - admin: - navbar: - account_settings: :'user.registrations.edit.header' diff --git a/config/locales/zh/user/registrations.yml b/config/locales/zh/user/registrations.yml index 65730b7fb0d..f4602ae2640 100644 --- a/config/locales/zh/user/registrations.yml +++ b/config/locales/zh/user/registrations.yml @@ -3,10 +3,6 @@ zh: registrations: new: header: :'layout.navbar.register' - already_registered_html: > - 已经注册过? 如果你已经拥有一个Coursemology账号, 你可以直接%{sign_in}. - password_hint: '至少需要 #{length} 个字符' - sign_up: :'user.registrations.new.header' used: > 所提供的邀请码已被另一个账户所使用,请使用该账户登录。 used_with_email: > diff --git a/config/locales/zh/user/sessions.yml b/config/locales/zh/user/sessions.yml index 7f290e4859c..99ca047f037 100644 --- a/config/locales/zh/user/sessions.yml +++ b/config/locales/zh/user/sessions.yml @@ -3,4 +3,4 @@ zh: sessions: new: header: :'layout.navbar.sign_in' - sign_in: :'user.sessions.new.header' + From 8311277ff19909cfad59906389b66d13c234f390 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:19:22 +0800 Subject: [PATCH 053/173] feat(index): returns last course accessed times --- app/views/application/index.json.jbuilder | 5 +++++ client/app/types/home.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/views/application/index.json.jbuilder b/app/views/application/index.json.jbuilder index 7edba331f2c..76c4e750156 100644 --- a/app/views/application/index.json.jbuilder +++ b/app/views/application/index.json.jbuilder @@ -1,10 +1,15 @@ # frozen_string_literal: true if user_signed_in? my_courses = Course.containing_user(current_user).ordered_by_start_at + course_last_active_times_hash = CourseUser.for_user(current_user).pluck(:course_id, :last_active_at).to_h + if my_courses.present? json.courses my_courses do |course| + json.id course.id json.title course.title json.url course_path(course) + json.logoUrl url_to_course_logo(course) + json.lastActiveAt course_last_active_times_hash[course.id] end end diff --git a/client/app/types/home.ts b/client/app/types/home.ts index 6b807b45974..fa9a895409e 100644 --- a/client/app/types/home.ts +++ b/client/app/types/home.ts @@ -9,8 +9,16 @@ interface HomeLayoutUserData { instanceRole: InstanceUserRoles; } +export interface HomeLayoutCourseData { + id: number; + title: string; + url: string; + logoUrl: string; + lastActiveAt: string | null; +} + export interface HomeLayoutData { - courses?: { title: string; url: string }[]; + courses?: HomeLayoutCourseData[]; user?: HomeLayoutUserData; signOutUrl?: string; masqueradeUserName?: string; From 8e7a0db3e19e42834fcfed8606965ee15fbf6227 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:22:22 +0800 Subject: [PATCH 054/173] feat(index): returns primary email --- app/views/application/index.json.jbuilder | 5 +++-- client/app/types/home.ts | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/application/index.json.jbuilder b/app/views/application/index.json.jbuilder index 76c4e750156..5140f0d5cf0 100644 --- a/app/views/application/index.json.jbuilder +++ b/app/views/application/index.json.jbuilder @@ -14,15 +14,16 @@ if user_signed_in? end json.user do + json.id current_user.id json.name current_user.name + json.primaryEmail current_user.email json.url user_path(current_user) json.avatarUrl user_image(current_user) json.role current_user.role json.instanceRole controller.current_instance_user&.role + json.canCreateNewCourse can?(:create, Course.new) end - json.signOutUrl destroy_user_session_path - if user_masquerade? json.masqueradeUserName current_user.name json.stopMasqueradingUrl back_masquerade_path(current_user) diff --git a/client/app/types/home.ts b/client/app/types/home.ts index fa9a895409e..cf617ce874e 100644 --- a/client/app/types/home.ts +++ b/client/app/types/home.ts @@ -2,11 +2,14 @@ import { InstanceUserRoles } from './system/instance/users'; import { UserRoles } from './users'; interface HomeLayoutUserData { + id: number; name: string; + primaryEmail: string; url: string; avatarUrl: string; role: UserRoles; instanceRole: InstanceUserRoles; + canCreateNewCourse: boolean; } export interface HomeLayoutCourseData { @@ -20,7 +23,6 @@ export interface HomeLayoutCourseData { export interface HomeLayoutData { courses?: HomeLayoutCourseData[]; user?: HomeLayoutUserData; - signOutUrl?: string; masqueradeUserName?: string; stopMasqueradingUrl?: string; } From 8543d02fcc897ce24fea1773c508b7ad7fd2036b Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:22:41 +0800 Subject: [PATCH 055/173] feat(index): returns locale, time zone --- app/views/application/index.json.jbuilder | 3 +++ client/app/types/home.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/app/views/application/index.json.jbuilder b/app/views/application/index.json.jbuilder index 5140f0d5cf0..8c16b817077 100644 --- a/app/views/application/index.json.jbuilder +++ b/app/views/application/index.json.jbuilder @@ -1,4 +1,7 @@ # frozen_string_literal: true +json.locale I18n.locale +json.timeZone ActiveSupport::TimeZone::MAPPING[user_time_zone] + if user_signed_in? my_courses = Course.containing_user(current_user).ordered_by_start_at course_last_active_times_hash = CourseUser.for_user(current_user).pluck(:course_id, :last_active_at).to_h diff --git a/client/app/types/home.ts b/client/app/types/home.ts index cf617ce874e..0bcb29c5a44 100644 --- a/client/app/types/home.ts +++ b/client/app/types/home.ts @@ -21,6 +21,8 @@ export interface HomeLayoutCourseData { } export interface HomeLayoutData { + locale: string; + timeZone: string | null; courses?: HomeLayoutCourseData[]; user?: HomeLayoutUserData; masqueradeUserName?: string; From 193955d9576057fcfa82835f2867bf3fe06533f1 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Mon, 10 Jul 2023 13:36:16 +0800 Subject: [PATCH 056/173] chore(deps): bump react-router-dom from 6.11.1 to 6.14.1 in /client --- client/package.json | 2 +- client/yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/client/package.json b/client/package.json index 90963bf8d05..7fd3e5c1f0a 100644 --- a/client/package.json +++ b/client/package.json @@ -89,7 +89,7 @@ "react-player": "^2.12.0", "react-redux": "^8.1.2", "react-resizable": "^3.0.5", - "react-router-dom": "^6.11.1", + "react-router-dom": "^6.14.1", "react-scroll": "^1.8.9", "react-toastify": "^9.1.3", "react-tooltip": "^5.19.0", diff --git a/client/yarn.lock b/client/yarn.lock index afba1ef7cba..4a8844c8de0 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1965,10 +1965,10 @@ redux-thunk "^2.4.2" reselect "^4.1.8" -"@remix-run/router@1.6.2": - version "1.6.2" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.6.2.tgz#bbe75f8c59e0b7077584920ce2cc76f8f354934d" - integrity sha512-LzqpSrMK/3JBAVBI9u3NWtOhWNw5AMQfrUFYB0+bDHTSw17z++WJLsPsxAuK+oSddsxk4d7F/JcdDPM1M5YAhA== +"@remix-run/router@1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498" + integrity sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ== "@rollbar/react@^0.11.2": version "0.11.2" @@ -8335,20 +8335,20 @@ react-resizable@^3.0.5: prop-types "15.x" react-draggable "^4.0.3" -react-router-dom@^6.11.1: - version "6.11.2" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.11.2.tgz#324d55750ffe2ecd54ca4ec6b7bc7ab01741f170" - integrity sha512-JNbKtAeh1VSJQnH6RvBDNhxNwemRj7KxCzc5jb7zvDSKRnPWIFj9pO+eXqjM69gQJ0r46hSz1x4l9y0651DKWw== +react-router-dom@^6.14.1: + version "6.14.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.1.tgz#0ad7ba7abdf75baa61169d49f096f0494907a36f" + integrity sha512-ssF6M5UkQjHK70fgukCJyjlda0Dgono2QGwqGvuk7D+EDGHdacEN3Yke2LTMjkrpHuFwBfDFsEjGVXBDmL+bWw== dependencies: - "@remix-run/router" "1.6.2" - react-router "6.11.2" + "@remix-run/router" "1.7.1" + react-router "6.14.1" -react-router@6.11.2: - version "6.11.2" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.11.2.tgz#006301c4da1a173d7ad76b7ecd2da01b9dd3837a" - integrity sha512-74z9xUSaSX07t3LM+pS6Un0T55ibUE/79CzfZpy5wsPDZaea1F8QkrsiyRnA2YQ7LwE/umaydzXZV80iDCPkMg== +react-router@6.14.1: + version "6.14.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.1.tgz#5e82bcdabf21add859dc04b1859f91066b3a5810" + integrity sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g== dependencies: - "@remix-run/router" "1.6.2" + "@remix-run/router" "1.7.1" react-scroll@^1.8.9: version "1.8.9" From 4a59373e769a77c8de28d468ddc0b255101b7908 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:28:38 +0800 Subject: [PATCH 057/173] feat(api): add CSRF recycler, 401/403 redirects --- client/app/api/Base.js | 22 ----- client/app/api/Base.ts | 87 ++++++++++++++++++ client/app/lib/hooks/router/redirect.tsx | 109 +++++++++++++++++++++++ 3 files changed, 196 insertions(+), 22 deletions(-) delete mode 100644 client/app/api/Base.js create mode 100644 client/app/api/Base.ts create mode 100644 client/app/lib/hooks/router/redirect.tsx diff --git a/client/app/api/Base.js b/client/app/api/Base.js deleted file mode 100644 index ec18fc7240b..00000000000 --- a/client/app/api/Base.js +++ /dev/null @@ -1,22 +0,0 @@ -import axios from 'axios'; - -import { csrfToken } from 'lib/helpers/server-context'; - -export default class BaseAPI { - #client; - - constructor() { - this.#client = null; - } - - /** Returns the API client */ - get client() { - if (this.#client) return this.#client; - - const headers = { Accept: 'application/json', 'X-CSRF-Token': csrfToken }; - const params = { format: 'json' }; - - this.#client = axios.create({ headers, params }); - return this.#client; - } -} diff --git a/client/app/api/Base.ts b/client/app/api/Base.ts new file mode 100644 index 00000000000..173a4f182c5 --- /dev/null +++ b/client/app/api/Base.ts @@ -0,0 +1,87 @@ +import axios, { AxiosInstance, AxiosResponse } from 'axios'; + +import { + redirectToForbidden, + redirectToSignIn, +} from 'lib/hooks/router/redirect'; + +const isInvalidCSRFTokenResponse = (response?: AxiosResponse): boolean => + response?.status === 403 && + response.data?.error?.title?.toLowerCase().includes('csrf token'); + +const isUnauthenticatedResponse = (response?: AxiosResponse): boolean => + response?.status === 401 && + response.data?.error?.toLowerCase().includes('sign in or sign up'); + +const isUnauthorizedResponse = (response?: AxiosResponse): boolean => + response?.status === 403 && + response.data?.errors?.toLowerCase().includes('not authorized'); + +const MAX_CSRF_RETRIES = 3; + +export default class BaseAPI { + #client: AxiosInstance | null = null; + + #retries = 0; + + /** Returns the API client */ + get client(): AxiosInstance { + this.#client ??= this.#createAxiosInstance(); + return this.#client; + } + + #createAxiosInstance(): AxiosInstance { + const client = axios.create({ + headers: { Accept: 'application/json' }, + params: { format: 'json' }, + }); + + client.interceptors.request.use(async (config) => { + config.withCredentials = true; + if (config.method === 'get') return config; + + config.headers['X-CSRF-Token'] = await this.#getAndSaveCSRFToken(); + return config; + }); + + client.interceptors.response.use( + (response) => { + if (response.config.method !== 'get') this.#retries = 0; + + return response; + }, + async (error) => { + if ( + isInvalidCSRFTokenResponse(error.response) && + this.#retries < MAX_CSRF_RETRIES + ) { + BaseAPI.#clearCSRFToken(); + this.#retries += 1; + return client.request(error.config); + } + + if (isUnauthenticatedResponse(error.response)) redirectToSignIn(true); + + if (isUnauthorizedResponse(error.response)) redirectToForbidden(); + + return Promise.reject(error); + }, + ); + + return client; + } + + static #clearCSRFToken(): void { + globalThis._CSRF_TOKEN = undefined; + } + + async #getAndSaveCSRFToken(): Promise { + globalThis._CSRF_TOKEN ??= await this.#getCSRFToken(); + return globalThis._CSRF_TOKEN; + } + + async #getCSRFToken(): Promise { + const response = await this.#client?.get('/csrf_token'); + return response?.data.csrfToken; + } +} diff --git a/client/app/lib/hooks/router/redirect.tsx b/client/app/lib/hooks/router/redirect.tsx new file mode 100644 index 00000000000..687a781498a --- /dev/null +++ b/client/app/lib/hooks/router/redirect.tsx @@ -0,0 +1,109 @@ +import { Navigate, useSearchParams } from 'react-router-dom'; + +const NEXT_URL_SEARCH_PARAM = 'next'; +const EXPIRED_SESSION_SEARCH_PARAM = 'expired'; +const FORBIDDEN_SOURCE_URL_SEARCH_PARAM = 'from'; + +/** + * Defensively parse a URL, returning `null` if a valid URL cannot be created. This + * is because the `URL` constructor throws a `TypeError` if the URL is invalid. We + * don't want to block page load just because of an invalid URL. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/URL/URL#exceptions + */ +const defensivelyParseURL = (rawURL: string): string | null => { + try { + const url = new URL(rawURL, window.location.origin); + return url.pathname + url.search; + } catch { + return null; + } +}; + +const getCurrentURL = (): string => + window.location.pathname + window.location.search; + +const getAuthenticatableURL = (nextURL?: string, expired?: boolean): string => { + const url = new URL('/users/sign_in', window.location.origin); + if (nextURL) url.searchParams.append(NEXT_URL_SEARCH_PARAM, nextURL); + if (expired) url.searchParams.append(EXPIRED_SESSION_SEARCH_PARAM, 'true'); + return url.pathname + url.search; +}; + +const getForbiddenURL = (): string => { + const url = new URL('/forbidden', window.location.origin); + url.searchParams.append(FORBIDDEN_SOURCE_URL_SEARCH_PARAM, getCurrentURL()); + return url.pathname + url.search; +}; + +const getDefaultNotFoundURL = (): string => { + const chunks = window.location.host.split('.'); + if (chunks.length < 3) return '/404'; + + const protocol = window.location.protocol; + const domainName = chunks.at(-2); + const topLevelDomain = chunks.at(-1); + + const url = new URL(`${protocol}//${domainName}.${topLevelDomain}/404`); + + return url.toString(); +}; + +const useNextURL = (): { nextURL: string | null; expired: boolean } => { + const [searchParams] = useSearchParams(); + const nextRawURL = searchParams.get(NEXT_URL_SEARCH_PARAM); + const expired = searchParams.get(EXPIRED_SESSION_SEARCH_PARAM); + + return { + nextURL: nextRawURL && defensivelyParseURL(nextRawURL), + expired: Boolean(expired), + }; +}; + +/** + * Redirects to the sign in page with the current URL as the next URL. To be used + * in scopes outside React and/or React Router, e.g., Axios interceptors. + * + * @param expired Whether this redirect is caused by an expired session. + */ +export const redirectToSignIn = (expired?: boolean): void => { + window.location.href = getAuthenticatableURL(getCurrentURL(), expired); +}; + +export const redirectToForbidden = (): void => { + window.location.href = getForbiddenURL(); +}; + +export const redirectToDefaultNotFound = (): void => { + window.location.href = getDefaultNotFoundURL(); +}; + +export const getForbiddenSourceURL = (rawURL: string): string | null => { + const url = new URL(rawURL); + return url.searchParams.get(FORBIDDEN_SOURCE_URL_SEARCH_PARAM); +}; + +/** + * Redirects to the next URL if it exists, otherwise redirects to the home page. + */ +export const Redirectable = (): JSX.Element => { + const { nextURL } = useNextURL(); + return ; +}; + +/** + * Redirects to the sign in page with the current intercepted URL as the next URL. + */ +export const Authenticatable = (): JSX.Element => { + const redirectURL = getAuthenticatableURL(getCurrentURL()); + return ; +}; + +export const useRedirectable = (): { + redirectable: boolean; + expired: boolean; +} => { + const { nextURL, expired } = useNextURL(); + + return { redirectable: Boolean(nextURL?.trim()), expired }; +}; From 051b5a0becbcba8b1be5f9c55f9b64bc3df61e59 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:27:15 +0800 Subject: [PATCH 058/173] test(jest): make MockAdapter work with CSRF recycler --- client/app/__test__/mocks/ResizeObserver.js | 2 ++ client/app/__test__/mocks/axiosMock.js | 18 ++++++++++++++++++ .../LessonPlanSettings/__test__/index.test.tsx | 4 ++-- .../__test__/index.test.tsx | 4 ++-- .../FileManager/__test__/index.test.tsx | 5 ++--- .../question/scribing/__test__/index.test.tsx | 6 +++--- .../scribing/__test__/responses.test.ts | 4 ++-- .../__test__/SavingIndicator.test.tsx | 4 ++-- .../__test__/ScribingToolbar.test.jsx | 4 ++-- .../__test__/ObjectDuplication.test.jsx | 4 ++-- .../LessonPlanLayout/__test__/index.test.jsx | 4 ++-- .../LessonPlanEdit/__test__/ItemRow.test.jsx | 4 ++-- .../__test__/MilestoneRow.test.jsx | 4 ++-- .../course/survey/__test__/index.test.tsx | 4 ++-- .../survey/actions/__test__/responses.test.ts | 4 ++-- .../RespondButton/__test__/index.test.jsx | 4 ++-- .../pages/ResponseEdit/__test__/index.test.jsx | 4 ++-- .../ResponseIndex/__test__/index.test.jsx | 4 ++-- .../SurveyResults/__test__/index.test.jsx | 4 ++-- .../submission/actions/__test__/video.test.js | 4 ++-- client/tsconfig.json | 3 ++- 21 files changed, 59 insertions(+), 39 deletions(-) create mode 100644 client/app/__test__/mocks/axiosMock.js diff --git a/client/app/__test__/mocks/ResizeObserver.js b/client/app/__test__/mocks/ResizeObserver.js index d28d97506c9..02b3e8d25ab 100644 --- a/client/app/__test__/mocks/ResizeObserver.js +++ b/client/app/__test__/mocks/ResizeObserver.js @@ -1,6 +1,8 @@ class ResizeObserver { observe() {} + unobserve() {} + disconnect() {} } diff --git a/client/app/__test__/mocks/axiosMock.js b/client/app/__test__/mocks/axiosMock.js new file mode 100644 index 00000000000..321d17169fe --- /dev/null +++ b/client/app/__test__/mocks/axiosMock.js @@ -0,0 +1,18 @@ +import MockAdapter from 'axios-mock-adapter'; + +const registerCSRFTokenMockHandler = (mock) => { + mock.onGet('/csrf_token').reply(200, { csrfToken: 'mock_csrf_token' }); +}; + +export const createMockAdapter = (instance) => { + const mock = new MockAdapter(instance); + registerCSRFTokenMockHandler(mock); + + return Object.assign(mock, { + reset: () => { + mock.resetHandlers(); + mock.resetHistory(); + registerCSRFTokenMockHandler(mock); + }, + }); +}; diff --git a/client/app/bundles/course/admin/pages/LessonPlanSettings/__test__/index.test.tsx b/client/app/bundles/course/admin/pages/LessonPlanSettings/__test__/index.test.tsx index e4f585601c2..fe8240e4cda 100644 --- a/client/app/bundles/course/admin/pages/LessonPlanSettings/__test__/index.test.tsx +++ b/client/app/bundles/course/admin/pages/LessonPlanSettings/__test__/index.test.tsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { fireEvent, render, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; @@ -26,7 +26,7 @@ const expectedPayload = { }, }; -const mock = new MockAdapter(CourseAPI.admin.lessonPlan.client); +const mock = createMockAdapter(CourseAPI.admin.lessonPlan.client); describe('', () => { it('allow lesson plan item settings to be set', async () => { diff --git a/client/app/bundles/course/admin/pages/NotificationSettings/__test__/index.test.tsx b/client/app/bundles/course/admin/pages/NotificationSettings/__test__/index.test.tsx index 5d092e044de..26aabff3dea 100644 --- a/client/app/bundles/course/admin/pages/NotificationSettings/__test__/index.test.tsx +++ b/client/app/bundles/course/admin/pages/NotificationSettings/__test__/index.test.tsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { fireEvent, render, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; @@ -24,7 +24,7 @@ const expectedPayload = { }, }; -const mock = new MockAdapter(CourseAPI.admin.notifications.client); +const mock = createMockAdapter(CourseAPI.admin.notifications.client); describe('', () => { it('allow emails notification settings to be set', async () => { diff --git a/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx b/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx index 52a572dce35..813ee464d05 100644 --- a/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx +++ b/client/app/bundles/course/assessment/components/FileManager/__test__/index.test.tsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { act, fireEvent, render, RenderResult, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; @@ -29,8 +29,7 @@ const NEW_MATERIAL = { deleting: false, }; -const client = CourseAPI.materialFolders.client; -const mock = new MockAdapter(client); +const mock = createMockAdapter(CourseAPI.materialFolders.client); let fileManager: RenderResult; beforeEach(() => { diff --git a/client/app/bundles/course/assessment/question/scribing/__test__/index.test.tsx b/client/app/bundles/course/assessment/question/scribing/__test__/index.test.tsx index 54d77bec064..791d50d4e39 100644 --- a/client/app/bundles/course/assessment/question/scribing/__test__/index.test.tsx +++ b/client/app/bundles/course/assessment/question/scribing/__test__/index.test.tsx @@ -1,11 +1,11 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { fireEvent, render, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; import ScribingQuestion from 'course/assessment/question/scribing/ScribingQuestion'; -const mock = new MockAdapter(CourseAPI.assessment.question.scribing.client); -const assessmentsMock = new MockAdapter( +const mock = createMockAdapter(CourseAPI.assessment.question.scribing.client); +const assessmentsMock = createMockAdapter( CourseAPI.assessment.assessments.client, ); diff --git a/client/app/bundles/course/assessment/question/scribing/__test__/responses.test.ts b/client/app/bundles/course/assessment/question/scribing/__test__/responses.test.ts index c1b519f0fee..795ad7d3f49 100644 --- a/client/app/bundles/course/assessment/question/scribing/__test__/responses.test.ts +++ b/client/app/bundles/course/assessment/question/scribing/__test__/responses.test.ts @@ -1,5 +1,5 @@ import { waitFor } from '@testing-library/react'; -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { dispatch } from 'store'; import CourseAPI from 'api/course'; @@ -9,7 +9,7 @@ import { createScribingQuestion, updateScribingQuestion } from '../operations'; // Mock axios const client = CourseAPI.assessment.question.scribing.client; -const mock = new MockAdapter(client); +const mock = createMockAdapter(client); beforeEach(() => { mock.reset(); diff --git a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/SavingIndicator.test.tsx b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/SavingIndicator.test.tsx index ea5cd7fa5dd..01d681a5c04 100644 --- a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/SavingIndicator.test.tsx +++ b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/SavingIndicator.test.tsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { dispatch } from 'store'; import { render, waitFor } from 'test-utils'; @@ -8,7 +8,7 @@ import ScribingView from 'course/assessment/submission/containers/ScribingView'; import { updateScribingAnswer } from '../../../actions/scribing'; import actionTypes from '../../../constants'; -const mock = new MockAdapter(CourseAPI.assessment.answer.scribing.client); +const mock = createMockAdapter(CourseAPI.assessment.answer.scribing.client); const assessmentId = 1; const submissionId = 2; diff --git a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx index 2e3fa8d411e..6e1d6df73cb 100644 --- a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx +++ b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { dispatch } from 'store'; import { act, fireEvent, render } from 'test-utils'; @@ -15,7 +15,7 @@ import actionTypes, { } from '../../../constants'; const client = CourseAPI.assessment.answer.scribing.client; -const mock = new MockAdapter(client); +const mock = createMockAdapter(client); const assessmentId = 1; const submissionId = 2; diff --git a/client/app/bundles/course/duplication/pages/Duplication/__test__/ObjectDuplication.test.jsx b/client/app/bundles/course/duplication/pages/Duplication/__test__/ObjectDuplication.test.jsx index 11ebdcd1d71..25ec78abe30 100644 --- a/client/app/bundles/course/duplication/pages/Duplication/__test__/ObjectDuplication.test.jsx +++ b/client/app/bundles/course/duplication/pages/Duplication/__test__/ObjectDuplication.test.jsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { store } from 'store'; import { render, waitFor } from 'test-utils'; @@ -7,7 +7,7 @@ import CourseAPI from 'api/course'; import ObjectDuplication from '../index'; const client = CourseAPI.duplication.client; -const mock = new MockAdapter(client); +const mock = createMockAdapter(client); const responseData = { sourceCourse: { id: 5 }, diff --git a/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/index.test.jsx b/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/index.test.jsx index 1d35b8968dc..7ca44bee468 100644 --- a/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/index.test.jsx +++ b/client/app/bundles/course/lesson-plan/containers/LessonPlanLayout/__test__/index.test.jsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { render, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; @@ -6,7 +6,7 @@ import CourseAPI from 'api/course'; import LessonPlanLayout from '..'; const client = CourseAPI.lessonPlan.client; -const mock = new MockAdapter(client); +const mock = createMockAdapter(client); beforeEach(() => { mock.reset(); diff --git a/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/ItemRow.test.jsx b/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/ItemRow.test.jsx index e72d108ae32..a8cc1ddd790 100644 --- a/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/ItemRow.test.jsx +++ b/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/ItemRow.test.jsx @@ -1,11 +1,11 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { fireEvent, render, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; import ItemRow from '../ItemRow'; -const mock = new MockAdapter(CourseAPI.lessonPlan.client); +const mock = createMockAdapter(CourseAPI.lessonPlan.client); const startAt = '01-01-2017'; const endAt = '02-02-2017'; diff --git a/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/MilestoneRow.test.jsx b/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/MilestoneRow.test.jsx index c250ebf04a1..6e0aee4ee9f 100644 --- a/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/MilestoneRow.test.jsx +++ b/client/app/bundles/course/lesson-plan/pages/LessonPlanEdit/__test__/MilestoneRow.test.jsx @@ -1,11 +1,11 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { fireEvent, render, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; import MilestoneRow from '../MilestoneRow'; -const mock = new MockAdapter(CourseAPI.lessonPlan.client); +const mock = createMockAdapter(CourseAPI.lessonPlan.client); beforeEach(() => { mock.reset(); diff --git a/client/app/bundles/course/survey/__test__/index.test.tsx b/client/app/bundles/course/survey/__test__/index.test.tsx index fc94da11505..3a66c1049de 100644 --- a/client/app/bundles/course/survey/__test__/index.test.tsx +++ b/client/app/bundles/course/survey/__test__/index.test.tsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { render, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; @@ -18,7 +18,7 @@ const SURVEYS = [ }, ]; -const mock = new MockAdapter(CourseAPI.survey.surveys.client); +const mock = createMockAdapter(CourseAPI.survey.surveys.client); beforeEach(() => { mock.reset(); diff --git a/client/app/bundles/course/survey/actions/__test__/responses.test.ts b/client/app/bundles/course/survey/actions/__test__/responses.test.ts index db202903d74..c39b7427020 100644 --- a/client/app/bundles/course/survey/actions/__test__/responses.test.ts +++ b/client/app/bundles/course/survey/actions/__test__/responses.test.ts @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { dispatch } from 'store'; import CourseAPI from 'api/course'; @@ -7,7 +7,7 @@ import history from 'lib/history'; import { createResponse } from '../responses'; const client = CourseAPI.survey.responses.client; -const mock = new MockAdapter(client); +const mock = createMockAdapter(client); const mockNavigate = jest.fn(); beforeEach(() => { diff --git a/client/app/bundles/course/survey/containers/RespondButton/__test__/index.test.jsx b/client/app/bundles/course/survey/containers/RespondButton/__test__/index.test.jsx index 78a2be26517..3ea1c1d1eb1 100644 --- a/client/app/bundles/course/survey/containers/RespondButton/__test__/index.test.jsx +++ b/client/app/bundles/course/survey/containers/RespondButton/__test__/index.test.jsx @@ -1,11 +1,11 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { fireEvent, render, waitFor } from 'test-utils'; import CourseAPI from 'api/course'; import RespondButton from '../index'; -const mock = new MockAdapter(CourseAPI.survey.responses.client); +const mock = createMockAdapter(CourseAPI.survey.responses.client); beforeEach(() => { mock.reset(); diff --git a/client/app/bundles/course/survey/pages/ResponseEdit/__test__/index.test.jsx b/client/app/bundles/course/survey/pages/ResponseEdit/__test__/index.test.jsx index d6afa29c668..82ee7f1bb03 100644 --- a/client/app/bundles/course/survey/pages/ResponseEdit/__test__/index.test.jsx +++ b/client/app/bundles/course/survey/pages/ResponseEdit/__test__/index.test.jsx @@ -1,7 +1,7 @@ import { connect } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; -import MockAdapter from 'axios-mock-adapter'; import { mount } from 'enzyme'; +import { createMockAdapter } from 'mocks/axiosMock'; import CourseAPI from 'api/course'; import storeCreator from 'course/survey/store'; @@ -9,7 +9,7 @@ import storeCreator from 'course/survey/store'; import ResponseEdit from '../index'; const client = CourseAPI.survey.responses.client; -const mock = new MockAdapter(client); +const mock = createMockAdapter(client); const responseData = { response: { diff --git a/client/app/bundles/course/survey/pages/ResponseIndex/__test__/index.test.jsx b/client/app/bundles/course/survey/pages/ResponseIndex/__test__/index.test.jsx index 8c696f81fc6..85c86d38a05 100644 --- a/client/app/bundles/course/survey/pages/ResponseIndex/__test__/index.test.jsx +++ b/client/app/bundles/course/survey/pages/ResponseIndex/__test__/index.test.jsx @@ -1,11 +1,11 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { render, waitFor, within } from 'test-utils'; import CourseAPI from 'api/course'; import ResponseIndex from '../index'; -const mock = new MockAdapter(CourseAPI.survey.responses.client); +const mock = createMockAdapter(CourseAPI.survey.responses.client); const responsesData = { responses: [ diff --git a/client/app/bundles/course/survey/pages/SurveyResults/__test__/index.test.jsx b/client/app/bundles/course/survey/pages/SurveyResults/__test__/index.test.jsx index 8aea327ddfc..f6eb700e30a 100644 --- a/client/app/bundles/course/survey/pages/SurveyResults/__test__/index.test.jsx +++ b/client/app/bundles/course/survey/pages/SurveyResults/__test__/index.test.jsx @@ -1,4 +1,4 @@ -import MockAdapter from 'axios-mock-adapter'; +import { createMockAdapter } from 'mocks/axiosMock'; import { store } from 'store'; import { fireEvent, render, waitFor } from 'test-utils'; @@ -7,7 +7,7 @@ import CourseAPI from 'api/course'; import { SurveyResults } from '../index'; const client = CourseAPI.survey.surveys.client; -const mock = new MockAdapter(client); +const mock = createMockAdapter(client); const data = { sections: [ diff --git a/client/app/bundles/course/video/submission/actions/__test__/video.test.js b/client/app/bundles/course/video/submission/actions/__test__/video.test.js index 467deee3b4e..76a36aa167a 100644 --- a/client/app/bundles/course/video/submission/actions/__test__/video.test.js +++ b/client/app/bundles/course/video/submission/actions/__test__/video.test.js @@ -1,5 +1,5 @@ -import MockAdapter from 'axios-mock-adapter'; import { List as makeImmutableList, Map as makeImmutableMap } from 'immutable'; +import { createMockAdapter } from 'mocks/axiosMock'; import CourseAPI from 'api/course'; import { playerStates } from 'lib/constants/videoConstants'; @@ -10,7 +10,7 @@ import { changePlayerState, endSession, sendEvents } from '../video'; const videoId = '1'; const client = CourseAPI.video.sessions.client; -const mock = new MockAdapter(client, { delayResponse: 0 }); +const mock = createMockAdapter(client, { delayResponse: 0 }); const oldSessionsFixtures = makeImmutableMap({ 25: { diff --git a/client/tsconfig.json b/client/tsconfig.json index 967e6b6411e..6a56c9ced94 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -29,7 +29,8 @@ "paths": { "lib/*": ["lib/*"], "course/*": ["bundles/course/*"], - "test-utils": ["utilities/test-utils"] + "test-utils": ["utilities/test-utils"], + "mocks/*": ["__test__/mocks/*"] }, "pretty": true, "resolveJsonModule": true, From 32be2caa155ae9b08bacb0eaccffa848999eacfd Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:30:20 +0800 Subject: [PATCH 059/173] feat(ScribingViewComponent): code-split, lazy load components, deps --- .../ScribingView/ScribingCanvas.jsx | 3 +- .../__test__/SavingIndicator.test.tsx | 6 -- .../__test__/ScribingToolbar.test.jsx | 6 -- .../ScribingView/__test__/index.test.tsx | 9 +-- .../components/ScribingView/index.jsx | 64 ++++++++----------- .../submission/loaders/ScribingViewLoader.js | 7 -- 6 files changed, 32 insertions(+), 63 deletions(-) delete mode 100644 client/app/bundles/course/assessment/submission/loaders/ScribingViewLoader.js diff --git a/client/app/bundles/course/assessment/submission/components/ScribingView/ScribingCanvas.jsx b/client/app/bundles/course/assessment/submission/components/ScribingView/ScribingCanvas.jsx index 7cbdd3f2d6e..4e0003bbe58 100644 --- a/client/app/bundles/course/assessment/submission/components/ScribingView/ScribingCanvas.jsx +++ b/client/app/bundles/course/assessment/submission/components/ScribingView/ScribingCanvas.jsx @@ -1,5 +1,6 @@ -/* eslint-disable no-undef, react/sort-comp */ +/* eslint-disable react/sort-comp */ import { Component } from 'react'; +import { fabric } from 'fabric'; import PropTypes from 'prop-types'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; diff --git a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/SavingIndicator.test.tsx b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/SavingIndicator.test.tsx index 01d681a5c04..f42a56fe024 100644 --- a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/SavingIndicator.test.tsx +++ b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/SavingIndicator.test.tsx @@ -54,12 +54,6 @@ const mockSubmission = { ], }; -// stub import function -jest.mock( - 'course/assessment/submission/loaders/ScribingViewLoader', - () => (): Promise => Promise.resolve(), -); - beforeEach(() => { mock.reset(); diff --git a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx index 6e1d6df73cb..06f4f9681d1 100644 --- a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx +++ b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/ScribingToolbar.test.jsx @@ -138,12 +138,6 @@ const props = { setRedo: jest.fn(), }; -// stub import function -jest.mock( - 'course/assessment/submission/loaders/ScribingViewLoader', - () => () => Promise.resolve(), -); - beforeEach(() => { mock.reset(); diff --git a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/index.test.tsx b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/index.test.tsx index 32039fefb3e..daa91fd1627 100644 --- a/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/index.test.tsx +++ b/client/app/bundles/course/assessment/submission/components/ScribingView/__test__/index.test.tsx @@ -50,13 +50,8 @@ const mockSubmission = { ], }; -jest.mock( - 'course/assessment/submission/loaders/ScribingViewLoader', - () => (): Promise => Promise.resolve(), -); - describe('ScribingView', () => { - it('renders canvas', () => { + it('renders canvas', async () => { dispatch({ type: actionTypes.FETCH_SUBMISSION_SUCCESS, payload: mockSubmission, @@ -69,6 +64,6 @@ describe('ScribingView', () => { const page = render(, { at: [url] }); - expect(page.getByTestId(`canvas-${answerId}`)).toBeVisible(); + expect(await page.findByTestId(`canvas-${answerId}`)).toBeVisible(); }); }); diff --git a/client/app/bundles/course/assessment/submission/components/ScribingView/index.jsx b/client/app/bundles/course/assessment/submission/components/ScribingView/index.jsx index e28e2ee30c6..5fff8029796 100644 --- a/client/app/bundles/course/assessment/submission/components/ScribingView/index.jsx +++ b/client/app/bundles/course/assessment/submission/components/ScribingView/index.jsx @@ -1,46 +1,38 @@ -import { Component } from 'react'; +import { lazy, Suspense } from 'react'; import PropTypes from 'prop-types'; -import scribingViewLoader from 'course/assessment/submission/loaders/ScribingViewLoader'; +import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { submissionShape } from '../../propTypes'; -import ScribingCanvas from './ScribingCanvas'; -import ScribingToolbar from './ScribingToolbar'; +const ScribingCanvas = lazy(() => + import(/* webpackChunkName: "ScribingCanvas" */ './ScribingCanvas'), +); -const propTypes = { - answerId: PropTypes.number.isRequired, - submission: submissionShape, -}; +const ScribingToolbar = lazy(() => + import(/* webpackChunkName: "ScribingToolbar" */ './ScribingToolbar'), +); -const styles = { - canvasDiv: { - alignItems: 'center', - marginBottom: 8, - }, -}; +const ScribingViewComponent = (props) => { + const { answerId, submission } = props; + if (!answerId) return null; + + return ( + }> +
+ {submission.canUpdate && ( + + )} -export default class ScribingViewComponent extends Component { - UNSAFE_componentWillMount() { - scribingViewLoader().then(() => { - this.forceUpdate(); - }); - } - - render() { - const { answerId, submission } = this.props; - return answerId ? ( -
- {submission.canUpdate ? ( - - ) : null} - +
- ) : null; - } -} + + ); +}; + +ScribingViewComponent.propTypes = { + answerId: PropTypes.number.isRequired, + submission: submissionShape, +}; -ScribingViewComponent.propTypes = propTypes; +export default ScribingViewComponent; diff --git a/client/app/bundles/course/assessment/submission/loaders/ScribingViewLoader.js b/client/app/bundles/course/assessment/submission/loaders/ScribingViewLoader.js deleted file mode 100644 index 2ef0dc4a754..00000000000 --- a/client/app/bundles/course/assessment/submission/loaders/ScribingViewLoader.js +++ /dev/null @@ -1,7 +0,0 @@ -const { Promise } = global; - -export default () => - Promise.all([ - import(/* webpackChunkName: "react-color" */ 'react-color'), - import(/* webpackChunkName: "fabric" */ 'fabric'), - ]); From ee13884e8eaf5b27194fe904c317ac0a832f812c Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:32:17 +0800 Subject: [PATCH 060/173] feat(ErrorBoundary): rewrite to TS, add raw error stack --- .../lib/components/wrappers/ErrorBoundary.jsx | 36 -------------- .../lib/components/wrappers/ErrorBoundary.tsx | 47 +++++++++++++++++++ 2 files changed, 47 insertions(+), 36 deletions(-) delete mode 100644 client/app/lib/components/wrappers/ErrorBoundary.jsx create mode 100644 client/app/lib/components/wrappers/ErrorBoundary.tsx diff --git a/client/app/lib/components/wrappers/ErrorBoundary.jsx b/client/app/lib/components/wrappers/ErrorBoundary.jsx deleted file mode 100644 index f8341b77462..00000000000 --- a/client/app/lib/components/wrappers/ErrorBoundary.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; - -const propTypes = { - children: PropTypes.element.isRequired, -}; - -class ErrorBoundary extends Component { - constructor(props) { - super(props); - this.state = { hasError: false, error: null, info: null }; - } - - componentDidCatch(error, info) { - this.setState({ hasError: true }); - this.setState({ error, info }); - } - - render() { - if (this.state.hasError) { - // You can render any custom fallback UI - return ( - <> -

Something went wrong.

-

{this.state.error.toString()}

-

{this.state.info.componentStack}

- - ); - } - return this.props.children; - } -} - -ErrorBoundary.propTypes = propTypes; - -export default ErrorBoundary; diff --git a/client/app/lib/components/wrappers/ErrorBoundary.tsx b/client/app/lib/components/wrappers/ErrorBoundary.tsx new file mode 100644 index 00000000000..4bac61d1fc4 --- /dev/null +++ b/client/app/lib/components/wrappers/ErrorBoundary.tsx @@ -0,0 +1,47 @@ +import { Component, ErrorInfo, ReactNode } from 'react'; + +interface ErrorBoundaryProps { + children: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + info: ErrorInfo | null; +} + +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + + this.state = { + hasError: false, + error: null, + info: null, + }; + } + + override componentDidCatch(error: Error, info: ErrorInfo): void { + this.setState({ hasError: true, error, info }); + } + + override render(): ReactNode { + const { hasError, error, info } = this.state; + + if (!hasError) return this.props.children; + + return ( +
+

Something went wrong.

+

{error?.toString()}

+
+          Component Stack
+          {info?.componentStack}
+        
+
{error?.stack}
+
+ ); + } +} + +export default ErrorBoundary; From a4b4f57add615394276a6c247e2962755b6a1a0b Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:34:57 +0800 Subject: [PATCH 061/173] feat(components): add CAPTCHAField --- client/app/declaration.d.ts | 1 + .../components/core/fields/CAPTCHAField.tsx | 35 +++++++++++++++++++ client/package.json | 2 ++ client/webpack.common.js | 4 +-- client/yarn.lock | 23 ++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 client/app/lib/components/core/fields/CAPTCHAField.tsx diff --git a/client/app/declaration.d.ts b/client/app/declaration.d.ts index b816d455761..dca669d3a41 100644 --- a/client/app/declaration.d.ts +++ b/client/app/declaration.d.ts @@ -17,3 +17,4 @@ declare module '*.svg?url' { declare const FIRST_BUILD_YEAR: string; declare const LATEST_BUILD_YEAR: string; + diff --git a/client/app/lib/components/core/fields/CAPTCHAField.tsx b/client/app/lib/components/core/fields/CAPTCHAField.tsx new file mode 100644 index 00000000000..9a92b09166f --- /dev/null +++ b/client/app/lib/components/core/fields/CAPTCHAField.tsx @@ -0,0 +1,35 @@ +import { forwardRef, useImperativeHandle, useRef } from 'react'; +import ReCAPTCHA from 'react-google-recaptcha'; + +interface CAPTCHAFieldProps { + onChange?: (value: string | null) => void; +} + +interface CAPTCHAFieldRef { + reset: () => void; +} + +const SITEKEY = process.env.GOOGLE_RECAPTCHA_SITE_KEY; + +const CAPTCHAField = forwardRef( + (props, ref): JSX.Element => { + const captchaRef = useRef(null); + + useImperativeHandle(ref, () => ({ + reset: (): void => { + captchaRef.current?.reset(); + props.onChange?.(null); + }, + })); + + if (!SITEKEY) throw new Error('GOOGLE_RECAPTCHA_SITE_KEY is not set'); + + return ( + + ); + }, +); + +CAPTCHAField.displayName = 'CAPTCHAField'; + +export default CAPTCHAField; diff --git a/client/package.json b/client/package.json index 7fd3e5c1f0a..4cce310ef1c 100644 --- a/client/package.json +++ b/client/package.json @@ -82,6 +82,7 @@ "react-draggable": "^4.4.5", "react-dropzone": "^14.2.3", "react-emitter-factory": "^1.1.2", + "react-google-recaptcha": "^3.1.0", "react-hook-form": "^7.43.9", "react-hot-keys": "^2.7.2", "react-image-crop": "^10.1.5", @@ -130,6 +131,7 @@ "@types/react": "^18.2.17", "@types/react-beautiful-dnd": "^13.1.4", "@types/react-dom": "^18.2.7", + "@types/react-google-recaptcha": "^2.1.5", "@types/react-resizable": "^3.0.4", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", diff --git a/client/webpack.common.js b/client/webpack.common.js index 804e3e3065b..fd29ab75bfd 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -5,7 +5,7 @@ const { DefinePlugin, } = require('webpack'); const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); -const Dotenv = require('dotenv-webpack'); +const DotenvPlugin = require('dotenv-webpack'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const packageJSON = require('./package.json'); @@ -67,7 +67,7 @@ module.exports = { moduleIds: 'deterministic', }, plugins: [ - new Dotenv(), + new DotenvPlugin(), new IgnorePlugin({ resourceRegExp: /__test__/ }), new WebpackManifestPlugin({ publicPath: '/webpack/', diff --git a/client/yarn.lock b/client/yarn.lock index 4a8844c8de0..d28566430ef 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2460,6 +2460,13 @@ dependencies: "@types/react" "*" +"@types/react-google-recaptcha@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.5.tgz#af157dc2e4bde3355f9b815a64f90e85cfa9df8b" + integrity sha512-iWTjmVttlNgp0teyh7eBXqNOQzVq2RWNiFROWjraOptRnb1OcHJehQnji0sjqIRAk9K0z8stjyhU+OLpPb0N6w== + dependencies: + "@types/react" "*" + "@types/react-is@^18.2.1": version "18.2.1" resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-18.2.1.tgz#61d01c2a6fc089a53520c0b66996d458fdc46863" @@ -8148,6 +8155,14 @@ react-ace@^10.1.0: lodash.isequal "^4.5.0" prop-types "^15.7.2" +react-async-script@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/react-async-script/-/react-async-script-1.2.0.tgz#ab9412a26f0b83f5e2e00de1d2befc9400834b21" + integrity sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q== + dependencies: + hoist-non-react-statics "^3.3.0" + prop-types "^15.5.0" + react-chartjs-2@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz#9941e7397fb963f28bb557addb401e9ff96c6681" @@ -8243,6 +8258,14 @@ react-fast-compare@^3.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== +react-google-recaptcha@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz#44aaab834495d922b9d93d7d7a7fb2326315b4ab" + integrity sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg== + dependencies: + prop-types "^15.5.0" + react-async-script "^1.2.0" + react-hook-form@^7.43.9: version "7.43.9" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.43.9.tgz#84b56ac2f38f8e946c6032ccb760e13a1037c66d" From 7d44c77cb9d06f0e9fac9e1b935b7c1f8b22d89a Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:35:26 +0800 Subject: [PATCH 062/173] chore(types): add @types/react-scroll --- client/package.json | 1 + client/yarn.lock | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/client/package.json b/client/package.json index 4cce310ef1c..750d679f96e 100644 --- a/client/package.json +++ b/client/package.json @@ -133,6 +133,7 @@ "@types/react-dom": "^18.2.7", "@types/react-google-recaptcha": "^2.1.5", "@types/react-resizable": "^3.0.4", + "@types/react-scroll": "^1.8.7", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", "@types/sharedworker": "^0.0.101", diff --git a/client/yarn.lock b/client/yarn.lock index d28566430ef..885ca3cae5d 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2481,6 +2481,13 @@ dependencies: "@types/react" "*" +"@types/react-scroll@^1.8.7": + version "1.8.7" + resolved "https://registry.yarnpkg.com/@types/react-scroll/-/react-scroll-1.8.7.tgz#7241c6ccd47839d79227a23a5184d727245c7324" + integrity sha512-BB8g+hQL7OtBPWg/NcES6p5u6vduZonGl1BxrsGUwcefE53pfI0pFDd1lRFndgEUE6whYdFfhD+j0sZZT/6brQ== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.4.5", "@types/react-transition-group@^4.4.6": version "4.4.6" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.6.tgz#18187bcda5281f8e10dfc48f0943e2fdf4f75e2e" From 1eff4dee9ac72625a11f02eb4b7479260f245e4b Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:37:12 +0800 Subject: [PATCH 063/173] chore(webpack): add HtmlWebpackPlugin --- client/package.json | 1 + client/public/index.html | 13 +++++++++++++ client/webpack.common.js | 5 ++++- client/webpack.prod.js | 3 +-- client/yarn.lock | 24 ++++++++++++++++++++++++ 5 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 client/public/index.html diff --git a/client/package.json b/client/package.json index 750d679f96e..e21632e8d77 100644 --- a/client/package.json +++ b/client/package.json @@ -170,6 +170,7 @@ "eslint-plugin-sonarjs": "^0.19.0", "expose-loader": "^4.1.0", "fork-ts-checker-webpack-plugin": "^8.0.0", + "html-webpack-plugin": "^5.5.3", "jest": "^29.6.2", "jest-canvas-mock": "^2.5.2", "jest-environment-jsdom": "^29.6.2", diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 00000000000..d4f9b4274d9 --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,13 @@ + + + + + + + Coursemology + + + +
+ + diff --git a/client/webpack.common.js b/client/webpack.common.js index fd29ab75bfd..d212c2f555e 100644 --- a/client/webpack.common.js +++ b/client/webpack.common.js @@ -7,6 +7,7 @@ const { const { WebpackManifestPlugin } = require('webpack-manifest-plugin'); const DotenvPlugin = require('dotenv-webpack'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); const packageJSON = require('./package.json'); @@ -20,7 +21,8 @@ module.exports = { ], }, output: { - path: join(__dirname, '..', 'public', 'webpack'), + path: join(__dirname, 'build'), + publicPath: '/', }, resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'], @@ -73,6 +75,7 @@ module.exports = { publicPath: '/webpack/', writeToFileEmit: true, }), + new HtmlWebpackPlugin({ template: './public/index.html' }), // Do not require all locales in moment new ContextReplacementPlugin(/moment\/locale$/, /^\.\/(en-.*|zh-.*)$/), new ForkTsCheckerWebpackPlugin({ diff --git a/client/webpack.prod.js b/client/webpack.prod.js index 90349c2fee6..4946a154698 100644 --- a/client/webpack.prod.js +++ b/client/webpack.prod.js @@ -6,8 +6,7 @@ module.exports = merge(common, { mode: 'production', devtool: 'source-map', output: { - filename: '[name]-[contenthash].js', - publicPath: '/webpack/', + publicPath: '/static/', }, optimization: { usedExports: true, diff --git a/client/yarn.lock b/client/yarn.lock index 885ca3cae5d..f3e9ca34a8d 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -5558,6 +5558,30 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-minifier-terser@^6.0.2: + version "6.1.0" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" + integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== + dependencies: + camel-case "^4.1.2" + clean-css "^5.2.2" + commander "^8.3.0" + he "^1.2.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.10.0" + +html-webpack-plugin@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz#72270f4a78e222b5825b296e5e3e1328ad525a3e" + integrity sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg== + dependencies: + "@types/html-minifier-terser" "^6.0.0" + html-minifier-terser "^6.0.2" + lodash "^4.17.21" + pretty-error "^4.0.0" + tapable "^2.0.0" + htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" From 0852f4fd33abc4ec3404cdb65dea343b7e221744 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:38:21 +0800 Subject: [PATCH 064/173] chore(webpack): set up webpack-dev-server as reverse proxy --- client/package.json | 6 ++++- client/webpack.dev.js | 55 ++++++++++++++++++++++++++----------------- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/client/package.json b/client/package.json index e21632e8d77..71beeb0a114 100644 --- a/client/package.json +++ b/client/package.json @@ -198,5 +198,9 @@ "license": "MIT", "firstBuildYear": 2013, "repository": "git+https://github.com/Coursemology/coursemology2.git", - "main": "app/index.js" + "main": "app/index.js", + "devServer": { + "appHost": "lvh.me", + "serverPort": 5000 + } } diff --git a/client/webpack.dev.js b/client/webpack.dev.js index a3c2d804df0..57f319be633 100644 --- a/client/webpack.dev.js +++ b/client/webpack.dev.js @@ -2,33 +2,46 @@ const { merge } = require('webpack-merge'); const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); const common = require('./webpack.common'); +const packageJSON = require('./package.json'); -const DEV_SERVER_PORT = 8080; -const DEFAULT_LOCALHOST_HOST = 'localhost:5000'; +const SERVER_PORT = packageJSON.devServer.serverPort; +const APP_HOST = packageJSON.devServer.appHost; module.exports = merge(common, { mode: 'development', - output: { - filename: '[name].js', - pathinfo: false, - publicPath: `//localhost:${DEV_SERVER_PORT}/webpack/`, - - /** - * If the host name of the app (e.g., `localhost:5000`) is different from - * that of webpack-dev-server's (e.g., `localhost:8080`), worker scripts - * and assets packed by webpack will be hosted under webpack-dev-server's - * host name. Accessing these resources from the app's host name will - * trigger `SecurityError` in-browser due to the different origins. Forcing - * webpack-dev-server's `publicPath` to the app's host name bypasses this, - * but may not work if the app is hosted on multiple different domains, - * e.g., on both `localhost` and ngrok. - */ - workerPublicPath: `//${DEFAULT_LOCALHOST_HOST}/webpack/`, - }, devtool: 'eval-cheap-module-source-map', devServer: { - port: DEV_SERVER_PORT, - headers: { 'Access-Control-Allow-Origin': '*' }, + allowedHosts: [`.${APP_HOST}`], + historyApiFallback: true, + devMiddleware: { + index: false, + }, + proxy: { + context: () => true, + changeOrigin: true, + onProxyReq: (proxyReq) => { + proxyReq.setHeader('origin', `http://${proxyReq.host}:${SERVER_PORT}`); + }, + router: (request) => ({ + protocol: 'http:', + host: request.headers.host.split(':')[0], + port: SERVER_PORT, + }), + bypass: (request) => { + const target = request.headers.host.split(':')[0]; + + if (request.query.format === 'json') { + console.info( + '\x1b[36m%s\x1b[0m', + `[proxy] ${request.url} -> ${target}${request.url}`, + ); + + return null; + } + + return '/index.html'; + }, + }, }, optimization: { removeAvailableModules: false, From bb85c5f4ab71fd8f37452875871b1467090a8289 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:39:23 +0800 Subject: [PATCH 065/173] feat(store): add session store --- client/app/bundles/common/store.ts | 50 ++++++++++++++ client/app/lib/constants/sharedConstants.ts | 3 + client/app/lib/hooks/session.ts | 73 +++++++++++++++++++++ client/app/store.ts | 11 +++- 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 client/app/bundles/common/store.ts create mode 100644 client/app/lib/hooks/session.ts diff --git a/client/app/bundles/common/store.ts b/client/app/bundles/common/store.ts new file mode 100644 index 00000000000..09a33d89881 --- /dev/null +++ b/client/app/bundles/common/store.ts @@ -0,0 +1,50 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { + DEFAULT_LOCALE, + DEFAULT_TIME_ZONE, +} from 'lib/constants/sharedConstants'; + +/** + * For now, we store a boolean instead of `userId?: number` because there + * isn't a need to store the `userId` at time of writing. Storing `userId` + * would mean that we have to also update it when the user masquerades. + * + * A boolean is kept here to prevent future developers from trying to use + * `userId` when its state update isn't fully thought out. If we ever + * decide to use `userId` more than just an indicator of authentication + * state, we can `SessionState` and `useAuthState`. These abstractions + * were made to make it easier to change the authentication implementations. + */ +export interface SessionState { + authenticated: boolean; + locale: string; + timeZone: string; +} + +const initialState: SessionState = { + authenticated: false, + locale: DEFAULT_LOCALE, + timeZone: DEFAULT_TIME_ZONE, +}; + +export const sessionStore = createSlice({ + name: 'session', + initialState, + reducers: { + setAuthenticated: (state, action: PayloadAction) => { + state.authenticated = action.payload; + }, + setI18nConfig: ( + state, + action: PayloadAction<{ locale?: string; timeZone?: string }>, + ) => { + state.locale = action.payload.locale ?? DEFAULT_LOCALE; + state.timeZone = action.payload.timeZone ?? DEFAULT_TIME_ZONE; + }, + }, +}); + +export const actions = sessionStore.actions; + +export default sessionStore.reducer; diff --git a/client/app/lib/constants/sharedConstants.ts b/client/app/lib/constants/sharedConstants.ts index 83a9da2244d..0546b25a95f 100644 --- a/client/app/lib/constants/sharedConstants.ts +++ b/client/app/lib/constants/sharedConstants.ts @@ -61,3 +61,6 @@ export default { STAFF_ROLES, AVAILABLE_LOCALES, }; + +export const DEFAULT_LOCALE = 'en' as const; +export const DEFAULT_TIME_ZONE = 'Asia/Singapore' as const; diff --git a/client/app/lib/hooks/session.ts b/client/app/lib/hooks/session.ts new file mode 100644 index 00000000000..de3ee5b4ab2 --- /dev/null +++ b/client/app/lib/hooks/session.ts @@ -0,0 +1,73 @@ +import { createSelector, Dispatch } from '@reduxjs/toolkit'; +import { AppState, dispatch as imperativeDispatch, store } from 'store'; + +import { actions, SessionState } from 'bundles/common/store'; + +import { useAppDispatch, useAppSelector } from './store'; + +const selectSessionStore = (state: AppState): SessionState => state.session; + +const selectAuthState = createSelector( + selectSessionStore, + (session) => session.authenticated, +); + +const selectI18nConfig = createSelector(selectSessionStore, (session) => ({ + locale: session.locale, + timeZone: session.timeZone, +})); + +export const useAuthState = (): boolean => useAppSelector(selectAuthState); + +interface UseAuthenticatorHook { + authenticate: () => void; + deauthenticate: () => void; +} + +/** + * NEVER export this method or attempt to use it anywhere else without good reasons. + * Ideally, developers should seek to use `useAuthState` where possible. This is + * an internal implementation to prevent repeated dispatches. + */ +const getAuthState = (): boolean => store.getState()?.session?.authenticated; + +const createAuthenticator = (dispatch: Dispatch): UseAuthenticatorHook => ({ + authenticate: (): void => { + if (getAuthState()) return; + dispatch(actions.setAuthenticated(true)); + }, + deauthenticate: (): void => { + if (!getAuthState()) return; + dispatch(actions.setAuthenticated(false)); + }, +}); + +export const useAuthenticator = (): UseAuthenticatorHook => { + const dispatch = useAppDispatch(); + + return createAuthenticator(dispatch); +}; + +/** + * Ideally, developers should seek to use `useAuthenticator` where possible. This + * authenticator is only used for internal logic outside React, e.g., Axios requests, + * React Router loaders, etc. + */ +export const imperativeAuthenticator = createAuthenticator(imperativeDispatch); + +interface I18nConfig { + locale: string; + timeZone: string; +} + +export const useI18nConfig = (): I18nConfig => useAppSelector(selectI18nConfig); + +export const setI18nConfig = (config: Partial): void => { + const session = store.getState()?.session; + const currentLocale = session?.locale; + const currentTimeZone = session?.timeZone; + if (currentLocale === config.locale && currentTimeZone === config.timeZone) + return; + + imperativeDispatch(actions.setI18nConfig(config)); +}; diff --git a/client/app/store.ts b/client/app/store.ts index b1f1e7e9999..26a9767dc87 100644 --- a/client/app/store.ts +++ b/client/app/store.ts @@ -12,6 +12,7 @@ import deleteConfirmationReducer from 'lib/reducers/deleteConfirmation'; import notificationPopupReducer from 'lib/reducers/notificationPopup'; import globalAnnouncementReducer from './bundles/announcements/store'; +import sessionReducer from './bundles/common/store'; import achievementsReducer from './bundles/course/achievement/store'; import lessonPlanSettingsReducer from './bundles/course/admin/reducers/lessonPlanSettings'; import notificationSettingsReducer from './bundles/course/admin/reducers/notificationSettings'; @@ -85,6 +86,7 @@ const rootReducer = combineReducers({ user: globalUserReducer, announcements: globalAnnouncementReducer, }), + session: sessionReducer, // The following reducers are for UI related rendering. // TODO: remove these (avoid using redux to render UI components) @@ -94,8 +96,13 @@ const rootReducer = combineReducers({ const RESET_STORE_ACTION_TYPE = 'RESET_STORE_BOOM'; -const purgeableRootReducer: Reducer = (state, action) => { - if (action.type === RESET_STORE_ACTION_TYPE) state = undefined; +const purgeableRootReducer: Reducer = (state, action) => { + if (action.type === RESET_STORE_ACTION_TYPE) { + // `session` is generally NOT ephemeral. If `session` is accidentally + // purged without intuition, the router may flicker and break. + state = { session: state?.session } as AppState; + } + return rootReducer(state, action); }; From 777a42250af44d132fd2796c15d9f3ecfef66293 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:58:52 +0800 Subject: [PATCH 066/173] feat(toast): standardise toast styles --- .../announcements/GlobalAnnouncementIndex.tsx | 2 +- .../lib/components/wrappers/ToastProvider.tsx | 27 +++- client/app/lib/hooks/toast/index.ts | 2 + .../app/lib/hooks/{ => toast}/loadingToast.ts | 6 +- client/app/lib/hooks/toast/toast.tsx | 145 ++++++++++++++++++ 5 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 client/app/lib/hooks/toast/index.ts rename client/app/lib/hooks/{ => toast}/loadingToast.ts (81%) create mode 100644 client/app/lib/hooks/toast/toast.tsx diff --git a/client/app/bundles/announcements/GlobalAnnouncementIndex.tsx b/client/app/bundles/announcements/GlobalAnnouncementIndex.tsx index 0006d00221c..7cca36a8b31 100644 --- a/client/app/bundles/announcements/GlobalAnnouncementIndex.tsx +++ b/client/app/bundles/announcements/GlobalAnnouncementIndex.tsx @@ -1,11 +1,11 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import AnnouncementsDisplay from 'bundles/course/announcements/components/misc/AnnouncementsDisplay'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { indexAnnouncements } from './operations'; import { getAllAnnouncementMiniEntities } from './selectors'; diff --git a/client/app/lib/components/wrappers/ToastProvider.tsx b/client/app/lib/components/wrappers/ToastProvider.tsx index ff7d8aea3ce..81a570ad825 100644 --- a/client/app/lib/components/wrappers/ToastProvider.tsx +++ b/client/app/lib/components/wrappers/ToastProvider.tsx @@ -1,19 +1,42 @@ import { ReactNode } from 'react'; -import { ToastContainer } from 'react-toastify'; +import { ToastContainer, TypeOptions } from 'react-toastify'; import { injectStyle } from 'react-toastify/dist/inject-style'; +import { Close } from '@mui/icons-material'; injectStyle(); +export const DEFAULT_TOAST_TIMEOUT_MS = 5000 as const; + interface ToastProviderProps { children: ReactNode; } +const colors: Record = { + default: 'bg-neutral-800', + success: 'bg-green-600', + warning: 'bg-amber-600', + error: 'bg-red-700', + info: 'bg-sky-600', +}; + const ToastProvider = (props: ToastProviderProps): JSX.Element => { return ( <> {props.children} - + 'flex'} + closeButton={} + draggable={false} + hideProgressBar + position="bottom-center" + toastClassName={(toast): string => + `relative shadow-xl rounded-lg mb-4 flex p-5 items-start justify-between ${ + colors[toast?.type ?? 'default'] + }` + } + /> ); }; diff --git a/client/app/lib/hooks/toast/index.ts b/client/app/lib/hooks/toast/index.ts new file mode 100644 index 00000000000..e66dcf94f45 --- /dev/null +++ b/client/app/lib/hooks/toast/index.ts @@ -0,0 +1,2 @@ +export { default as loadingToast } from './loadingToast'; +export { default } from './toast'; diff --git a/client/app/lib/hooks/loadingToast.ts b/client/app/lib/hooks/toast/loadingToast.ts similarity index 81% rename from client/app/lib/hooks/loadingToast.ts rename to client/app/lib/hooks/toast/loadingToast.ts index b7527a296d1..52768b27d7c 100644 --- a/client/app/lib/hooks/loadingToast.ts +++ b/client/app/lib/hooks/toast/loadingToast.ts @@ -1,6 +1,6 @@ -import { toast } from 'react-toastify'; +import { DEFAULT_TOAST_TIMEOUT_MS } from 'lib/components/wrappers/ToastProvider'; -const DEFAULT_TOAST_TIMEOUT_MS = 5000 as const; +import toast from './toast'; type Updater = (message: string) => void; @@ -21,6 +21,7 @@ const loadingToast = (loadingMessage: string): LoadingToast => { isLoading: false, autoClose: DEFAULT_TOAST_TIMEOUT_MS, render: message, + closeButton: true, }), error: (message) => toast.update(id, { @@ -28,6 +29,7 @@ const loadingToast = (loadingMessage: string): LoadingToast => { isLoading: false, autoClose: DEFAULT_TOAST_TIMEOUT_MS, render: message, + closeButton: true, }), }; }; diff --git a/client/app/lib/hooks/toast/toast.tsx b/client/app/lib/hooks/toast/toast.tsx new file mode 100644 index 00000000000..65d9eb728b8 --- /dev/null +++ b/client/app/lib/hooks/toast/toast.tsx @@ -0,0 +1,145 @@ +import { ReactNode } from 'react'; +import { + Id, + toast as toastify, + ToastOptions, + TypeOptions, + UpdateOptions, +} from 'react-toastify'; +import { + ErrorOutline, + InfoOutlined, + SvgIconComponent, + TaskAlt, + WarningAmber, +} from '@mui/icons-material'; +import { Typography } from '@mui/material'; +import { produce } from 'immer'; + +type Toaster = (message: string, options?: ToastOptions) => Id; + +interface PromisedToastMessages { + pending?: ReactNode; + error?: ReactNode; + success?: ReactNode; +} + +type PromisedToaster = ( + data: Promise, + messages: PromisedToastMessages, + options?: ToastOptions, +) => Promise; + +/** + * `UpdateOptions` also allows for `render` to be a function of type + * `(props: ToastContentProps

) => ReactNode`. Here, we define + * a stricter version of `UpdateOptions` to simplify our adapter, and + * since we only usually pass string `message`s. + */ +interface NodeOnlyUpdateOptions extends UpdateOptions { + render?: ReactNode; +} + +const icons: Partial> = { + error: ErrorOutline, + info: InfoOutlined, + success: TaskAlt, + warning: WarningAmber, +}; + +const getIconForToastType = (type: TypeOptions): JSX.Element | undefined => { + const Icon = icons[type]; + if (!Icon) return undefined; + + return ; +}; + +const formattedMessage = (message: ReactNode): JSX.Element => ( + {message} +); + +const isUpdateOptions = ( + options?: NodeOnlyUpdateOptions | ToastOptions, +): options is NodeOnlyUpdateOptions => + options !== undefined && + (options as NodeOnlyUpdateOptions).render !== undefined; + +/** + * Adds our default icons depending on the `type` of the toast. If + * `options` is an `UpdateOptions`, we also format `render`. + */ +const customize = ( + options?: O, +): O | undefined => { + if (!options) return undefined; + + return produce(options, (draft) => { + if (isUpdateOptions(draft)) draft.render = formattedMessage(draft.render); + + draft.icon = getIconForToastType(draft.type ?? 'default'); + }); +}; + +const launch: Toaster = (message, options?) => + toastify(formattedMessage(message), customize(options)); + +const toast: Toaster = (message, options?) => + launch(message, { ...options, type: 'default' }); + +const success: Toaster = (message, options?) => + launch(message, { ...options, type: 'success' }); + +const info: Toaster = (message, options?) => + launch(message, { ...options, type: 'info' }); + +const warn: Toaster = (message, options?) => + launch(message, { ...options, type: 'warning' }); + +const error: Toaster = (message, options?) => + launch(message, { ...options, type: 'error' }); + +/** + * We do not `customize` the options here because we want to retain + * the default loading spinner. + */ +const loading: Toaster = (message, options?) => + toastify.loading(formattedMessage(message), options); + +const update = (id: Id, options?: NodeOnlyUpdateOptions): void => + toastify.update(id, customize(options)); + +const promise: PromisedToaster = (data, messages, options?) => { + return toastify.promise( + data, + { + pending: messages.pending + ? { render: formattedMessage(messages.pending) } + : undefined, + error: messages.error + ? { + render: formattedMessage(messages.error), + type: 'error', + icon: getIconForToastType('error'), + } + : undefined, + success: messages.success + ? { + render: formattedMessage(messages.success), + type: 'success', + icon: getIconForToastType('success'), + } + : undefined, + }, + customize(options), + ); +}; + +export default Object.assign(toast, { + success, + info, + warn, + error, + loading, + update, + promise, +}); From d9862d4d918d9c193af279186603425409525543 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:59:11 +0800 Subject: [PATCH 067/173] feat(toast): react-toastify -> lib/toast --- .../buttons/AchievementManagementButtons.tsx | 2 +- .../components/misc/AchievementReordering.tsx | 42 +++++------ .../AchievementAwardManager.tsx | 2 +- .../pages/AchievementEdit/index.tsx | 2 +- .../pages/AchievementNew/index.tsx | 2 +- .../pages/AchievementsIndex/index.tsx | 2 +- .../pages/AnnouncementsSettings/index.tsx | 2 +- .../admin/pages/AssessmentSettings/index.tsx | 2 +- .../admin/pages/CodaveriSettings/index.tsx | 2 +- .../admin/pages/CommentsSettings/index.tsx | 2 +- .../admin/pages/ComponentSettings/index.tsx | 2 +- .../admin/pages/CourseSettings/index.tsx | 2 +- .../admin/pages/ForumsSettings/index.tsx | 2 +- .../admin/pages/LeaderboardSettings/index.tsx | 2 +- .../pages/LessonPlanSettings/operations.ts | 2 +- .../admin/pages/MaterialsSettings/index.tsx | 2 +- .../pages/NotificationSettings/operations.ts | 2 +- .../admin/pages/SidebarSettings/index.tsx | 2 +- .../admin/pages/VideosSettings/index.tsx | 2 +- .../components/misc/AnnouncementCard.tsx | 2 +- .../pages/AnnouncementEdit/index.tsx | 2 +- .../pages/AnnouncementNew/index.tsx | 2 +- .../pages/AnnouncementsIndex/index.tsx | 2 +- .../ConvertMcqMrqPrompt.tsx | 12 ++-- .../components/FileManager/index.tsx | 2 +- .../AssessmentShow/AssessmentShowHeader.tsx | 15 ++-- .../pages/AssessmentShow/QuestionsManager.tsx | 2 +- .../prompts/DeleteQuestionButtonPrompt.tsx | 12 ++-- .../prompts/DuplicationPrompt.tsx | 69 ++++++++----------- .../EditForumPostResponsePage.tsx | 2 +- .../NewForumPostResponsePage.tsx | 2 +- .../multiple-responses/EditMcqMrqPage.tsx | 2 +- .../multiple-responses/NewMcqMrqPage.tsx | 2 +- .../question/programming/ProgrammingForm.tsx | 2 +- .../text-responses/EditTextResponsePage.tsx | 2 +- .../text-responses/NewTextResponsePage.tsx | 2 +- .../voice-responses/EditVoicePage.tsx | 2 +- .../question/voice-responses/NewVoicePage.tsx | 2 +- .../buttons/SkillManagementButtons.tsx | 2 +- .../skills/components/dialogs/SkillDialog.tsx | 2 +- .../skills/pages/SkillsIndex/index.tsx | 2 +- .../answers/ForumPostResponse/index.jsx | 2 +- .../submission/containers/Annotations.jsx | 2 +- .../submission/containers/Comments.jsx | 3 +- .../submissions/SubmissionsIndex.tsx | 2 +- .../components/misc/SubmissionTabs.tsx | 2 +- .../components/buttons/TodoIgnoreButton.tsx | 7 +- .../forms/CourseInvitationCodeForm.tsx | 2 +- .../components/misc/CourseEnrolOptions.tsx | 2 +- .../course/courses/pages/CourseShow/index.tsx | 2 +- .../courses/pages/CoursesIndex/index.tsx | 2 +- .../course/courses/pages/CoursesNew/index.tsx | 2 +- .../components/cards/CodaveriCommentCard.tsx | 2 +- .../topics/components/cards/CommentCard.tsx | 2 +- .../topics/components/fields/CommentField.tsx | 2 +- .../topics/components/lists/TopicList.tsx | 2 +- .../topics/pages/CommentIndex/index.tsx | 2 +- .../bundles/course/duplication/operations.js | 2 +- .../buttons/PendingEnrolRequestsButtons.tsx | 2 +- .../pages/UserRequests/index.tsx | 2 +- .../components/forms/DisbursementForm.tsx | 2 +- .../components/forms/FilterForm.tsx | 2 +- .../forms/ForumDisbursementForm.tsx | 2 +- .../pages/DisbursementIndex/index.tsx | 2 +- .../pages/ForumDisbursement/index.tsx | 2 +- .../buttons/ForumManagementButtons.tsx | 2 +- .../buttons/ForumTopicManagementButtons.tsx | 2 +- .../ForumTopicPostEditActionButtons.tsx | 2 +- .../ForumTopicPostManagementButtons.tsx | 2 +- .../forum/components/buttons/HideButton.tsx | 2 +- .../forum/components/buttons/LockButton.tsx | 2 +- .../components/buttons/MarkAnswerButton.tsx | 2 +- .../components/buttons/SubscribeButton.tsx | 2 +- .../components/buttons/VotePostButton.tsx | 2 +- .../forum/components/cards/ReplyCard.tsx | 2 +- .../course/forum/pages/ForumEdit/index.tsx | 2 +- .../course/forum/pages/ForumNew/index.tsx | 2 +- .../course/forum/pages/ForumShow/index.tsx | 2 +- .../forum/pages/ForumTopicEdit/index.tsx | 2 +- .../forum/pages/ForumTopicNew/index.tsx | 2 +- .../forum/pages/ForumTopicPostNew/index.tsx | 2 +- .../forum/pages/ForumTopicShow/index.tsx | 2 +- .../course/forum/pages/ForumsIndex/index.tsx | 2 +- .../course/group/pages/GroupIndex/index.jsx | 2 +- .../pages/LeaderboardIndex/index.tsx | 2 +- .../buttons/DownloadFolderButton.tsx | 2 +- .../buttons/WorkbinTableButtons.tsx | 2 +- .../folders/components/misc/MaterialEdit.tsx | 2 +- .../components/misc/MaterialUpload.tsx | 2 +- .../components/misc/MultipleFileInput.tsx | 3 +- .../folders/pages/FolderEdit/index.tsx | 2 +- .../folders/pages/FolderNew/index.tsx | 2 +- .../components/CreateRenameTimelinePrompt.tsx | 2 +- .../components/TimePopup/TimePopup.tsx | 2 +- .../TimelinesOverviewItem.tsx | 2 +- .../TimelinesStack/AssignedTimeline.tsx | 2 +- .../buttons/PendingInvitationsButtons.tsx | 2 +- .../buttons/ResendAllInvitationsButton.tsx | 2 +- .../components/forms/IndividualInviteForm.tsx | 2 +- .../pages/InvitationsIndex/index.tsx | 2 +- .../pages/InviteUsers/index.tsx | 2 +- .../pages/InviteUsersFileUpload/index.tsx | 2 +- .../InviteUsersRegistrationCode/index.tsx | 2 +- .../buttons/PointManagementButtons.tsx | 2 +- .../buttons/UserManagementButtons.tsx | 2 +- .../components/misc/PersonalTimeEditor.tsx | 2 +- .../users/components/misc/UpgradeToStaff.tsx | 2 +- .../tables/ExperiencePointsTable.tsx | 2 +- .../tables/ManageUsersTable/AlgorithmMenu.tsx | 2 +- .../BulkAssignTimelineButton.tsx | 2 +- .../tables/ManageUsersTable/PhantomSwitch.tsx | 2 +- .../tables/ManageUsersTable/RoleMenu.tsx | 2 +- .../tables/ManageUsersTable/TimelineMenu.tsx | 2 +- .../tables/ManageUsersTable/UserNameField.tsx | 2 +- .../course/users/pages/ManageStaff/index.tsx | 2 +- .../users/pages/ManageStudents/index.tsx | 2 +- .../users/pages/PersonalTimes/index.tsx | 2 +- .../users/pages/PersonalTimesShow/index.tsx | 2 +- .../course/users/pages/UsersIndex/index.tsx | 2 +- .../pages/UserVideoSubmissionsIndex/index.tsx | 2 +- .../buttons/VideoManagementButtons.tsx | 2 +- .../components/buttons/WatchVideoButton.tsx | 2 +- .../course/video/pages/VideoEdit/index.tsx | 2 +- .../course/video/pages/VideoNew/index.tsx | 2 +- .../course/video/pages/VideoShow/index.tsx | 2 +- .../course/video/pages/VideosIndex/index.tsx | 2 +- .../video/submission/actions/discussion.js | 3 +- .../pages/VideoSubmissionEdit/index.tsx | 4 +- .../pages/VideoSubmissionShow/index.tsx | 4 +- .../pages/VideoSubmissionsIndex/index.tsx | 2 +- .../components/buttons/CoursesButtons.tsx | 2 +- .../components/buttons/InstancesButtons.tsx | 2 +- .../admin/components/buttons/UsersButtons.tsx | 2 +- .../tables/InstancesTable/InstanceField.tsx | 2 +- .../admin/components/tables/UsersTable.tsx | 2 +- .../admin/admin/pages/AnnouncementsIndex.tsx | 2 +- .../system/admin/admin/pages/CoursesIndex.tsx | 2 +- .../system/admin/admin/pages/InstanceNew.tsx | 2 +- .../admin/admin/pages/InstancesIndex.tsx | 2 +- .../system/admin/admin/pages/UsersIndex.tsx | 2 +- .../buttons/PendingInvitationsButtons.tsx | 2 +- .../buttons/PendingRoleRequestsButtons.tsx | 2 +- .../buttons/ResendAllInvitationsButton.tsx | 2 +- .../components/buttons/UsersButtons.tsx | 2 +- .../components/forms/IndividualInviteForm.tsx | 2 +- .../forms/InstanceUserRoleRequestForm.tsx | 2 +- .../forms/RejectWithMessageForm.tsx | 2 +- .../instance/components/tables/UsersTable.tsx | 2 +- .../pages/InstanceAnnouncementsIndex.tsx | 2 +- .../pages/InstanceComponentsIndex.tsx | 2 +- .../instance/pages/InstanceCoursesIndex.tsx | 2 +- .../pages/InstanceUserRoleRequestsIndex.tsx | 2 +- .../instance/pages/InstanceUsersIndex.tsx | 2 +- .../pages/InstanceUsersInvitations.tsx | 2 +- .../bundles/user/AccountSettings/index.tsx | 2 +- .../lib/components/core/AvatarSelector.tsx | 2 +- .../core/layouts/ContactableErrorAlert.tsx | 2 +- .../extensions/conditions/ConditionRow.tsx | 4 +- .../conditions/ConditionsManager.tsx | 2 +- client/app/lib/components/form/Form.tsx | 2 +- .../app/lib/components/wrappers/Preload.tsx | 2 +- 161 files changed, 230 insertions(+), 246 deletions(-) diff --git a/client/app/bundles/course/achievement/components/buttons/AchievementManagementButtons.tsx b/client/app/bundles/course/achievement/components/buttons/AchievementManagementButtons.tsx index 0a205b64f8f..8ff9e70acef 100644 --- a/client/app/bundles/course/achievement/components/buttons/AchievementManagementButtons.tsx +++ b/client/app/bundles/course/achievement/components/buttons/AchievementManagementButtons.tsx @@ -1,13 +1,13 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { AchievementMiniEntity } from 'types/course/achievements'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import EditButton from 'lib/components/core/buttons/EditButton'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { deleteAchievement } from '../../operations'; diff --git a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx index 4e63f3697b8..4413c1ec4e0 100644 --- a/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx +++ b/client/app/bundles/course/achievement/components/misc/AchievementReordering.tsx @@ -1,13 +1,13 @@ -import { FC } from 'react'; -import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; +import { defineMessages } from 'react-intl'; import { Button } from '@mui/material'; -import axios from 'lib/axios'; +import CourseAPI from 'api/course'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; require('jquery-ui/ui/widgets/sortable'); -interface Props extends WrappedComponentProps { +interface AchievementReorderingProps { handleReordering: (state: boolean) => void; isReordering: boolean; } @@ -45,21 +45,21 @@ function serializedOrdering(): string { return $('tbody').first().sortable('serialize', options); } -function submitReordering(ordering: string): Promise { - const action = `${window.location.pathname}/reorder`; +const AchievementReordering = ( + props: AchievementReorderingProps, +): JSX.Element => { + const { handleReordering, isReordering } = props; - return axios - .post(action, ordering) - .then(() => { - toast.success(translations.updateSuccess.defaultMessage); - }) - .catch(() => { - toast.error(translations.updateFailed.defaultMessage); - }); -} + const { t } = useTranslation(); -const AchievementReordering: FC = (props: Props) => { - const { intl, handleReordering, isReordering } = props; + async function submitReordering(ordering: string): Promise { + try { + await CourseAPI.achievements.reorder(ordering); + toast.success(t(translations.updateSuccess)); + } catch { + toast.error(t(translations.updateFailed)); + } + } return ( ); }; -export default injectIntl(AchievementReordering); +export default AchievementReordering; diff --git a/client/app/bundles/course/achievement/pages/AchievementAward/AchievementAwardManager.tsx b/client/app/bundles/course/achievement/pages/AchievementAward/AchievementAwardManager.tsx index a9dd87cbf7e..0b8b4fd23f7 100644 --- a/client/app/bundles/course/achievement/pages/AchievementAward/AchievementAwardManager.tsx +++ b/client/app/bundles/course/achievement/pages/AchievementAward/AchievementAwardManager.tsx @@ -6,7 +6,6 @@ import { WrappedComponentProps, } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Button, Checkbox, Grid, Tooltip } from '@mui/material'; import { blue, green, red } from '@mui/material/colors'; import equal from 'fast-deep-equal'; @@ -23,6 +22,7 @@ import Note from 'lib/components/core/Note'; import { getAchievementURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { formatShortDateTime } from 'lib/moment'; import { awardAchievement } from '../../operations'; diff --git a/client/app/bundles/course/achievement/pages/AchievementEdit/index.tsx b/client/app/bundles/course/achievement/pages/AchievementEdit/index.tsx index 23d0ca6cd21..4ec02530441 100644 --- a/client/app/bundles/course/achievement/pages/AchievementEdit/index.tsx +++ b/client/app/bundles/course/achievement/pages/AchievementEdit/index.tsx @@ -1,10 +1,10 @@ import { FC, useEffect } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import AchievementForm from '../../components/forms/AchievementForm'; diff --git a/client/app/bundles/course/achievement/pages/AchievementNew/index.tsx b/client/app/bundles/course/achievement/pages/AchievementNew/index.tsx index e966867690c..e1d568ebc49 100644 --- a/client/app/bundles/course/achievement/pages/AchievementNew/index.tsx +++ b/client/app/bundles/course/achievement/pages/AchievementNew/index.tsx @@ -1,12 +1,12 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { getAchievementURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import AchievementForm from '../../components/forms/AchievementForm'; diff --git a/client/app/bundles/course/achievement/pages/AchievementsIndex/index.tsx b/client/app/bundles/course/achievement/pages/AchievementsIndex/index.tsx index b29d02b5dc0..fe23364c08a 100644 --- a/client/app/bundles/course/achievement/pages/AchievementsIndex/index.tsx +++ b/client/app/bundles/course/achievement/pages/AchievementsIndex/index.tsx @@ -1,11 +1,11 @@ import { FC, ReactElement, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import AddButton from 'lib/components/core/buttons/AddButton'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import AchievementReordering from '../../components/misc/AchievementReordering'; diff --git a/client/app/bundles/course/admin/pages/AnnouncementsSettings/index.tsx b/client/app/bundles/course/admin/pages/AnnouncementsSettings/index.tsx index 3d69c117770..9df8f61ea84 100644 --- a/client/app/bundles/course/admin/pages/AnnouncementsSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/AnnouncementsSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { AnnouncementsSettingsData } from 'types/course/admin/announcements'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/AssessmentSettings/index.tsx b/client/app/bundles/course/admin/pages/AssessmentSettings/index.tsx index fa76a6cdba5..dbf0d2ca0ea 100644 --- a/client/app/bundles/course/admin/pages/AssessmentSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/AssessmentSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { AssessmentSettingsData } from 'types/course/admin/assessments'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/CodaveriSettings/index.tsx b/client/app/bundles/course/admin/pages/CodaveriSettings/index.tsx index b0847d615aa..541b6142311 100644 --- a/client/app/bundles/course/admin/pages/CodaveriSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/CodaveriSettings/index.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { CodaveriSettingsData } from 'types/course/admin/codaveri'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/CommentsSettings/index.tsx b/client/app/bundles/course/admin/pages/CommentsSettings/index.tsx index ef1b5a45c01..47682f048e0 100644 --- a/client/app/bundles/course/admin/pages/CommentsSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/CommentsSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { CommentsSettingsData } from 'types/course/admin/comments'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/ComponentSettings/index.tsx b/client/app/bundles/course/admin/pages/ComponentSettings/index.tsx index 6aedebdaf8b..33b6f65215f 100644 --- a/client/app/bundles/course/admin/pages/ComponentSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/ComponentSettings/index.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { CourseComponents } from 'types/course/admin/components'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/CourseSettings/index.tsx b/client/app/bundles/course/admin/pages/CourseSettings/index.tsx index b9a926a91a1..70f6fa8f083 100644 --- a/client/app/bundles/course/admin/pages/CourseSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/CourseSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { CourseInfo, TimeOffset, TimeZones } from 'types/course/admin/course'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/ForumsSettings/index.tsx b/client/app/bundles/course/admin/pages/ForumsSettings/index.tsx index aa156f5ab27..f6b5d41ed5b 100644 --- a/client/app/bundles/course/admin/pages/ForumsSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/ForumsSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { ForumsSettingsData } from 'types/course/admin/forums'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/LeaderboardSettings/index.tsx b/client/app/bundles/course/admin/pages/LeaderboardSettings/index.tsx index 7784ba5ef40..609999d2f7f 100644 --- a/client/app/bundles/course/admin/pages/LeaderboardSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/LeaderboardSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { LeaderboardSettingsData } from 'types/course/admin/leaderboard'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/LessonPlanSettings/operations.ts b/client/app/bundles/course/admin/pages/LessonPlanSettings/operations.ts index 502af5638be..52376628ca8 100644 --- a/client/app/bundles/course/admin/pages/LessonPlanSettings/operations.ts +++ b/client/app/bundles/course/admin/pages/LessonPlanSettings/operations.ts @@ -1,7 +1,7 @@ -import { toast } from 'react-toastify'; import { AxiosError } from 'axios'; import CourseAPI from 'api/course'; +import toast from 'lib/hooks/toast'; import { update } from '../../reducers/lessonPlanSettings'; diff --git a/client/app/bundles/course/admin/pages/MaterialsSettings/index.tsx b/client/app/bundles/course/admin/pages/MaterialsSettings/index.tsx index 7b8c0274ff1..d43bd1f2e7f 100644 --- a/client/app/bundles/course/admin/pages/MaterialsSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/MaterialsSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { MaterialsSettingsData } from 'types/course/admin/materials'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from 'lib/translations/form'; diff --git a/client/app/bundles/course/admin/pages/NotificationSettings/operations.ts b/client/app/bundles/course/admin/pages/NotificationSettings/operations.ts index b03c73eda8c..4eb024eb4fa 100644 --- a/client/app/bundles/course/admin/pages/NotificationSettings/operations.ts +++ b/client/app/bundles/course/admin/pages/NotificationSettings/operations.ts @@ -1,7 +1,7 @@ -import { toast } from 'react-toastify'; import { AxiosError } from 'axios'; import CourseAPI from 'api/course'; +import toast from 'lib/hooks/toast'; import { update } from '../../reducers/notificationSettings'; diff --git a/client/app/bundles/course/admin/pages/SidebarSettings/index.tsx b/client/app/bundles/course/admin/pages/SidebarSettings/index.tsx index a8e6e695e5b..39ead2cc02b 100644 --- a/client/app/bundles/course/admin/pages/SidebarSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/SidebarSettings/index.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { SidebarItems } from 'types/course/admin/sidebar'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { fetchSidebarItems, updateSidebarItems } from './operations'; diff --git a/client/app/bundles/course/admin/pages/VideosSettings/index.tsx b/client/app/bundles/course/admin/pages/VideosSettings/index.tsx index a8005662dee..c02a6b1418b 100644 --- a/client/app/bundles/course/admin/pages/VideosSettings/index.tsx +++ b/client/app/bundles/course/admin/pages/VideosSettings/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { VideosSettingsData, VideosTab } from 'types/course/admin/videos'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/announcements/components/misc/AnnouncementCard.tsx b/client/app/bundles/course/announcements/components/misc/AnnouncementCard.tsx index a767768e579..5ded70c5303 100644 --- a/client/app/bundles/course/announcements/components/misc/AnnouncementCard.tsx +++ b/client/app/bundles/course/announcements/components/misc/AnnouncementCard.tsx @@ -1,6 +1,5 @@ import { FC, memo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { DateRange, PushPin } from '@mui/icons-material'; import { Paper, Typography } from '@mui/material'; import equal from 'fast-deep-equal'; @@ -15,6 +14,7 @@ import EditButton from 'lib/components/core/buttons/EditButton'; import CustomTooltip from 'lib/components/core/CustomTooltip'; import Link from 'lib/components/core/Link'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { formatFullDateTime } from 'lib/moment'; import AnnouncementEdit from '../../pages/AnnouncementEdit'; diff --git a/client/app/bundles/course/announcements/pages/AnnouncementEdit/index.tsx b/client/app/bundles/course/announcements/pages/AnnouncementEdit/index.tsx index a5e17947795..1716049bea7 100644 --- a/client/app/bundles/course/announcements/pages/AnnouncementEdit/index.tsx +++ b/client/app/bundles/course/announcements/pages/AnnouncementEdit/index.tsx @@ -1,11 +1,11 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Operation } from 'store'; import { AnnouncementFormData } from 'types/course/announcements'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import AnnouncementForm from '../../components/forms/AnnouncementForm'; diff --git a/client/app/bundles/course/announcements/pages/AnnouncementNew/index.tsx b/client/app/bundles/course/announcements/pages/AnnouncementNew/index.tsx index 099a429f38c..eaeb048d958 100644 --- a/client/app/bundles/course/announcements/pages/AnnouncementNew/index.tsx +++ b/client/app/bundles/course/announcements/pages/AnnouncementNew/index.tsx @@ -1,11 +1,11 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Operation } from 'store'; import { AnnouncementFormData } from 'types/course/announcements'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import AnnouncementForm, { diff --git a/client/app/bundles/course/announcements/pages/AnnouncementsIndex/index.tsx b/client/app/bundles/course/announcements/pages/AnnouncementsIndex/index.tsx index e3844a88e55..4e97afd98c0 100644 --- a/client/app/bundles/course/announcements/pages/AnnouncementsIndex/index.tsx +++ b/client/app/bundles/course/announcements/pages/AnnouncementsIndex/index.tsx @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import NewAnnouncementButton from '../../components/buttons/NewAnnouncementButton'; diff --git a/client/app/bundles/course/assessment/components/ConvertMcqMrqButton/ConvertMcqMrqPrompt.tsx b/client/app/bundles/course/assessment/components/ConvertMcqMrqButton/ConvertMcqMrqPrompt.tsx index 2ad9eb8b067..2d40cf115e1 100644 --- a/client/app/bundles/course/assessment/components/ConvertMcqMrqButton/ConvertMcqMrqPrompt.tsx +++ b/client/app/bundles/course/assessment/components/ConvertMcqMrqButton/ConvertMcqMrqPrompt.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { East } from '@mui/icons-material'; import { Alert, Chip, Typography } from '@mui/material'; import { McqMrqListData } from 'types/course/assessment/question/multiple-responses'; import Prompt, { PromptText } from 'lib/components/core/dialogs/Prompt'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { convertMcqMrq } from '../../operations'; @@ -50,17 +50,15 @@ const ConvertMcqMrqPrompt = (props: ConvertMcqMrqPromptProps): JSX.Element => { success: unsubmit ? t(translations.questionTypeChangedUnsubmitted) : t(translations.questionTypeChanged), - error: { - render: ({ data }) => { - const error = (data as Error)?.message; - return error || t(translations.errorChangingQuestionType); - }, - }, }) .then((data) => { props.onConvertComplete({ ...question, ...data }); props.onClose(); }) + .catch((error) => { + const message = (error as Error)?.message; + toast.error(message || t(translations.errorChangingQuestionType)); + }) .finally(() => setConverting(false)); }; diff --git a/client/app/bundles/course/assessment/components/FileManager/index.tsx b/client/app/bundles/course/assessment/components/FileManager/index.tsx index 0d6ce8495c9..76b9fccd2b0 100644 --- a/client/app/bundles/course/assessment/components/FileManager/index.tsx +++ b/client/app/bundles/course/assessment/components/FileManager/index.tsx @@ -1,6 +1,5 @@ import { CSSProperties, useState } from 'react'; import { injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Checkbox, CircularProgress } from '@mui/material'; import { AxiosError } from 'axios'; @@ -10,6 +9,7 @@ import DataTable from 'lib/components/core/layouts/DataTable'; import Link from 'lib/components/core/Link'; import { getWorkbinFileURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; +import toast from 'lib/hooks/toast'; import { formatLongDateTime } from 'lib/moment'; import Toolbar from './Toolbar'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx b/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx index c944a2668f8..ecc4171f65e 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx @@ -1,6 +1,5 @@ import { MouseEventHandler, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Assessment, Create, @@ -15,6 +14,7 @@ import { import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import { PromptText } from 'lib/components/core/dialogs/Prompt'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { attemptAssessment, deleteAssessment } from '../../operations'; @@ -70,15 +70,14 @@ const AssessmentShowHeader = ( .promise(deleteAssessment(deleteUrl), { pending: t(translations.deletingAssessment), success: t(translations.assessmentDeleted), - error: { - render: ({ data }) => { - const error = (data as Error)?.message; - return error || t(translations.errorDeletingAssessment); - }, - }, }) .then((data: AssessmentDeleteResult) => navigate(data.redirect)) - .catch(() => setDeleting(false)); + .catch((error) => { + const message = (error as Error)?.message; + toast.error(message || t(translations.errorDeletingAssessment)); + + setDeleting(false); + }); }; return ( diff --git a/client/app/bundles/course/assessment/pages/AssessmentShow/QuestionsManager.tsx b/client/app/bundles/course/assessment/pages/AssessmentShow/QuestionsManager.tsx index bb7f0fef257..dcc9510b4bd 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentShow/QuestionsManager.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentShow/QuestionsManager.tsx @@ -1,11 +1,11 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd'; import { Paper } from '@mui/material'; import { produce } from 'immer'; import { AssessmentData } from 'types/course/assessment/assessments'; import { QuestionData } from 'types/course/assessment/questions'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { reorderQuestions } from '../../operations'; diff --git a/client/app/bundles/course/assessment/pages/AssessmentShow/prompts/DeleteQuestionButtonPrompt.tsx b/client/app/bundles/course/assessment/pages/AssessmentShow/prompts/DeleteQuestionButtonPrompt.tsx index 538535cbf63..efd9e891bbc 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentShow/prompts/DeleteQuestionButtonPrompt.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentShow/prompts/DeleteQuestionButtonPrompt.tsx @@ -1,9 +1,9 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { QuestionData } from 'types/course/assessment/questions'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import { PromptText } from 'lib/components/core/dialogs/Prompt'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { deleteQuestion } from '../../../operations'; @@ -31,14 +31,12 @@ const DeleteQuestionButtonPrompt = ( .promise(deleteQuestion(question.deleteUrl), { pending: t(translations.deletingQuestion), success: t(translations.questionDeleted), - error: { - render: ({ data }) => { - const error = (data as Error)?.message; - return error || t(translations.errorDeletingQuestion); - }, - }, }) .then(props.onDelete) + .catch((error) => { + const message = (error as Error)?.message; + toast.error(message || t(translations.errorDeletingQuestion)); + }) .finally(() => setDeleting(false)); }; diff --git a/client/app/bundles/course/assessment/pages/AssessmentShow/prompts/DuplicationPrompt.tsx b/client/app/bundles/course/assessment/pages/AssessmentShow/prompts/DuplicationPrompt.tsx index a1d7939d875..cab58aa454a 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentShow/prompts/DuplicationPrompt.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentShow/prompts/DuplicationPrompt.tsx @@ -1,6 +1,5 @@ import { Fragment, useDeferredValue, useMemo, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { ArrowForwardRounded, SearchOffRounded } from '@mui/icons-material'; import { List, @@ -17,6 +16,7 @@ import { QuestionData } from 'types/course/assessment/questions'; import Prompt, { PromptText } from 'lib/components/core/dialogs/Prompt'; import TextField from 'lib/components/core/fields/TextField'; import Link from 'lib/components/core/Link'; +import { loadingToast } from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { duplicateQuestion } from '../../../operations'; @@ -127,46 +127,37 @@ const DuplicationPrompt = (props: DuplicationPromptProps): JSX.Element => { const navigate = useNavigate(); const { pathname } = useLocation(); - const duplicate = (duplicationUrl: string): void => { + const duplicate = async (duplicationUrl: string): Promise => { setDuplicating(true); - toast - .promise(duplicateQuestion(duplicationUrl), { - pending: t(translations.duplicatingQuestion), - success: { - render: ({ data: result }) => { - const destinationUrl = result?.destinationUrl; - - if (destinationUrl === pathname) { - navigate(0); - - return ( - - {t(translations.questionDuplicatedRefreshing)} - - ); - } - - return ( - - {t(translations.questionDuplicated)} -   - - {t(translations.goToAssessment)} → - - - ); - }, - }, - error: { - render: ({ data }) => { - const error = (data as Error)?.message; - return error || t(translations.errorDuplicatingQuestion); - }, - }, - }) - .then(props.onClose) - .finally(() => setDuplicating(false)); + const toast = loadingToast(t(translations.duplicatingQuestion)); + + try { + const result = await duplicateQuestion(duplicationUrl); + const destinationUrl = result?.destinationUrl; + + if (destinationUrl === pathname) { + navigate(0); + toast.success(t(translations.questionDuplicatedRefreshing)); + } else { + toast.success( + t(translations.questionDuplicated, { + link: (chunk) => ( + + {chunk} → + + ), + }), + ); + } + + props.onClose(); + } catch (error) { + const message = (error as Error)?.message; + toast.error(message || t(translations.errorDuplicatingQuestion)); + } finally { + setDuplicating(false); + } }; const targetsList = useMemo( diff --git a/client/app/bundles/course/assessment/question/forum-post-responses/EditForumPostResponsePage.tsx b/client/app/bundles/course/assessment/question/forum-post-responses/EditForumPostResponsePage.tsx index aa1c5e5d5e5..49f7150accd 100644 --- a/client/app/bundles/course/assessment/question/forum-post-responses/EditForumPostResponsePage.tsx +++ b/client/app/bundles/course/assessment/question/forum-post-responses/EditForumPostResponsePage.tsx @@ -1,5 +1,4 @@ import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { ForumPostResponseData, ForumPostResponseFormData, @@ -7,6 +6,7 @@ import { import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/assessment/question/forum-post-responses/NewForumPostResponsePage.tsx b/client/app/bundles/course/assessment/question/forum-post-responses/NewForumPostResponsePage.tsx index b2699173e1f..00360095ec2 100644 --- a/client/app/bundles/course/assessment/question/forum-post-responses/NewForumPostResponsePage.tsx +++ b/client/app/bundles/course/assessment/question/forum-post-responses/NewForumPostResponsePage.tsx @@ -1,4 +1,3 @@ -import { toast } from 'react-toastify'; import { ForumPostResponseData, ForumPostResponseFormData, @@ -6,6 +5,7 @@ import { import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from '../../translations'; diff --git a/client/app/bundles/course/assessment/question/multiple-responses/EditMcqMrqPage.tsx b/client/app/bundles/course/assessment/question/multiple-responses/EditMcqMrqPage.tsx index ceb53d5e4f2..eff8d66560b 100644 --- a/client/app/bundles/course/assessment/question/multiple-responses/EditMcqMrqPage.tsx +++ b/client/app/bundles/course/assessment/question/multiple-responses/EditMcqMrqPage.tsx @@ -1,6 +1,5 @@ import { ElementType } from 'react'; import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { McqMrqData, McqMrqFormData, @@ -8,6 +7,7 @@ import { import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/assessment/question/multiple-responses/NewMcqMrqPage.tsx b/client/app/bundles/course/assessment/question/multiple-responses/NewMcqMrqPage.tsx index e978e306de1..df1b9ce8f5d 100644 --- a/client/app/bundles/course/assessment/question/multiple-responses/NewMcqMrqPage.tsx +++ b/client/app/bundles/course/assessment/question/multiple-responses/NewMcqMrqPage.tsx @@ -1,6 +1,5 @@ import { ElementType } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { McqMrqData, McqMrqFormData, @@ -9,6 +8,7 @@ import { import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; import { DataHandle } from 'lib/hooks/router/dynamicNest'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from '../../translations'; diff --git a/client/app/bundles/course/assessment/question/programming/ProgrammingForm.tsx b/client/app/bundles/course/assessment/question/programming/ProgrammingForm.tsx index d1b243bb484..4413fa19f87 100644 --- a/client/app/bundles/course/assessment/question/programming/ProgrammingForm.tsx +++ b/client/app/bundles/course/assessment/question/programming/ProgrammingForm.tsx @@ -7,7 +7,7 @@ import { import Section from 'lib/components/core/layouts/Section'; import Form, { FormEmitter } from 'lib/components/form/Form'; -import loadingToast from 'lib/hooks/loadingToast'; +import { loadingToast } from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from '../../translations'; diff --git a/client/app/bundles/course/assessment/question/text-responses/EditTextResponsePage.tsx b/client/app/bundles/course/assessment/question/text-responses/EditTextResponsePage.tsx index 852b38f17e6..e3221b77ef6 100644 --- a/client/app/bundles/course/assessment/question/text-responses/EditTextResponsePage.tsx +++ b/client/app/bundles/course/assessment/question/text-responses/EditTextResponsePage.tsx @@ -1,5 +1,4 @@ import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { TextResponseData, TextResponseFormData, @@ -7,6 +6,7 @@ import { import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/assessment/question/text-responses/NewTextResponsePage.tsx b/client/app/bundles/course/assessment/question/text-responses/NewTextResponsePage.tsx index ccacd59eb07..a723d0822ea 100644 --- a/client/app/bundles/course/assessment/question/text-responses/NewTextResponsePage.tsx +++ b/client/app/bundles/course/assessment/question/text-responses/NewTextResponsePage.tsx @@ -1,6 +1,5 @@ import { ElementType } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { TextResponseData, TextResponseFormData, @@ -9,6 +8,7 @@ import { import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; import { DataHandle } from 'lib/hooks/router/dynamicNest'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from '../../translations'; diff --git a/client/app/bundles/course/assessment/question/voice-responses/EditVoicePage.tsx b/client/app/bundles/course/assessment/question/voice-responses/EditVoicePage.tsx index 0e523ce9f9c..7f9a18a8e6e 100644 --- a/client/app/bundles/course/assessment/question/voice-responses/EditVoicePage.tsx +++ b/client/app/bundles/course/assessment/question/voice-responses/EditVoicePage.tsx @@ -1,5 +1,4 @@ import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { VoiceResponseData, VoiceResponseFormData, @@ -7,6 +6,7 @@ import { import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/assessment/question/voice-responses/NewVoicePage.tsx b/client/app/bundles/course/assessment/question/voice-responses/NewVoicePage.tsx index a33ff5c2db4..28405e77774 100644 --- a/client/app/bundles/course/assessment/question/voice-responses/NewVoicePage.tsx +++ b/client/app/bundles/course/assessment/question/voice-responses/NewVoicePage.tsx @@ -1,4 +1,3 @@ -import { toast } from 'react-toastify'; import { VoiceResponseData, VoiceResponseFormData, @@ -6,6 +5,7 @@ import { import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from '../../translations'; diff --git a/client/app/bundles/course/assessment/skills/components/buttons/SkillManagementButtons.tsx b/client/app/bundles/course/assessment/skills/components/buttons/SkillManagementButtons.tsx index 14721a2150a..cc721bd1a2f 100644 --- a/client/app/bundles/course/assessment/skills/components/buttons/SkillManagementButtons.tsx +++ b/client/app/bundles/course/assessment/skills/components/buttons/SkillManagementButtons.tsx @@ -1,6 +1,5 @@ import { FC, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { SkillBranchMiniEntity, SkillMiniEntity, @@ -9,6 +8,7 @@ import { import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import EditButton from 'lib/components/core/buttons/EditButton'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { deleteSkill, deleteSkillBranch } from '../../operations'; diff --git a/client/app/bundles/course/assessment/skills/components/dialogs/SkillDialog.tsx b/client/app/bundles/course/assessment/skills/components/dialogs/SkillDialog.tsx index 560580625a1..2ffbdc7e8ec 100644 --- a/client/app/bundles/course/assessment/skills/components/dialogs/SkillDialog.tsx +++ b/client/app/bundles/course/assessment/skills/components/dialogs/SkillDialog.tsx @@ -1,6 +1,5 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { SkillBranchMiniEntity, SkillBranchOptions, @@ -10,6 +9,7 @@ import { import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { diff --git a/client/app/bundles/course/assessment/skills/pages/SkillsIndex/index.tsx b/client/app/bundles/course/assessment/skills/pages/SkillsIndex/index.tsx index a6144fdc6cb..09eef30a3c9 100644 --- a/client/app/bundles/course/assessment/skills/pages/SkillsIndex/index.tsx +++ b/client/app/bundles/course/assessment/skills/pages/SkillsIndex/index.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Grid } from '@mui/material'; import { SkillBranchMiniEntity, @@ -11,6 +10,7 @@ import { import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import SkillDialog from '../../components/dialogs/SkillDialog'; import SkillsTable from '../../components/tables/SkillsTable'; diff --git a/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/index.jsx b/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/index.jsx index d6c88dabc54..a1879074a7e 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/index.jsx +++ b/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/index.jsx @@ -1,11 +1,11 @@ import { useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { toast } from 'react-toastify'; import PropTypes from 'prop-types'; import { questionShape } from 'course/assessment/submission/propTypes'; import Error from 'lib/components/core/ErrorCard'; import FormRichTextField from 'lib/components/form/fields/RichTextField'; +import toast from 'lib/hooks/toast'; import ForumPostSelect from './ForumPostSelect'; diff --git a/client/app/bundles/course/assessment/submission/containers/Annotations.jsx b/client/app/bundles/course/assessment/submission/containers/Annotations.jsx index a8da3828b72..f53bd467f50 100644 --- a/client/app/bundles/course/assessment/submission/containers/Annotations.jsx +++ b/client/app/bundles/course/assessment/submission/containers/Annotations.jsx @@ -1,11 +1,11 @@ import { Component } from 'react'; import { defineMessages, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { toast } from 'react-toastify'; import { Button, Card, CardContent } from '@mui/material'; import PropTypes from 'prop-types'; import withRouter from 'lib/components/navigation/withRouter'; +import toast from 'lib/hooks/toast'; import * as annotationActions from '../actions/annotations'; import CodaveriCommentCard from '../components/comment/CodaveriCommentCard'; diff --git a/client/app/bundles/course/assessment/submission/containers/Comments.jsx b/client/app/bundles/course/assessment/submission/containers/Comments.jsx index 8cbaf7484a9..e1da611d709 100644 --- a/client/app/bundles/course/assessment/submission/containers/Comments.jsx +++ b/client/app/bundles/course/assessment/submission/containers/Comments.jsx @@ -1,10 +1,11 @@ import { Component } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { toast } from 'react-toastify'; import { grey } from '@mui/material/colors'; import PropTypes from 'prop-types'; +import toast from 'lib/hooks/toast'; + import * as commentActions from '../actions/comments'; import CommentCard from '../components/comment/CommentCard'; import CommentField from '../components/comment/CommentField'; diff --git a/client/app/bundles/course/assessment/submissions/SubmissionsIndex.tsx b/client/app/bundles/course/assessment/submissions/SubmissionsIndex.tsx index e11a14b8c28..238af34e37b 100644 --- a/client/app/bundles/course/assessment/submissions/SubmissionsIndex.tsx +++ b/client/app/bundles/course/assessment/submissions/SubmissionsIndex.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { SubmissionAssessmentFilterData, SubmissionGroupFilterData, @@ -11,6 +10,7 @@ import BackendPagination from 'lib/components/core/layouts/BackendPagination'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import SubmissionFilter from './components/misc/SubmissionFilter'; diff --git a/client/app/bundles/course/assessment/submissions/components/misc/SubmissionTabs.tsx b/client/app/bundles/course/assessment/submissions/components/misc/SubmissionTabs.tsx index 4f33f87bdbe..e5ee9295d94 100644 --- a/client/app/bundles/course/assessment/submissions/components/misc/SubmissionTabs.tsx +++ b/client/app/bundles/course/assessment/submissions/components/misc/SubmissionTabs.tsx @@ -1,12 +1,12 @@ import { Dispatch, FC, SetStateAction, useEffect } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Box, Tab, Tabs } from '@mui/material'; import { tabsStyle } from 'theme/mui-style'; import { SubmissionsTabData } from 'types/course/assessment/submissions'; import CustomBadge from 'lib/components/extensions/CustomBadge'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { fetchAllStudentsPendingSubmissions, diff --git a/client/app/bundles/course/courses/components/buttons/TodoIgnoreButton.tsx b/client/app/bundles/course/courses/components/buttons/TodoIgnoreButton.tsx index 4c975c8283b..2111d46e515 100644 --- a/client/app/bundles/course/courses/components/buttons/TodoIgnoreButton.tsx +++ b/client/app/bundles/course/courses/components/buttons/TodoIgnoreButton.tsx @@ -1,10 +1,10 @@ import { FC } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { removeTodo } from '../../operations'; @@ -48,10 +48,7 @@ const TodoIgnoreButton: FC = (props) => { diff --git a/client/app/bundles/course/courses/components/forms/CourseInvitationCodeForm.tsx b/client/app/bundles/course/courses/components/forms/CourseInvitationCodeForm.tsx index 804426220b3..fe6ff96dfc5 100644 --- a/client/app/bundles/course/courses/components/forms/CourseInvitationCodeForm.tsx +++ b/client/app/bundles/course/courses/components/forms/CourseInvitationCodeForm.tsx @@ -1,11 +1,11 @@ import { FC, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Button, TextField } from '@mui/material'; import { getRegistrationURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { sendNewRegistrationCode } from '../../operations'; diff --git a/client/app/bundles/course/courses/components/misc/CourseEnrolOptions.tsx b/client/app/bundles/course/courses/components/misc/CourseEnrolOptions.tsx index 7a48bd93395..988595a52ce 100644 --- a/client/app/bundles/course/courses/components/misc/CourseEnrolOptions.tsx +++ b/client/app/bundles/course/courses/components/misc/CourseEnrolOptions.tsx @@ -1,11 +1,11 @@ import { FC } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import { getEnrolRequestURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { cancelEnrolRequest, submitEnrolRequest } from '../../operations'; import CourseInvitationCodeForm from '../forms/CourseInvitationCodeForm'; diff --git a/client/app/bundles/course/courses/pages/CourseShow/index.tsx b/client/app/bundles/course/courses/pages/CourseShow/index.tsx index 6c88463a537..fd24b79c9e5 100644 --- a/client/app/bundles/course/courses/pages/CourseShow/index.tsx +++ b/client/app/bundles/course/courses/pages/CourseShow/index.tsx @@ -1,7 +1,6 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Typography } from '@mui/material'; import { CourseEntity } from 'types/course/courses'; @@ -9,6 +8,7 @@ import AvatarWithLabel from 'lib/components/core/AvatarWithLabel'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import CourseAnnouncements from '../../components/misc/CourseAnnouncements'; import CourseEnrolOptions from '../../components/misc/CourseEnrolOptions'; diff --git a/client/app/bundles/course/courses/pages/CoursesIndex/index.tsx b/client/app/bundles/course/courses/pages/CoursesIndex/index.tsx index b9a211c488d..de0a60bf495 100644 --- a/client/app/bundles/course/courses/pages/CoursesIndex/index.tsx +++ b/client/app/bundles/course/courses/pages/CoursesIndex/index.tsx @@ -1,12 +1,12 @@ import { FC, ReactElement, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useSearchParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import InstanceUserRoleRequestForm from '../../../../system/admin/instance/instance/components/forms/InstanceUserRoleRequestForm'; diff --git a/client/app/bundles/course/courses/pages/CoursesNew/index.tsx b/client/app/bundles/course/courses/pages/CoursesNew/index.tsx index e8e51ef23f9..be4543d589f 100644 --- a/client/app/bundles/course/courses/pages/CoursesNew/index.tsx +++ b/client/app/bundles/course/courses/pages/CoursesNew/index.tsx @@ -1,10 +1,10 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { NewCourseFormData } from 'types/course/courses'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import NewCourseForm from '../../components/forms/NewCourseForm'; diff --git a/client/app/bundles/course/discussion/topics/components/cards/CodaveriCommentCard.tsx b/client/app/bundles/course/discussion/topics/components/cards/CodaveriCommentCard.tsx index 47e39002277..b18cb32c64f 100644 --- a/client/app/bundles/course/discussion/topics/components/cards/CodaveriCommentCard.tsx +++ b/client/app/bundles/course/discussion/topics/components/cards/CodaveriCommentCard.tsx @@ -1,6 +1,5 @@ import { FC, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { ArrowBack, Check, Clear, Reply } from '@mui/icons-material'; import { LoadingButton } from '@mui/lab'; import { @@ -17,6 +16,7 @@ import { CommentPostMiniEntity } from 'types/course/comments'; import ConfirmationDialog from 'lib/components/core/dialogs/ConfirmationDialog'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { formatLongDateTime } from 'lib/moment'; import { deletePost, updatePostCodaveri } from '../../operations'; diff --git a/client/app/bundles/course/discussion/topics/components/cards/CommentCard.tsx b/client/app/bundles/course/discussion/topics/components/cards/CommentCard.tsx index 0d1af9a3ee6..1c2ac56aba3 100644 --- a/client/app/bundles/course/discussion/topics/components/cards/CommentCard.tsx +++ b/client/app/bundles/course/discussion/topics/components/cards/CommentCard.tsx @@ -5,7 +5,6 @@ import { injectIntl, WrappedComponentProps, } from 'react-intl'; -import { toast } from 'react-toastify'; import Delete from '@mui/icons-material/Delete'; import Edit from '@mui/icons-material/Edit'; import { LoadingButton } from '@mui/lab'; @@ -17,6 +16,7 @@ import ConfirmationDialog from 'lib/components/core/dialogs/ConfirmationDialog'; import CKEditorRichText from 'lib/components/core/fields/CKEditorRichText'; import Link from 'lib/components/core/Link'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { formatLongDateTime } from 'lib/moment'; import { deletePost, updatePost } from '../../operations'; diff --git a/client/app/bundles/course/discussion/topics/components/fields/CommentField.tsx b/client/app/bundles/course/discussion/topics/components/fields/CommentField.tsx index ba32da60ce1..fb0c574faa0 100644 --- a/client/app/bundles/course/discussion/topics/components/fields/CommentField.tsx +++ b/client/app/bundles/course/discussion/topics/components/fields/CommentField.tsx @@ -5,12 +5,12 @@ import { injectIntl, WrappedComponentProps, } from 'react-intl'; -import { toast } from 'react-toastify'; import { LoadingButton } from '@mui/lab'; import { CommentTopicEntity } from 'types/course/comments'; import CKEditorRichText from 'lib/components/core/fields/CKEditorRichText'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { createPost } from '../../operations'; diff --git a/client/app/bundles/course/discussion/topics/components/lists/TopicList.tsx b/client/app/bundles/course/discussion/topics/components/lists/TopicList.tsx index c8a2b8a89fe..ec70070df64 100644 --- a/client/app/bundles/course/discussion/topics/components/lists/TopicList.tsx +++ b/client/app/bundles/course/discussion/topics/components/lists/TopicList.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Grid } from '@mui/material'; import { CommentSettings, CommentTopicEntity } from 'types/course/comments'; @@ -8,6 +7,7 @@ import BackendPagination from 'lib/components/core/layouts/BackendPagination'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { fetchCommentData } from '../../operations'; diff --git a/client/app/bundles/course/discussion/topics/pages/CommentIndex/index.tsx b/client/app/bundles/course/discussion/topics/pages/CommentIndex/index.tsx index 23b58efadf3..8a7ce6d4fdb 100644 --- a/client/app/bundles/course/discussion/topics/pages/CommentIndex/index.tsx +++ b/client/app/bundles/course/discussion/topics/pages/CommentIndex/index.tsx @@ -5,7 +5,6 @@ import { IntlShape, WrappedComponentProps, } from 'react-intl'; -import { toast } from 'react-toastify'; import { Box, Tab, Tabs } from '@mui/material'; import { tabsStyle } from 'theme/mui-style'; import { @@ -19,6 +18,7 @@ import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import CustomBadge from 'lib/components/extensions/CustomBadge'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import TopicList from '../../components/lists/TopicList'; import { fetchTabData } from '../../operations'; diff --git a/client/app/bundles/course/duplication/operations.js b/client/app/bundles/course/duplication/operations.js index 5385157e1ab..b2944925db6 100644 --- a/client/app/bundles/course/duplication/operations.js +++ b/client/app/bundles/course/duplication/operations.js @@ -2,7 +2,7 @@ import CourseAPI from 'api/course'; import actionTypes from 'course/duplication/constants'; import pollJob from 'lib/helpers/jobHelpers'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; -import loadingToast from 'lib/hooks/loadingToast'; +import { loadingToast } from 'lib/hooks/toast'; import { actions } from './store'; import { getItemsPayload } from './utils'; diff --git a/client/app/bundles/course/enrol-requests/components/buttons/PendingEnrolRequestsButtons.tsx b/client/app/bundles/course/enrol-requests/components/buttons/PendingEnrolRequestsButtons.tsx index 08986c0c608..9154e9cd815 100644 --- a/client/app/bundles/course/enrol-requests/components/buttons/PendingEnrolRequestsButtons.tsx +++ b/client/app/bundles/course/enrol-requests/components/buttons/PendingEnrolRequestsButtons.tsx @@ -1,6 +1,5 @@ import { FC, memo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import equal from 'fast-deep-equal'; import { EnrolRequestRowData } from 'types/course/enrolRequests'; @@ -8,6 +7,7 @@ import AcceptButton from 'lib/components/core/buttons/AcceptButton'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import { COURSE_USER_ROLES } from 'lib/constants/sharedConstants'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { approveEnrolRequest, rejectEnrolRequest } from '../../operations'; diff --git a/client/app/bundles/course/enrol-requests/pages/UserRequests/index.tsx b/client/app/bundles/course/enrol-requests/pages/UserRequests/index.tsx index 1c353f6c2cc..8749789204e 100644 --- a/client/app/bundles/course/enrol-requests/pages/UserRequests/index.tsx +++ b/client/app/bundles/course/enrol-requests/pages/UserRequests/index.tsx @@ -1,11 +1,11 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import UserManagementTabs from '../../../users/components/navigation/UserManagementTabs'; import PendingEnrolRequestsButtons from '../../components/buttons/PendingEnrolRequestsButtons'; diff --git a/client/app/bundles/course/experience-points/disbursement/components/forms/DisbursementForm.tsx b/client/app/bundles/course/experience-points/disbursement/components/forms/DisbursementForm.tsx index ab8c0a5a807..05182f83629 100644 --- a/client/app/bundles/course/experience-points/disbursement/components/forms/DisbursementForm.tsx +++ b/client/app/bundles/course/experience-points/disbursement/components/forms/DisbursementForm.tsx @@ -6,7 +6,6 @@ import { injectIntl, WrappedComponentProps, } from 'react-intl'; -import { toast } from 'react-toastify'; import { yupResolver } from '@hookform/resolvers/yup'; import { Autocomplete, Button, Grid, TextField } from '@mui/material'; import { @@ -21,6 +20,7 @@ import Page from 'lib/components/core/layouts/Page'; import FormTextField from 'lib/components/form/fields/TextField'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import formTranslations from 'lib/translations/form'; import { createDisbursement } from '../../operations'; diff --git a/client/app/bundles/course/experience-points/disbursement/components/forms/FilterForm.tsx b/client/app/bundles/course/experience-points/disbursement/components/forms/FilterForm.tsx index 06545cdd572..7644f3d4dfb 100644 --- a/client/app/bundles/course/experience-points/disbursement/components/forms/FilterForm.tsx +++ b/client/app/bundles/course/experience-points/disbursement/components/forms/FilterForm.tsx @@ -6,7 +6,6 @@ import { injectIntl, WrappedComponentProps, } from 'react-intl'; -import { toast } from 'react-toastify'; import { yupResolver } from '@hookform/resolvers/yup'; import { LoadingButton } from '@mui/lab'; import { Grid } from '@mui/material'; @@ -19,6 +18,7 @@ import FormDateTimePickerField from 'lib/components/form/fields/DateTimePickerFi import FormTextField from 'lib/components/form/fields/TextField'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import formTranslations from 'lib/translations/form'; import { fetchFilteredForumDisbursements } from '../../operations'; diff --git a/client/app/bundles/course/experience-points/disbursement/components/forms/ForumDisbursementForm.tsx b/client/app/bundles/course/experience-points/disbursement/components/forms/ForumDisbursementForm.tsx index 09570b2999f..9bde44a1bfe 100644 --- a/client/app/bundles/course/experience-points/disbursement/components/forms/ForumDisbursementForm.tsx +++ b/client/app/bundles/course/experience-points/disbursement/components/forms/ForumDisbursementForm.tsx @@ -6,7 +6,6 @@ import { injectIntl, WrappedComponentProps, } from 'react-intl'; -import { toast } from 'react-toastify'; import { yupResolver } from '@hookform/resolvers/yup'; import { Button, Grid } from '@mui/material'; import equal from 'fast-deep-equal'; @@ -23,6 +22,7 @@ import Page from 'lib/components/core/layouts/Page'; import FormTextField from 'lib/components/form/fields/TextField'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import formTranslations from 'lib/translations/form'; import { createForumDisbursement } from '../../operations'; diff --git a/client/app/bundles/course/experience-points/disbursement/pages/DisbursementIndex/index.tsx b/client/app/bundles/course/experience-points/disbursement/pages/DisbursementIndex/index.tsx index 26e25c0a0fc..c62ccde03ad 100644 --- a/client/app/bundles/course/experience-points/disbursement/pages/DisbursementIndex/index.tsx +++ b/client/app/bundles/course/experience-points/disbursement/pages/DisbursementIndex/index.tsx @@ -5,7 +5,6 @@ import { injectIntl, WrappedComponentProps, } from 'react-intl'; -import { toast } from 'react-toastify'; import { Group } from '@mui/icons-material'; import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; import { Tab, Tabs } from '@mui/material'; @@ -14,6 +13,7 @@ import palette from 'theme/palette'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { fetchDisbursements, fetchForumDisbursements } from '../../operations'; import ForumDisbursement from '../ForumDisbursement'; diff --git a/client/app/bundles/course/experience-points/disbursement/pages/ForumDisbursement/index.tsx b/client/app/bundles/course/experience-points/disbursement/pages/ForumDisbursement/index.tsx index 430ebcd4e94..94654237d30 100644 --- a/client/app/bundles/course/experience-points/disbursement/pages/ForumDisbursement/index.tsx +++ b/client/app/bundles/course/experience-points/disbursement/pages/ForumDisbursement/index.tsx @@ -1,6 +1,5 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import CloseIcon from '@mui/icons-material/Close'; import { Dialog, @@ -17,6 +16,7 @@ import Link from 'lib/components/core/Link'; import { getCourseUserURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { formatLongDateTime } from 'lib/moment'; diff --git a/client/app/bundles/course/forum/components/buttons/ForumManagementButtons.tsx b/client/app/bundles/course/forum/components/buttons/ForumManagementButtons.tsx index c8b0f8811e7..5cb0e7c4bd6 100644 --- a/client/app/bundles/course/forum/components/buttons/ForumManagementButtons.tsx +++ b/client/app/bundles/course/forum/components/buttons/ForumManagementButtons.tsx @@ -1,7 +1,6 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { MoreHoriz } from '@mui/icons-material'; import { ClickAwayListener, IconButton } from '@mui/material'; import { ForumEntity } from 'types/course/forums'; @@ -10,6 +9,7 @@ import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import EditButton from 'lib/components/core/buttons/EditButton'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { deleteForum } from '../../operations'; diff --git a/client/app/bundles/course/forum/components/buttons/ForumTopicManagementButtons.tsx b/client/app/bundles/course/forum/components/buttons/ForumTopicManagementButtons.tsx index 7d451e82bca..1fff548c88d 100644 --- a/client/app/bundles/course/forum/components/buttons/ForumTopicManagementButtons.tsx +++ b/client/app/bundles/course/forum/components/buttons/ForumTopicManagementButtons.tsx @@ -1,7 +1,6 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate, useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { MoreHoriz } from '@mui/icons-material'; import { ClickAwayListener, IconButton } from '@mui/material'; import { ForumTopicEntity } from 'types/course/forums'; @@ -10,6 +9,7 @@ import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import EditButton from 'lib/components/core/buttons/EditButton'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { deleteForumTopic } from '../../operations'; diff --git a/client/app/bundles/course/forum/components/buttons/ForumTopicPostEditActionButtons.tsx b/client/app/bundles/course/forum/components/buttons/ForumTopicPostEditActionButtons.tsx index 1e4fbe931fd..9e6872e3057 100644 --- a/client/app/bundles/course/forum/components/buttons/ForumTopicPostEditActionButtons.tsx +++ b/client/app/bundles/course/forum/components/buttons/ForumTopicPostEditActionButtons.tsx @@ -1,11 +1,11 @@ import { Dispatch, FC, SetStateAction, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import { ForumTopicPostEntity } from 'types/course/forums'; import Prompt, { PromptText } from 'lib/components/core/dialogs/Prompt'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/forum/components/buttons/ForumTopicPostManagementButtons.tsx b/client/app/bundles/course/forum/components/buttons/ForumTopicPostManagementButtons.tsx index c9f8f5796c0..456b694792b 100644 --- a/client/app/bundles/course/forum/components/buttons/ForumTopicPostManagementButtons.tsx +++ b/client/app/bundles/course/forum/components/buttons/ForumTopicPostManagementButtons.tsx @@ -1,13 +1,13 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate, useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { ForumTopicPostEntity } from 'types/course/forums'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import EditButton from 'lib/components/core/buttons/EditButton'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { deleteForumTopicPost } from '../../operations'; diff --git a/client/app/bundles/course/forum/components/buttons/HideButton.tsx b/client/app/bundles/course/forum/components/buttons/HideButton.tsx index f91deadb682..fd6e03f6414 100644 --- a/client/app/bundles/course/forum/components/buttons/HideButton.tsx +++ b/client/app/bundles/course/forum/components/buttons/HideButton.tsx @@ -1,10 +1,10 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import { ForumTopicEntity } from 'types/course/forums'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { updateForumTopicHidden } from '../../operations'; diff --git a/client/app/bundles/course/forum/components/buttons/LockButton.tsx b/client/app/bundles/course/forum/components/buttons/LockButton.tsx index 03a0be2f538..5f7b494b38b 100644 --- a/client/app/bundles/course/forum/components/buttons/LockButton.tsx +++ b/client/app/bundles/course/forum/components/buttons/LockButton.tsx @@ -1,10 +1,10 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import { ForumTopicEntity } from 'types/course/forums'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { updateForumTopicLocked } from '../../operations'; diff --git a/client/app/bundles/course/forum/components/buttons/MarkAnswerButton.tsx b/client/app/bundles/course/forum/components/buttons/MarkAnswerButton.tsx index c08a1dfabaa..459e5aec179 100644 --- a/client/app/bundles/course/forum/components/buttons/MarkAnswerButton.tsx +++ b/client/app/bundles/course/forum/components/buttons/MarkAnswerButton.tsx @@ -1,5 +1,4 @@ import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { CheckCircle, CheckCircleOutline } from '@mui/icons-material'; import { Chip, IconButton, IconButtonProps } from '@mui/material'; import { @@ -9,6 +8,7 @@ import { } from 'types/course/forums'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { toggleForumTopicPostAnswer } from '../../operations'; diff --git a/client/app/bundles/course/forum/components/buttons/SubscribeButton.tsx b/client/app/bundles/course/forum/components/buttons/SubscribeButton.tsx index 623228e2428..20d8fef56fb 100644 --- a/client/app/bundles/course/forum/components/buttons/SubscribeButton.tsx +++ b/client/app/bundles/course/forum/components/buttons/SubscribeButton.tsx @@ -1,12 +1,12 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useSearchParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Button, Switch, Tooltip } from '@mui/material'; import { EmailSubscriptionSetting } from 'types/course/forums'; import Link from 'lib/components/core/Link'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { diff --git a/client/app/bundles/course/forum/components/buttons/VotePostButton.tsx b/client/app/bundles/course/forum/components/buttons/VotePostButton.tsx index 9a3760cd3ad..4cd8dd4676e 100644 --- a/client/app/bundles/course/forum/components/buttons/VotePostButton.tsx +++ b/client/app/bundles/course/forum/components/buttons/VotePostButton.tsx @@ -1,6 +1,5 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { ThumbDownAlt, ThumbDownOffAlt, @@ -11,6 +10,7 @@ import { IconButton, IconButtonProps } from '@mui/material'; import { ForumTopicPostEntity } from 'types/course/forums'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { voteTopicPost } from '../../operations'; diff --git a/client/app/bundles/course/forum/components/cards/ReplyCard.tsx b/client/app/bundles/course/forum/components/cards/ReplyCard.tsx index d4fe9fd93de..9a3d9566fc7 100644 --- a/client/app/bundles/course/forum/components/cards/ReplyCard.tsx +++ b/client/app/bundles/course/forum/components/cards/ReplyCard.tsx @@ -2,13 +2,13 @@ import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useParams } from 'react-router-dom'; import { Element, scroller } from 'react-scroll'; -import { toast } from 'react-toastify'; import { Button, Card, CardActions, CardContent } from '@mui/material'; import { ForumTopicPostFormData } from 'types/course/forums'; import Checkbox from 'lib/components/core/buttons/Checkbox'; import CKEditorRichText from 'lib/components/core/fields/CKEditorRichText'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/forum/pages/ForumEdit/index.tsx b/client/app/bundles/course/forum/pages/ForumEdit/index.tsx index 5f131253f7d..390d63e8f2f 100644 --- a/client/app/bundles/course/forum/pages/ForumEdit/index.tsx +++ b/client/app/bundles/course/forum/pages/ForumEdit/index.tsx @@ -1,11 +1,11 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { ForumEntity, ForumFormData } from 'types/course/forums'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import ForumForm from '../../components/forms/ForumForm'; diff --git a/client/app/bundles/course/forum/pages/ForumNew/index.tsx b/client/app/bundles/course/forum/pages/ForumNew/index.tsx index 6aeef72a962..33ee2623248 100644 --- a/client/app/bundles/course/forum/pages/ForumNew/index.tsx +++ b/client/app/bundles/course/forum/pages/ForumNew/index.tsx @@ -1,9 +1,9 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import ForumForm from '../../components/forms/ForumForm'; diff --git a/client/app/bundles/course/forum/pages/ForumShow/index.tsx b/client/app/bundles/course/forum/pages/ForumShow/index.tsx index 82569fc43f8..5753e1bf01f 100644 --- a/client/app/bundles/course/forum/pages/ForumShow/index.tsx +++ b/client/app/bundles/course/forum/pages/ForumShow/index.tsx @@ -1,12 +1,12 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import AddButton from 'lib/components/core/buttons/AddButton'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import ForumManagementButtons from '../../components/buttons/ForumManagementButtons'; diff --git a/client/app/bundles/course/forum/pages/ForumTopicEdit/index.tsx b/client/app/bundles/course/forum/pages/ForumTopicEdit/index.tsx index 00167924fb7..0ed0f771822 100644 --- a/client/app/bundles/course/forum/pages/ForumTopicEdit/index.tsx +++ b/client/app/bundles/course/forum/pages/ForumTopicEdit/index.tsx @@ -1,11 +1,11 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { ForumTopicEntity, ForumTopicFormData } from 'types/course/forums'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import ForumTopicForm from '../../components/forms/ForumTopicForm'; diff --git a/client/app/bundles/course/forum/pages/ForumTopicNew/index.tsx b/client/app/bundles/course/forum/pages/ForumTopicNew/index.tsx index 42ff9c7b76b..13d90081c29 100644 --- a/client/app/bundles/course/forum/pages/ForumTopicNew/index.tsx +++ b/client/app/bundles/course/forum/pages/ForumTopicNew/index.tsx @@ -1,11 +1,11 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate, useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { ForumTopicFormData, TopicType } from 'types/course/forums'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import ForumTopicForm from '../../components/forms/ForumTopicForm'; diff --git a/client/app/bundles/course/forum/pages/ForumTopicPostNew/index.tsx b/client/app/bundles/course/forum/pages/ForumTopicPostNew/index.tsx index 9af02e88729..6db8b1e8cd9 100644 --- a/client/app/bundles/course/forum/pages/ForumTopicPostNew/index.tsx +++ b/client/app/bundles/course/forum/pages/ForumTopicPostNew/index.tsx @@ -2,12 +2,12 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useParams } from 'react-router-dom'; import { scroller } from 'react-scroll'; -import { toast } from 'react-toastify'; import { Add } from '@mui/icons-material'; import { Fab, Tooltip } from '@mui/material'; import { ForumTopicEntity, ForumTopicPostFormData } from 'types/course/forums'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import ForumTopicPostForm from '../../components/forms/ForumTopicPostForm'; diff --git a/client/app/bundles/course/forum/pages/ForumTopicShow/index.tsx b/client/app/bundles/course/forum/pages/ForumTopicShow/index.tsx index 38cb08157a4..987da3108e9 100644 --- a/client/app/bundles/course/forum/pages/ForumTopicShow/index.tsx +++ b/client/app/bundles/course/forum/pages/ForumTopicShow/index.tsx @@ -1,7 +1,6 @@ import { FC, ReactElement, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Box } from '@mui/material'; import { TopicType } from 'types/course/forums'; @@ -9,6 +8,7 @@ import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import ForumTopicManagementButtons from '../../components/buttons/ForumTopicManagementButtons'; diff --git a/client/app/bundles/course/forum/pages/ForumsIndex/index.tsx b/client/app/bundles/course/forum/pages/ForumsIndex/index.tsx index 1f2072c4640..343640c8528 100644 --- a/client/app/bundles/course/forum/pages/ForumsIndex/index.tsx +++ b/client/app/bundles/course/forum/pages/ForumsIndex/index.tsx @@ -1,11 +1,11 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import AddButton from 'lib/components/core/buttons/AddButton'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import MarkAllAsReadButton from '../../components/buttons/MarkAllAsReadButton'; diff --git a/client/app/bundles/course/group/pages/GroupIndex/index.jsx b/client/app/bundles/course/group/pages/GroupIndex/index.jsx index 05605d4e146..a34c16ed6c6 100644 --- a/client/app/bundles/course/group/pages/GroupIndex/index.jsx +++ b/client/app/bundles/course/group/pages/GroupIndex/index.jsx @@ -1,7 +1,6 @@ import { useEffect, useState } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import { Outlet, useNavigate, useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Tab, Tabs } from '@mui/material'; import PropTypes from 'prop-types'; @@ -10,6 +9,7 @@ import Page from 'lib/components/core/layouts/Page'; import Link from 'lib/components/core/Link'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; +import toast from 'lib/hooks/toast'; import GroupNew from '../GroupNew'; diff --git a/client/app/bundles/course/leaderboard/pages/LeaderboardIndex/index.tsx b/client/app/bundles/course/leaderboard/pages/LeaderboardIndex/index.tsx index 9935f2650f7..68b5fdddcb9 100644 --- a/client/app/bundles/course/leaderboard/pages/LeaderboardIndex/index.tsx +++ b/client/app/bundles/course/leaderboard/pages/LeaderboardIndex/index.tsx @@ -5,7 +5,6 @@ import { injectIntl, WrappedComponentProps, } from 'react-intl'; -import { toast } from 'react-toastify'; import { AutoFixHigh, EmojiEvents, Group, Person } from '@mui/icons-material'; import { Grid, Tab, Tabs } from '@mui/material'; import { useTheme } from '@mui/material/styles'; @@ -15,6 +14,7 @@ import palette from 'theme/palette'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import LeaderboardTable from '../../components/tables/LeaderboardTable'; import fetchLeaderboard from '../../operations'; diff --git a/client/app/bundles/course/material/folders/components/buttons/DownloadFolderButton.tsx b/client/app/bundles/course/material/folders/components/buttons/DownloadFolderButton.tsx index 63dc3e795ad..939a903e302 100644 --- a/client/app/bundles/course/material/folders/components/buttons/DownloadFolderButton.tsx +++ b/client/app/bundles/course/material/folders/components/buttons/DownloadFolderButton.tsx @@ -1,6 +1,5 @@ import { FC, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Download as DownloadIcon, Downloading as DownloadingIcon, @@ -9,6 +8,7 @@ import { IconButton, Tooltip } from '@mui/material'; import CustomTooltip from 'lib/components/core/CustomTooltip'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { downloadFolder } from '../../operations'; diff --git a/client/app/bundles/course/material/folders/components/buttons/WorkbinTableButtons.tsx b/client/app/bundles/course/material/folders/components/buttons/WorkbinTableButtons.tsx index 287f59045b6..b8ff6a88b13 100644 --- a/client/app/bundles/course/material/folders/components/buttons/WorkbinTableButtons.tsx +++ b/client/app/bundles/course/material/folders/components/buttons/WorkbinTableButtons.tsx @@ -1,11 +1,11 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Stack } from '@mui/material'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import EditButton from 'lib/components/core/buttons/EditButton'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { deleteFolder, deleteMaterial } from '../../operations'; diff --git a/client/app/bundles/course/material/folders/components/misc/MaterialEdit.tsx b/client/app/bundles/course/material/folders/components/misc/MaterialEdit.tsx index fa550452d08..46463a0ffc6 100644 --- a/client/app/bundles/course/material/folders/components/misc/MaterialEdit.tsx +++ b/client/app/bundles/course/material/folders/components/misc/MaterialEdit.tsx @@ -1,10 +1,10 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { MaterialFormData } from 'types/course/material/folders'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { updateMaterial } from '../../operations'; diff --git a/client/app/bundles/course/material/folders/components/misc/MaterialUpload.tsx b/client/app/bundles/course/material/folders/components/misc/MaterialUpload.tsx index 9c840f3e210..d78e80664fa 100644 --- a/client/app/bundles/course/material/folders/components/misc/MaterialUpload.tsx +++ b/client/app/bundles/course/material/folders/components/misc/MaterialUpload.tsx @@ -1,10 +1,10 @@ import { FC, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Dialog, DialogContent, DialogTitle } from '@mui/material'; import ConfirmationDialog from 'lib/components/core/dialogs/ConfirmationDialog'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { uploadMaterials } from '../../operations'; import MaterialUploadForm from '../forms/MaterialUploadForm'; diff --git a/client/app/bundles/course/material/folders/components/misc/MultipleFileInput.tsx b/client/app/bundles/course/material/folders/components/misc/MultipleFileInput.tsx index c317f4f1df8..10c0b3bbcd4 100644 --- a/client/app/bundles/course/material/folders/components/misc/MultipleFileInput.tsx +++ b/client/app/bundles/course/material/folders/components/misc/MultipleFileInput.tsx @@ -1,10 +1,11 @@ import { Dispatch, FC, SetStateAction, useState } from 'react'; import Dropzone from 'react-dropzone'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { FileUpload as FileUploadIcon } from '@mui/icons-material'; import { Chip } from '@mui/material'; +import toast from 'lib/hooks/toast'; + interface Props extends WrappedComponentProps { uploadedFiles: File[]; setUploadedFiles: Dispatch>; diff --git a/client/app/bundles/course/material/folders/pages/FolderEdit/index.tsx b/client/app/bundles/course/material/folders/pages/FolderEdit/index.tsx index 23ec1a0a8f2..aec4e6aefa4 100644 --- a/client/app/bundles/course/material/folders/pages/FolderEdit/index.tsx +++ b/client/app/bundles/course/material/folders/pages/FolderEdit/index.tsx @@ -1,10 +1,10 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { FolderFormData } from 'types/course/material/folders'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import FolderForm from '../../components/forms/FolderForm'; diff --git a/client/app/bundles/course/material/folders/pages/FolderNew/index.tsx b/client/app/bundles/course/material/folders/pages/FolderNew/index.tsx index 6c688cf6421..96e6c7e044d 100644 --- a/client/app/bundles/course/material/folders/pages/FolderNew/index.tsx +++ b/client/app/bundles/course/material/folders/pages/FolderNew/index.tsx @@ -1,10 +1,10 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { FolderFormData } from 'types/course/material/folders'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import FolderForm from '../../components/forms/FolderForm'; diff --git a/client/app/bundles/course/reference-timelines/components/CreateRenameTimelinePrompt.tsx b/client/app/bundles/course/reference-timelines/components/CreateRenameTimelinePrompt.tsx index 1de6cef7981..29633a34fe8 100644 --- a/client/app/bundles/course/reference-timelines/components/CreateRenameTimelinePrompt.tsx +++ b/client/app/bundles/course/reference-timelines/components/CreateRenameTimelinePrompt.tsx @@ -1,11 +1,11 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { Alert } from '@mui/material'; import { TimelineData } from 'types/course/referenceTimelines'; import Prompt from 'lib/components/core/dialogs/Prompt'; import TextField from 'lib/components/core/fields/TextField'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx index 899b5653715..ceb9814270b 100644 --- a/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimePopup/TimePopup.tsx @@ -1,4 +1,3 @@ -import { toast } from 'react-toastify'; import { Typography } from '@mui/material'; import moment from 'moment'; import { @@ -7,6 +6,7 @@ import { } from 'types/course/referenceTimelines'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { useSetLastSaved } from '../../contexts'; diff --git a/client/app/bundles/course/reference-timelines/components/TimelinesOverview/TimelinesOverviewItem.tsx b/client/app/bundles/course/reference-timelines/components/TimelinesOverview/TimelinesOverviewItem.tsx index 90d83787efc..974d74e2b46 100644 --- a/client/app/bundles/course/reference-timelines/components/TimelinesOverview/TimelinesOverviewItem.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimelinesOverview/TimelinesOverviewItem.tsx @@ -1,11 +1,11 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { MoreVert } from '@mui/icons-material'; import { Divider, IconButton, Menu, MenuItem } from '@mui/material'; import { TimelineData } from 'types/course/referenceTimelines'; import Checkbox from 'lib/components/core/buttons/Checkbox'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { useLastSaved, useSetLastSaved } from '../../contexts'; diff --git a/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignedTimeline.tsx b/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignedTimeline.tsx index 6557489fa69..0f28d27b393 100644 --- a/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignedTimeline.tsx +++ b/client/app/bundles/course/reference-timelines/components/TimelinesStack/AssignedTimeline.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { ItemWithTimeData, TimeData, @@ -7,6 +6,7 @@ import { } from 'types/course/referenceTimelines'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { useLastSaved, useSetLastSaved } from '../../contexts'; diff --git a/client/app/bundles/course/user-invitations/components/buttons/PendingInvitationsButtons.tsx b/client/app/bundles/course/user-invitations/components/buttons/PendingInvitationsButtons.tsx index df2f63759ea..b5d04b33b0a 100644 --- a/client/app/bundles/course/user-invitations/components/buttons/PendingInvitationsButtons.tsx +++ b/client/app/bundles/course/user-invitations/components/buttons/PendingInvitationsButtons.tsx @@ -1,12 +1,12 @@ import { FC, memo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import equal from 'fast-deep-equal'; import { InvitationRowData } from 'types/course/userInvitations'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import EmailButton from 'lib/components/core/buttons/EmailButton'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { deleteInvitation, resendInvitationEmail } from '../../operations'; diff --git a/client/app/bundles/course/user-invitations/components/buttons/ResendAllInvitationsButton.tsx b/client/app/bundles/course/user-invitations/components/buttons/ResendAllInvitationsButton.tsx index 05a8f0279e5..d9507d3f833 100644 --- a/client/app/bundles/course/user-invitations/components/buttons/ResendAllInvitationsButton.tsx +++ b/client/app/bundles/course/user-invitations/components/buttons/ResendAllInvitationsButton.tsx @@ -1,9 +1,9 @@ import { FC, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { LoadingButton } from '@mui/lab'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { resendAllInvitations } from '../../operations'; diff --git a/client/app/bundles/course/user-invitations/components/forms/IndividualInviteForm.tsx b/client/app/bundles/course/user-invitations/components/forms/IndividualInviteForm.tsx index f0cf37bcdd1..15a0f44e09e 100644 --- a/client/app/bundles/course/user-invitations/components/forms/IndividualInviteForm.tsx +++ b/client/app/bundles/course/user-invitations/components/forms/IndividualInviteForm.tsx @@ -1,7 +1,6 @@ import { FC, useEffect, useState } from 'react'; import { useFieldArray, useForm } from 'react-hook-form'; import { injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { yupResolver } from '@hookform/resolvers/yup'; import { IndividualInvites, @@ -12,6 +11,7 @@ import * as yup from 'yup'; import ErrorText from 'lib/components/core/ErrorText'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import formTranslations from 'lib/translations/form'; import messagesTranslations from 'lib/translations/messages'; diff --git a/client/app/bundles/course/user-invitations/pages/InvitationsIndex/index.tsx b/client/app/bundles/course/user-invitations/pages/InvitationsIndex/index.tsx index cd35ada3db0..4653392bc7d 100644 --- a/client/app/bundles/course/user-invitations/pages/InvitationsIndex/index.tsx +++ b/client/app/bundles/course/user-invitations/pages/InvitationsIndex/index.tsx @@ -1,11 +1,11 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Box, Typography } from '@mui/material'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import UserManagementTabs from '../../../users/components/navigation/UserManagementTabs'; import PendingInvitationsButtons from '../../components/buttons/PendingInvitationsButtons'; diff --git a/client/app/bundles/course/user-invitations/pages/InviteUsers/index.tsx b/client/app/bundles/course/user-invitations/pages/InviteUsers/index.tsx index 697d87607bf..c302246e1ac 100644 --- a/client/app/bundles/course/user-invitations/pages/InviteUsers/index.tsx +++ b/client/app/bundles/course/user-invitations/pages/InviteUsers/index.tsx @@ -1,12 +1,12 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Grid, Typography } from '@mui/material'; import { InvitationResult } from 'types/course/userInvitations'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import UserManagementTabs from '../../../users/components/navigation/UserManagementTabs'; import RegistrationCodeButton from '../../components/buttons/RegistrationCodeButton'; diff --git a/client/app/bundles/course/user-invitations/pages/InviteUsersFileUpload/index.tsx b/client/app/bundles/course/user-invitations/pages/InviteUsersFileUpload/index.tsx index ee3a37b0fe6..42a4c915cc3 100644 --- a/client/app/bundles/course/user-invitations/pages/InviteUsersFileUpload/index.tsx +++ b/client/app/bundles/course/user-invitations/pages/InviteUsersFileUpload/index.tsx @@ -1,6 +1,5 @@ import { FC, ReactNode, useEffect, useState } from 'react'; import { defineMessages, FormattedMessage } from 'react-intl'; -import { toast } from 'react-toastify'; import DownloadIcon from '@mui/icons-material/Download'; import { Typography } from '@mui/material'; import { InvitationResult } from 'types/course/userInvitations'; @@ -8,6 +7,7 @@ import { InvitationResult } from 'types/course/userInvitations'; import CourseAPI from 'api/course'; import Link from 'lib/components/core/Link'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import FileUploadForm from '../../components/forms/InviteUsersFileUploadForm'; diff --git a/client/app/bundles/course/user-invitations/pages/InviteUsersRegistrationCode/index.tsx b/client/app/bundles/course/user-invitations/pages/InviteUsersRegistrationCode/index.tsx index a9a0d97c475..d89d21a63ef 100644 --- a/client/app/bundles/course/user-invitations/pages/InviteUsersRegistrationCode/index.tsx +++ b/client/app/bundles/course/user-invitations/pages/InviteUsersRegistrationCode/index.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { LoadingButton } from '@mui/lab'; import { Alert, @@ -15,6 +14,7 @@ import { } from '@mui/material'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { fetchRegistrationCode, diff --git a/client/app/bundles/course/users/components/buttons/PointManagementButtons.tsx b/client/app/bundles/course/users/components/buttons/PointManagementButtons.tsx index 71438547a4e..ad16ffd45da 100644 --- a/client/app/bundles/course/users/components/buttons/PointManagementButtons.tsx +++ b/client/app/bundles/course/users/components/buttons/PointManagementButtons.tsx @@ -1,6 +1,5 @@ import { FC, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { ExperiencePointsRecordPermissions, ExperiencePointsRowData, @@ -9,6 +8,7 @@ import { import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import SaveButton from 'lib/components/core/buttons/SaveButton'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { deleteExperiencePointsRecord, diff --git a/client/app/bundles/course/users/components/buttons/UserManagementButtons.tsx b/client/app/bundles/course/users/components/buttons/UserManagementButtons.tsx index 6d14d9fee57..5133e22aaac 100644 --- a/client/app/bundles/course/users/components/buttons/UserManagementButtons.tsx +++ b/client/app/bundles/course/users/components/buttons/UserManagementButtons.tsx @@ -1,12 +1,12 @@ import { FC, memo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import equal from 'fast-deep-equal'; import { CourseUserMiniEntity } from 'types/course/courseUsers'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import { COURSE_USER_ROLES } from 'lib/constants/sharedConstants'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { deleteUser } from '../../operations'; diff --git a/client/app/bundles/course/users/components/misc/PersonalTimeEditor.tsx b/client/app/bundles/course/users/components/misc/PersonalTimeEditor.tsx index 554badb7c20..a6dbaae8794 100644 --- a/client/app/bundles/course/users/components/misc/PersonalTimeEditor.tsx +++ b/client/app/bundles/course/users/components/misc/PersonalTimeEditor.tsx @@ -7,7 +7,6 @@ import { WrappedComponentProps, } from 'react-intl'; import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { yupResolver } from '@hookform/resolvers/yup'; import Add from '@mui/icons-material/Add'; import LockOpenOutlined from '@mui/icons-material/LockOpenOutlined'; @@ -23,6 +22,7 @@ import FormCheckboxField from 'lib/components/form/fields/CheckboxField'; import FormDateTimePickerField from 'lib/components/form/fields/DateTimePickerField'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import formTranslations from 'lib/translations/form'; import tableTranslations from 'lib/translations/table'; diff --git a/client/app/bundles/course/users/components/misc/UpgradeToStaff.tsx b/client/app/bundles/course/users/components/misc/UpgradeToStaff.tsx index 02e04453707..c769460892a 100644 --- a/client/app/bundles/course/users/components/misc/UpgradeToStaff.tsx +++ b/client/app/bundles/course/users/components/misc/UpgradeToStaff.tsx @@ -1,6 +1,5 @@ import { FC, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import CheckBoxIcon from '@mui/icons-material/CheckBox'; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import { LoadingButton } from '@mui/lab'; @@ -20,6 +19,7 @@ import { import { STAFF_ROLES } from 'lib/constants/sharedConstants'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { upgradeToStaff } from '../../operations'; import { getStudentOptionMiniEntities } from '../../selectors'; diff --git a/client/app/bundles/course/users/components/tables/ExperiencePointsTable.tsx b/client/app/bundles/course/users/components/tables/ExperiencePointsTable.tsx index 9658246151e..01ef1d59c51 100644 --- a/client/app/bundles/course/users/components/tables/ExperiencePointsTable.tsx +++ b/client/app/bundles/course/users/components/tables/ExperiencePointsTable.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Paper, Table, @@ -13,6 +12,7 @@ import { import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { getCourseUserId } from 'lib/helpers/url-helpers'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import tableTranslations from 'lib/translations/table'; import { fetchExperiencePointsRecord } from '../../operations'; diff --git a/client/app/bundles/course/users/components/tables/ManageUsersTable/AlgorithmMenu.tsx b/client/app/bundles/course/users/components/tables/ManageUsersTable/AlgorithmMenu.tsx index 8f75b5483c8..2309c7efec3 100644 --- a/client/app/bundles/course/users/components/tables/ManageUsersTable/AlgorithmMenu.tsx +++ b/client/app/bundles/course/users/components/tables/ManageUsersTable/AlgorithmMenu.tsx @@ -1,5 +1,4 @@ import { memo } from 'react'; -import { toast } from 'react-toastify'; import { MenuItem, TextField } from '@mui/material'; import equal from 'fast-deep-equal'; import { CourseUserMiniEntity } from 'types/course/courseUsers'; @@ -8,6 +7,7 @@ import { TimelineAlgorithm } from 'types/course/personalTimes'; import { updateUser } from 'bundles/course/users/operations'; import { TIMELINE_ALGORITHMS } from 'lib/constants/sharedConstants'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from './translations'; diff --git a/client/app/bundles/course/users/components/tables/ManageUsersTable/BulkAssignTimelineButton.tsx b/client/app/bundles/course/users/components/tables/ManageUsersTable/BulkAssignTimelineButton.tsx index 0653a43f24f..360c08e1356 100644 --- a/client/app/bundles/course/users/components/tables/ManageUsersTable/BulkAssignTimelineButton.tsx +++ b/client/app/bundles/course/users/components/tables/ManageUsersTable/BulkAssignTimelineButton.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { ExpandMore } from '@mui/icons-material'; import { Button, Menu, MenuItem } from '@mui/material'; import { CourseUserMiniEntity } from 'types/course/courseUsers'; @@ -7,6 +6,7 @@ import { TimelineData } from 'types/course/referenceTimelines'; import { assignToTimeline } from 'bundles/course/users/operations'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from './translations'; diff --git a/client/app/bundles/course/users/components/tables/ManageUsersTable/PhantomSwitch.tsx b/client/app/bundles/course/users/components/tables/ManageUsersTable/PhantomSwitch.tsx index 8e4300995e5..4a385f6e1df 100644 --- a/client/app/bundles/course/users/components/tables/ManageUsersTable/PhantomSwitch.tsx +++ b/client/app/bundles/course/users/components/tables/ManageUsersTable/PhantomSwitch.tsx @@ -1,11 +1,11 @@ import { memo } from 'react'; -import { toast } from 'react-toastify'; import { Switch } from '@mui/material'; import equal from 'fast-deep-equal'; import { CourseUserMiniEntity } from 'types/course/courseUsers'; import { updateUser } from 'bundles/course/users/operations'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from './translations'; diff --git a/client/app/bundles/course/users/components/tables/ManageUsersTable/RoleMenu.tsx b/client/app/bundles/course/users/components/tables/ManageUsersTable/RoleMenu.tsx index 2934ff7b37a..9809579837d 100644 --- a/client/app/bundles/course/users/components/tables/ManageUsersTable/RoleMenu.tsx +++ b/client/app/bundles/course/users/components/tables/ManageUsersTable/RoleMenu.tsx @@ -1,5 +1,4 @@ import { memo } from 'react'; -import { toast } from 'react-toastify'; import { MenuItem, TextField } from '@mui/material'; import equal from 'fast-deep-equal'; import { @@ -10,6 +9,7 @@ import { import { updateUser } from 'bundles/course/users/operations'; import { COURSE_USER_ROLES } from 'lib/constants/sharedConstants'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from './translations'; diff --git a/client/app/bundles/course/users/components/tables/ManageUsersTable/TimelineMenu.tsx b/client/app/bundles/course/users/components/tables/ManageUsersTable/TimelineMenu.tsx index f061cf1d33f..9ee88423f71 100644 --- a/client/app/bundles/course/users/components/tables/ManageUsersTable/TimelineMenu.tsx +++ b/client/app/bundles/course/users/components/tables/ManageUsersTable/TimelineMenu.tsx @@ -1,11 +1,11 @@ import { memo } from 'react'; -import { toast } from 'react-toastify'; import { TextField } from '@mui/material'; import equal from 'fast-deep-equal'; import { CourseUserMiniEntity } from 'types/course/courseUsers'; import { updateUser } from 'bundles/course/users/operations'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from './translations'; diff --git a/client/app/bundles/course/users/components/tables/ManageUsersTable/UserNameField.tsx b/client/app/bundles/course/users/components/tables/ManageUsersTable/UserNameField.tsx index 3ed17199947..b6c773ae58b 100644 --- a/client/app/bundles/course/users/components/tables/ManageUsersTable/UserNameField.tsx +++ b/client/app/bundles/course/users/components/tables/ManageUsersTable/UserNameField.tsx @@ -1,11 +1,11 @@ import { memo } from 'react'; -import { toast } from 'react-toastify'; import equal from 'fast-deep-equal'; import { CourseUserMiniEntity } from 'types/course/courseUsers'; import { updateUser } from 'bundles/course/users/operations'; import InlineEditTextField from 'lib/components/form/fields/DataTableInlineEditable/TextField'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from './translations'; diff --git a/client/app/bundles/course/users/pages/ManageStaff/index.tsx b/client/app/bundles/course/users/pages/ManageStaff/index.tsx index d539c19ce25..1f17105999f 100644 --- a/client/app/bundles/course/users/pages/ManageStaff/index.tsx +++ b/client/app/bundles/course/users/pages/ManageStaff/index.tsx @@ -1,11 +1,11 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import manageUsersTranslations from 'lib/translations/course/users/index'; import UserManagementButtons from '../../components/buttons/UserManagementButtons'; diff --git a/client/app/bundles/course/users/pages/ManageStudents/index.tsx b/client/app/bundles/course/users/pages/ManageStudents/index.tsx index 2ad6f6a4041..9f71f90cbc1 100644 --- a/client/app/bundles/course/users/pages/ManageStudents/index.tsx +++ b/client/app/bundles/course/users/pages/ManageStudents/index.tsx @@ -1,11 +1,11 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import manageUsersTranslations from 'lib/translations/course/users/index'; import UserManagementButtons from '../../components/buttons/UserManagementButtons'; diff --git a/client/app/bundles/course/users/pages/PersonalTimes/index.tsx b/client/app/bundles/course/users/pages/PersonalTimes/index.tsx index 9e7df44ccb1..7e61055eb95 100644 --- a/client/app/bundles/course/users/pages/PersonalTimes/index.tsx +++ b/client/app/bundles/course/users/pages/PersonalTimes/index.tsx @@ -1,11 +1,11 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Typography } from '@mui/material'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import SelectCourseUser from '../../components/misc/SelectCourseUser'; import UserManagementTabs from '../../components/navigation/UserManagementTabs'; diff --git a/client/app/bundles/course/users/pages/PersonalTimesShow/index.tsx b/client/app/bundles/course/users/pages/PersonalTimesShow/index.tsx index 76ef1073292..0cc39fca190 100644 --- a/client/app/bundles/course/users/pages/PersonalTimesShow/index.tsx +++ b/client/app/bundles/course/users/pages/PersonalTimesShow/index.tsx @@ -1,7 +1,6 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { LoadingButton } from '@mui/lab'; import { Grid, MenuItem, Stack, TextField, Typography } from '@mui/material'; import { CourseUserEntity } from 'types/course/courseUsers'; @@ -11,6 +10,7 @@ import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { TIMELINE_ALGORITHMS } from 'lib/constants/sharedConstants'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import SelectCourseUser from '../../components/misc/SelectCourseUser'; import UserManagementTabs from '../../components/navigation/UserManagementTabs'; diff --git a/client/app/bundles/course/users/pages/UsersIndex/index.tsx b/client/app/bundles/course/users/pages/UsersIndex/index.tsx index fca28d3dad9..ee8141a1d21 100644 --- a/client/app/bundles/course/users/pages/UsersIndex/index.tsx +++ b/client/app/bundles/course/users/pages/UsersIndex/index.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Avatar, Grid, Typography } from '@mui/material'; import Page from 'lib/components/core/layouts/Page'; @@ -9,6 +8,7 @@ import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { getCourseUserURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { fetchUsers } from '../../operations'; import { getAllStudentMiniEntities } from '../../selectors'; diff --git a/client/app/bundles/course/video-submissions/pages/UserVideoSubmissionsIndex/index.tsx b/client/app/bundles/course/video-submissions/pages/UserVideoSubmissionsIndex/index.tsx index b7b3d960735..8256b8db128 100644 --- a/client/app/bundles/course/video-submissions/pages/UserVideoSubmissionsIndex/index.tsx +++ b/client/app/bundles/course/video-submissions/pages/UserVideoSubmissionsIndex/index.tsx @@ -1,10 +1,10 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { VideoSubmissionListData } from 'types/course/videoSubmissions'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; +import toast from 'lib/hooks/toast'; import UserVideoSubmissionTable from '../../components/tables/UserVideoSubmissionTable'; import { fetchVideoSubmissions } from '../../operations'; diff --git a/client/app/bundles/course/video/components/buttons/VideoManagementButtons.tsx b/client/app/bundles/course/video/components/buttons/VideoManagementButtons.tsx index 031a9fa31cb..956edd27895 100644 --- a/client/app/bundles/course/video/components/buttons/VideoManagementButtons.tsx +++ b/client/app/bundles/course/video/components/buttons/VideoManagementButtons.tsx @@ -1,7 +1,6 @@ import { FC, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { VideoListData } from 'types/course/videos'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; @@ -9,6 +8,7 @@ import EditButton from 'lib/components/core/buttons/EditButton'; import { getVideosURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { deleteVideo } from '../../operations'; diff --git a/client/app/bundles/course/video/components/buttons/WatchVideoButton.tsx b/client/app/bundles/course/video/components/buttons/WatchVideoButton.tsx index 8ffc39a250f..8ebfef8b32b 100644 --- a/client/app/bundles/course/video/components/buttons/WatchVideoButton.tsx +++ b/client/app/bundles/course/video/components/buttons/WatchVideoButton.tsx @@ -1,13 +1,13 @@ import { FC } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import { VideoListData } from 'types/course/videos'; import CourseAPI from 'api/course'; import { getEditVideoSubmissionURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; +import toast from 'lib/hooks/toast'; interface Props extends WrappedComponentProps { video: VideoListData; diff --git a/client/app/bundles/course/video/pages/VideoEdit/index.tsx b/client/app/bundles/course/video/pages/VideoEdit/index.tsx index a41f944bdb7..8aee4b75092 100644 --- a/client/app/bundles/course/video/pages/VideoEdit/index.tsx +++ b/client/app/bundles/course/video/pages/VideoEdit/index.tsx @@ -1,10 +1,10 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { VideoFormData, VideoListData } from 'types/course/videos'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import VideoForm from '../../components/forms/VideoForm'; diff --git a/client/app/bundles/course/video/pages/VideoNew/index.tsx b/client/app/bundles/course/video/pages/VideoNew/index.tsx index a31be84ca61..a2bcc746129 100644 --- a/client/app/bundles/course/video/pages/VideoNew/index.tsx +++ b/client/app/bundles/course/video/pages/VideoNew/index.tsx @@ -1,10 +1,10 @@ import { FC, memo } from 'react'; import { defineMessages } from 'react-intl'; import { useSearchParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import VideoForm from '../../components/forms/VideoForm'; diff --git a/client/app/bundles/course/video/pages/VideoShow/index.tsx b/client/app/bundles/course/video/pages/VideoShow/index.tsx index de73ab1136c..150b808e616 100644 --- a/client/app/bundles/course/video/pages/VideoShow/index.tsx +++ b/client/app/bundles/course/video/pages/VideoShow/index.tsx @@ -1,7 +1,6 @@ import { FC, ReactElement, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Card, CardContent, CardHeader } from '@mui/material'; import DescriptionCard from 'lib/components/core/DescriptionCard'; @@ -10,6 +9,7 @@ import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { getVideosURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import VideoManagementButtons from '../../components/buttons/VideoManagementButtons'; import WatchVideoButton from '../../components/buttons/WatchVideoButton'; diff --git a/client/app/bundles/course/video/pages/VideosIndex/index.tsx b/client/app/bundles/course/video/pages/VideosIndex/index.tsx index 96e67249762..ab398e734c2 100644 --- a/client/app/bundles/course/video/pages/VideosIndex/index.tsx +++ b/client/app/bundles/course/video/pages/VideosIndex/index.tsx @@ -1,12 +1,12 @@ import { FC, ReactElement, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; import { useSearchParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import VideoTabs from '../../components/misc/VideoTabs'; diff --git a/client/app/bundles/course/video/submission/actions/discussion.js b/client/app/bundles/course/video/submission/actions/discussion.js index 48c5eef9439..77419197bd6 100644 --- a/client/app/bundles/course/video/submission/actions/discussion.js +++ b/client/app/bundles/course/video/submission/actions/discussion.js @@ -1,10 +1,9 @@ -import { toast } from 'react-toastify'; - import CourseAPI from 'api/course'; import { discussionActionTypes, postRequestingStatuses, } from 'lib/constants/videoConstants'; +import toast from 'lib/hooks/toast'; /** * Creates an action to update the new post being created with the main comment box. diff --git a/client/app/bundles/course/video/submission/pages/VideoSubmissionEdit/index.tsx b/client/app/bundles/course/video/submission/pages/VideoSubmissionEdit/index.tsx index 3f3936a1278..b88100b72dc 100644 --- a/client/app/bundles/course/video/submission/pages/VideoSubmissionEdit/index.tsx +++ b/client/app/bundles/course/video/submission/pages/VideoSubmissionEdit/index.tsx @@ -1,13 +1,13 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { VideoEditSubmissionData } from 'types/course/video/submissions'; import CourseAPI from 'api/course'; import DescriptionCard from 'lib/components/core/DescriptionCard'; import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; +import toast from 'lib/hooks/toast'; import SubmissionEditWithStore from './SubmissionEditWithStore'; @@ -38,7 +38,7 @@ const VideoSubmissionEdit: FC = (props) => { useEffect(() => { if (submissionId) { CourseAPI.video.submissions - .edit(submissionId) + .edit(+submissionId) .then((response) => { setEditVideoSubmission(response.data); setIsLoading(false); diff --git a/client/app/bundles/course/video/submission/pages/VideoSubmissionShow/index.tsx b/client/app/bundles/course/video/submission/pages/VideoSubmissionShow/index.tsx index f59bd11bfea..acb12271c8c 100644 --- a/client/app/bundles/course/video/submission/pages/VideoSubmissionShow/index.tsx +++ b/client/app/bundles/course/video/submission/pages/VideoSubmissionShow/index.tsx @@ -1,7 +1,6 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; import { useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; import { Card, CardContent, @@ -20,6 +19,7 @@ import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; import { getVideoSubmissionsURL } from 'lib/helpers/url-builders'; import { getCourseId, getVideoId } from 'lib/helpers/url-helpers'; +import toast from 'lib/hooks/toast'; import { formatLongDateTime } from 'lib/moment'; import StatisticsWithStore from './StatisticsWithStore'; @@ -84,7 +84,7 @@ const VideoSubmissionShow: FC = (props) => { useEffect(() => { if (submissionId) { CourseAPI.video.submissions - .fetch(submissionId) + .fetch(+submissionId) .then((response) => { setVideoSubmission(response.data); setIsLoading(false); diff --git a/client/app/bundles/course/video/submission/pages/VideoSubmissionsIndex/index.tsx b/client/app/bundles/course/video/submission/pages/VideoSubmissionsIndex/index.tsx index 86325a9b1a8..8b209a224c8 100644 --- a/client/app/bundles/course/video/submission/pages/VideoSubmissionsIndex/index.tsx +++ b/client/app/bundles/course/video/submission/pages/VideoSubmissionsIndex/index.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { VideoSubmission } from 'types/course/video/submissions'; import CourseAPI from 'api/course'; @@ -9,6 +8,7 @@ import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; import { getVideosURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; +import toast from 'lib/hooks/toast'; import VideoSubmissionsTable from '../../components/tables/VideoSubmissionsTable'; diff --git a/client/app/bundles/system/admin/admin/components/buttons/CoursesButtons.tsx b/client/app/bundles/system/admin/admin/components/buttons/CoursesButtons.tsx index a7388fb5c8f..9b28c0a98c1 100644 --- a/client/app/bundles/system/admin/admin/components/buttons/CoursesButtons.tsx +++ b/client/app/bundles/system/admin/admin/components/buttons/CoursesButtons.tsx @@ -1,6 +1,5 @@ import { FC, memo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Delete } from '@mui/icons-material'; import { IconButton } from '@mui/material'; import equal from 'fast-deep-equal'; @@ -9,6 +8,7 @@ import { CourseMiniEntity } from 'types/system/courses'; import DeleteCoursePrompt from 'bundles/course/admin/pages/CourseSettings/DeleteCoursePrompt'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; interface Props extends WrappedComponentProps { course: CourseMiniEntity; diff --git a/client/app/bundles/system/admin/admin/components/buttons/InstancesButtons.tsx b/client/app/bundles/system/admin/admin/components/buttons/InstancesButtons.tsx index 79847d542d6..655348d0323 100644 --- a/client/app/bundles/system/admin/admin/components/buttons/InstancesButtons.tsx +++ b/client/app/bundles/system/admin/admin/components/buttons/InstancesButtons.tsx @@ -1,11 +1,11 @@ import { FC, memo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import equal from 'fast-deep-equal'; import { InstanceMiniEntity } from 'types/system/instances'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { deleteInstance } from '../../operations'; diff --git a/client/app/bundles/system/admin/admin/components/buttons/UsersButtons.tsx b/client/app/bundles/system/admin/admin/components/buttons/UsersButtons.tsx index 9514971e03a..23ffab47329 100644 --- a/client/app/bundles/system/admin/admin/components/buttons/UsersButtons.tsx +++ b/client/app/bundles/system/admin/admin/components/buttons/UsersButtons.tsx @@ -1,6 +1,5 @@ import { FC, memo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import equal from 'fast-deep-equal'; import { UserMiniEntity } from 'types/users'; @@ -8,6 +7,7 @@ import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import MasqueradeButton from 'lib/components/core/buttons/MasqueradeButton'; import { USER_ROLES } from 'lib/constants/sharedConstants'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { deleteUser } from '../../operations'; diff --git a/client/app/bundles/system/admin/admin/components/tables/InstancesTable/InstanceField.tsx b/client/app/bundles/system/admin/admin/components/tables/InstancesTable/InstanceField.tsx index 63aafa35ccc..77114fc3bb4 100644 --- a/client/app/bundles/system/admin/admin/components/tables/InstancesTable/InstanceField.tsx +++ b/client/app/bundles/system/admin/admin/components/tables/InstancesTable/InstanceField.tsx @@ -1,9 +1,9 @@ import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { InstanceMiniEntity } from 'types/system/instances'; import InlineEditTextField from 'lib/components/form/fields/DataTableInlineEditable/TextField'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { updateInstance } from '../../../operations'; diff --git a/client/app/bundles/system/admin/admin/components/tables/UsersTable.tsx b/client/app/bundles/system/admin/admin/components/tables/UsersTable.tsx index 568b13c3a96..620a6e4df9d 100644 --- a/client/app/bundles/system/admin/admin/components/tables/UsersTable.tsx +++ b/client/app/bundles/system/admin/admin/components/tables/UsersTable.tsx @@ -1,6 +1,5 @@ import { FC, ReactElement, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { CircularProgress, MenuItem, @@ -25,6 +24,7 @@ import { } from 'lib/constants/sharedConstants'; import rebuildObjectFromRow from 'lib/helpers/mui-datatables-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import tableTranslations from 'lib/translations/table'; import { indexUsers, updateUser } from '../../operations'; diff --git a/client/app/bundles/system/admin/admin/pages/AnnouncementsIndex.tsx b/client/app/bundles/system/admin/admin/pages/AnnouncementsIndex.tsx index ed691181d85..1785d03813a 100644 --- a/client/app/bundles/system/admin/admin/pages/AnnouncementsIndex.tsx +++ b/client/app/bundles/system/admin/admin/pages/AnnouncementsIndex.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import AnnouncementsDisplay from 'bundles/course/announcements/components/misc/AnnouncementsDisplay'; @@ -8,6 +7,7 @@ import AnnouncementNew from 'bundles/course/announcements/pages/AnnouncementNew' import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { createAnnouncement, diff --git a/client/app/bundles/system/admin/admin/pages/CoursesIndex.tsx b/client/app/bundles/system/admin/admin/pages/CoursesIndex.tsx index 5ce0586e975..7fa97c19ef0 100644 --- a/client/app/bundles/system/admin/admin/pages/CoursesIndex.tsx +++ b/client/app/bundles/system/admin/admin/pages/CoursesIndex.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Typography } from '@mui/material'; import SummaryCard from 'lib/components/core/layouts/SummaryCard'; @@ -8,6 +7,7 @@ import Link from 'lib/components/core/Link'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { DEFAULT_TABLE_ROWS_PER_PAGE } from 'lib/constants/sharedConstants'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import CoursesButtons from '../components/buttons/CoursesButtons'; import CoursesTable from '../components/tables/CoursesTable'; diff --git a/client/app/bundles/system/admin/admin/pages/InstanceNew.tsx b/client/app/bundles/system/admin/admin/pages/InstanceNew.tsx index 376b42f2749..c8c606794ee 100644 --- a/client/app/bundles/system/admin/admin/pages/InstanceNew.tsx +++ b/client/app/bundles/system/admin/admin/pages/InstanceNew.tsx @@ -1,10 +1,10 @@ import { FC } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { InstanceFormData } from 'types/system/instances'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import InstanceForm from '../components/forms/InstanceForm'; diff --git a/client/app/bundles/system/admin/admin/pages/InstancesIndex.tsx b/client/app/bundles/system/admin/admin/pages/InstancesIndex.tsx index 21ebef3fb27..657d0e17b9a 100644 --- a/client/app/bundles/system/admin/admin/pages/InstancesIndex.tsx +++ b/client/app/bundles/system/admin/admin/pages/InstancesIndex.tsx @@ -1,10 +1,10 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import InstancesButtons from '../components/buttons/InstancesButtons'; diff --git a/client/app/bundles/system/admin/admin/pages/UsersIndex.tsx b/client/app/bundles/system/admin/admin/pages/UsersIndex.tsx index b7edc57fbcc..a57c1ee9841 100644 --- a/client/app/bundles/system/admin/admin/pages/UsersIndex.tsx +++ b/client/app/bundles/system/admin/admin/pages/UsersIndex.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useMemo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Typography } from '@mui/material'; import SummaryCard from 'lib/components/core/layouts/SummaryCard'; @@ -8,6 +7,7 @@ import Link from 'lib/components/core/Link'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { DEFAULT_TABLE_ROWS_PER_PAGE } from 'lib/constants/sharedConstants'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import UsersButtons from '../components/buttons/UsersButtons'; import UsersTable from '../components/tables/UsersTable'; diff --git a/client/app/bundles/system/admin/instance/instance/components/buttons/PendingInvitationsButtons.tsx b/client/app/bundles/system/admin/instance/instance/components/buttons/PendingInvitationsButtons.tsx index cdcb099f3cd..bf5374c7f2a 100644 --- a/client/app/bundles/system/admin/instance/instance/components/buttons/PendingInvitationsButtons.tsx +++ b/client/app/bundles/system/admin/instance/instance/components/buttons/PendingInvitationsButtons.tsx @@ -1,12 +1,12 @@ import { FC, memo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import equal from 'fast-deep-equal'; import { InvitationRowData } from 'types/system/instance/invitations'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import EmailButton from 'lib/components/core/buttons/EmailButton'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { deleteInvitation, resendInvitationEmail } from '../../operations'; diff --git a/client/app/bundles/system/admin/instance/instance/components/buttons/PendingRoleRequestsButtons.tsx b/client/app/bundles/system/admin/instance/instance/components/buttons/PendingRoleRequestsButtons.tsx index b46de1eebf2..f84819b2a46 100644 --- a/client/app/bundles/system/admin/instance/instance/components/buttons/PendingRoleRequestsButtons.tsx +++ b/client/app/bundles/system/admin/instance/instance/components/buttons/PendingRoleRequestsButtons.tsx @@ -1,6 +1,5 @@ import { FC, memo, useState } from 'react'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import equal from 'fast-deep-equal'; import { RoleRequestRowData } from 'types/system/instance/roleRequests'; @@ -9,6 +8,7 @@ import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import EmailButton from 'lib/components/core/buttons/EmailButton'; import { ROLE_REQUEST_ROLES } from 'lib/constants/sharedConstants'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import { approveRoleRequest, rejectRoleRequest } from '../../operations'; diff --git a/client/app/bundles/system/admin/instance/instance/components/buttons/ResendAllInvitationsButton.tsx b/client/app/bundles/system/admin/instance/instance/components/buttons/ResendAllInvitationsButton.tsx index 3792b9f024a..a598e22de5f 100644 --- a/client/app/bundles/system/admin/instance/instance/components/buttons/ResendAllInvitationsButton.tsx +++ b/client/app/bundles/system/admin/instance/instance/components/buttons/ResendAllInvitationsButton.tsx @@ -1,9 +1,9 @@ import { FC, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { LoadingButton } from '@mui/lab'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { resendAllInvitations } from '../../operations'; diff --git a/client/app/bundles/system/admin/instance/instance/components/buttons/UsersButtons.tsx b/client/app/bundles/system/admin/instance/instance/components/buttons/UsersButtons.tsx index 01ceee9793d..d12289dc1fa 100644 --- a/client/app/bundles/system/admin/instance/instance/components/buttons/UsersButtons.tsx +++ b/client/app/bundles/system/admin/instance/instance/components/buttons/UsersButtons.tsx @@ -1,12 +1,12 @@ import { FC, memo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import equal from 'fast-deep-equal'; import { InstanceUserMiniEntity } from 'types/system/instance/users'; import DeleteButton from 'lib/components/core/buttons/DeleteButton'; import { USER_ROLES } from 'lib/constants/sharedConstants'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { deleteUser } from '../../operations'; diff --git a/client/app/bundles/system/admin/instance/instance/components/forms/IndividualInviteForm.tsx b/client/app/bundles/system/admin/instance/instance/components/forms/IndividualInviteForm.tsx index 81b1c3782e1..8e664f922eb 100644 --- a/client/app/bundles/system/admin/instance/instance/components/forms/IndividualInviteForm.tsx +++ b/client/app/bundles/system/admin/instance/instance/components/forms/IndividualInviteForm.tsx @@ -1,7 +1,6 @@ import { FC, useEffect, useState } from 'react'; import { useFieldArray, useForm } from 'react-hook-form'; import { injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { yupResolver } from '@hookform/resolvers/yup'; import { IndividualInvites, @@ -12,6 +11,7 @@ import * as yup from 'yup'; import ErrorText from 'lib/components/core/ErrorText'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import formTranslations from 'lib/translations/form'; import messagesTranslations from 'lib/translations/messages'; diff --git a/client/app/bundles/system/admin/instance/instance/components/forms/InstanceUserRoleRequestForm.tsx b/client/app/bundles/system/admin/instance/instance/components/forms/InstanceUserRoleRequestForm.tsx index 59dfd800e1a..f6e9b998f7a 100644 --- a/client/app/bundles/system/admin/instance/instance/components/forms/InstanceUserRoleRequestForm.tsx +++ b/client/app/bundles/system/admin/instance/instance/components/forms/InstanceUserRoleRequestForm.tsx @@ -1,7 +1,6 @@ import { FC } from 'react'; import { Controller } from 'react-hook-form'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { RoleRequestBasicListData, UserRoleRequestForm, @@ -12,6 +11,7 @@ import FormDialog from 'lib/components/form/dialog/FormDialog'; import FormTextField from 'lib/components/form/fields/TextField'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import tableTranslations from 'lib/translations/table'; diff --git a/client/app/bundles/system/admin/instance/instance/components/forms/RejectWithMessageForm.tsx b/client/app/bundles/system/admin/instance/instance/components/forms/RejectWithMessageForm.tsx index 40b1dcdf61f..bbf530521bc 100644 --- a/client/app/bundles/system/admin/instance/instance/components/forms/RejectWithMessageForm.tsx +++ b/client/app/bundles/system/admin/instance/instance/components/forms/RejectWithMessageForm.tsx @@ -1,13 +1,13 @@ import { FC } from 'react'; import { Controller } from 'react-hook-form'; import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { TextField } from '@mui/material'; import { RoleRequestRowData } from 'types/system/instance/roleRequests'; import FormDialog from 'lib/components/form/dialog/FormDialog'; import FormTextField from 'lib/components/form/fields/TextField'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import tableTranslations from 'lib/translations/table'; diff --git a/client/app/bundles/system/admin/instance/instance/components/tables/UsersTable.tsx b/client/app/bundles/system/admin/instance/instance/components/tables/UsersTable.tsx index 2c4e1328a5a..69919198556 100644 --- a/client/app/bundles/system/admin/instance/instance/components/tables/UsersTable.tsx +++ b/client/app/bundles/system/admin/instance/instance/components/tables/UsersTable.tsx @@ -1,6 +1,5 @@ import { FC, ReactElement, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { CircularProgress, MenuItem, @@ -28,6 +27,7 @@ import { } from 'lib/constants/sharedConstants'; import rebuildObjectFromRow from 'lib/helpers/mui-datatables-helpers'; import { useAppDispatch } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import tableTranslations from 'lib/translations/table'; import { indexUsers, updateUser } from '../../operations'; diff --git a/client/app/bundles/system/admin/instance/instance/pages/InstanceAnnouncementsIndex.tsx b/client/app/bundles/system/admin/instance/instance/pages/InstanceAnnouncementsIndex.tsx index c870fa5df8d..f5631126b04 100644 --- a/client/app/bundles/system/admin/instance/instance/pages/InstanceAnnouncementsIndex.tsx +++ b/client/app/bundles/system/admin/instance/instance/pages/InstanceAnnouncementsIndex.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Button } from '@mui/material'; import AnnouncementsDisplay from 'bundles/course/announcements/components/misc/AnnouncementsDisplay'; @@ -9,6 +8,7 @@ import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import Note from 'lib/components/core/Note'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { createAnnouncement, diff --git a/client/app/bundles/system/admin/instance/instance/pages/InstanceComponentsIndex.tsx b/client/app/bundles/system/admin/instance/instance/pages/InstanceComponentsIndex.tsx index 28ab532db30..fd1e8216f88 100644 --- a/client/app/bundles/system/admin/instance/instance/pages/InstanceComponentsIndex.tsx +++ b/client/app/bundles/system/admin/instance/instance/pages/InstanceComponentsIndex.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Switch, Table, @@ -12,6 +11,7 @@ import { import { ComponentData } from 'types/system/instance/components'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; +import toast from 'lib/hooks/toast'; import tableTranslations from 'lib/translations/table'; import { indexComponents, updateComponents } from '../operations'; diff --git a/client/app/bundles/system/admin/instance/instance/pages/InstanceCoursesIndex.tsx b/client/app/bundles/system/admin/instance/instance/pages/InstanceCoursesIndex.tsx index 25eb256107b..2de35fd3bff 100644 --- a/client/app/bundles/system/admin/instance/instance/pages/InstanceCoursesIndex.tsx +++ b/client/app/bundles/system/admin/instance/instance/pages/InstanceCoursesIndex.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Typography } from '@mui/material'; import CoursesButtons from 'bundles/system/admin/admin/components/buttons/CoursesButtons'; @@ -10,6 +9,7 @@ import Link from 'lib/components/core/Link'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { DEFAULT_TABLE_ROWS_PER_PAGE } from 'lib/constants/sharedConstants'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import { deleteCourse, indexCourses } from '../operations'; import { getAdminCounts, getAllCourseMiniEntities } from '../selectors'; diff --git a/client/app/bundles/system/admin/instance/instance/pages/InstanceUserRoleRequestsIndex.tsx b/client/app/bundles/system/admin/instance/instance/pages/InstanceUserRoleRequestsIndex.tsx index 734789853f1..209ea39df2f 100644 --- a/client/app/bundles/system/admin/instance/instance/pages/InstanceUserRoleRequestsIndex.tsx +++ b/client/app/bundles/system/admin/instance/instance/pages/InstanceUserRoleRequestsIndex.tsx @@ -1,9 +1,9 @@ import { FC, useEffect, useMemo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import PendingRoleRequestsButtons from '../components/buttons/PendingRoleRequestsButtons'; import InstanceUserRoleRequestsTable from '../components/tables/InstanceUserRoleRequestsTable'; diff --git a/client/app/bundles/system/admin/instance/instance/pages/InstanceUsersIndex.tsx b/client/app/bundles/system/admin/instance/instance/pages/InstanceUsersIndex.tsx index 8725c0920e6..2d8667a24bd 100644 --- a/client/app/bundles/system/admin/instance/instance/pages/InstanceUsersIndex.tsx +++ b/client/app/bundles/system/admin/instance/instance/pages/InstanceUsersIndex.tsx @@ -1,6 +1,5 @@ import { FC, useEffect, useMemo, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import { Typography } from '@mui/material'; import SummaryCard from 'lib/components/core/layouts/SummaryCard'; @@ -8,6 +7,7 @@ import Link from 'lib/components/core/Link'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { DEFAULT_TABLE_ROWS_PER_PAGE } from 'lib/constants/sharedConstants'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import UsersButtons from '../components/buttons/UsersButtons'; import InstanceUsersTabs from '../components/navigation/InstanceUsersTabs'; diff --git a/client/app/bundles/system/admin/instance/instance/pages/InstanceUsersInvitations.tsx b/client/app/bundles/system/admin/instance/instance/pages/InstanceUsersInvitations.tsx index f88b319b043..9b1f092009e 100644 --- a/client/app/bundles/system/admin/instance/instance/pages/InstanceUsersInvitations.tsx +++ b/client/app/bundles/system/admin/instance/instance/pages/InstanceUsersInvitations.tsx @@ -1,9 +1,9 @@ import { FC, useEffect, useState } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import { toast } from 'react-toastify'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; +import toast from 'lib/hooks/toast'; import PendingInvitationsButtons from '../components/buttons/PendingInvitationsButtons'; import InstanceUsersTabs from '../components/navigation/InstanceUsersTabs'; diff --git a/client/app/bundles/user/AccountSettings/index.tsx b/client/app/bundles/user/AccountSettings/index.tsx index 49662afb936..c0160c2adbf 100644 --- a/client/app/bundles/user/AccountSettings/index.tsx +++ b/client/app/bundles/user/AccountSettings/index.tsx @@ -1,5 +1,4 @@ import { useState } from 'react'; -import { toast } from 'react-toastify'; import { TimeZones } from 'types/course/admin/course'; import { EmailData } from 'types/users'; @@ -7,6 +6,7 @@ import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { FormEmitter } from 'lib/components/form/Form'; import Preload from 'lib/components/wrappers/Preload'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/lib/components/core/AvatarSelector.tsx b/client/app/lib/components/core/AvatarSelector.tsx index 8c19004f513..e9ddfc7e7ba 100644 --- a/client/app/lib/components/core/AvatarSelector.tsx +++ b/client/app/lib/components/core/AvatarSelector.tsx @@ -1,11 +1,11 @@ import { ChangeEventHandler, useState } from 'react'; -import { toast } from 'react-toastify'; import { Create } from '@mui/icons-material'; import { Avatar, Button } from '@mui/material'; import translations from 'bundles/user/translations'; import ImageCropDialog from 'lib/components/core/dialogs/ImageCropDialog'; import Subsection from 'lib/components/core/layouts/Subsection'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import messagesTranslations from 'lib/translations/messages'; diff --git a/client/app/lib/components/core/layouts/ContactableErrorAlert.tsx b/client/app/lib/components/core/layouts/ContactableErrorAlert.tsx index f63773e384b..451eda63dc2 100644 --- a/client/app/lib/components/core/layouts/ContactableErrorAlert.tsx +++ b/client/app/lib/components/core/layouts/ContactableErrorAlert.tsx @@ -1,8 +1,8 @@ import { defineMessages } from 'react-intl'; -import { toast } from 'react-toastify'; import { Alert, AlertProps, Typography } from '@mui/material'; import Link from 'lib/components/core/Link'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; const translations = defineMessages({ diff --git a/client/app/lib/components/extensions/conditions/ConditionRow.tsx b/client/app/lib/components/extensions/conditions/ConditionRow.tsx index 4b8f4f72b8f..d96fd1f6e0e 100644 --- a/client/app/lib/components/extensions/conditions/ConditionRow.tsx +++ b/client/app/lib/components/extensions/conditions/ConditionRow.tsx @@ -1,9 +1,9 @@ import { createElement, useState } from 'react'; -import { toast } from 'react-toastify'; import { Create, Delete } from '@mui/icons-material'; import { IconButton, TableCell, TableRow, Typography } from '@mui/material'; import { ConditionData, ConditionsData } from 'types/course/conditions'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import specify from './specifiers'; @@ -60,7 +60,7 @@ const ConditionRow = ( {props.condition.description}

-
+
{editing ? ( createElement(component, { condition: props.condition, diff --git a/client/app/lib/components/extensions/conditions/ConditionsManager.tsx b/client/app/lib/components/extensions/conditions/ConditionsManager.tsx index 4d0c2afeb95..b7eea88c9ba 100644 --- a/client/app/lib/components/extensions/conditions/ConditionsManager.tsx +++ b/client/app/lib/components/extensions/conditions/ConditionsManager.tsx @@ -5,7 +5,6 @@ import { useRef, useState, } from 'react'; -import { toast } from 'react-toastify'; import { Add } from '@mui/icons-material'; import { Button, @@ -28,6 +27,7 @@ import { } from 'types/course/conditions'; import Subsection from 'lib/components/core/layouts/Subsection'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import formTranslations from 'lib/translations/form'; diff --git a/client/app/lib/components/form/Form.tsx b/client/app/lib/components/form/Form.tsx index 3739a777e83..77e37f6d214 100644 --- a/client/app/lib/components/form/Form.tsx +++ b/client/app/lib/components/form/Form.tsx @@ -13,13 +13,13 @@ import { useForm, UseFormWatch, } from 'react-hook-form'; -import { toast } from 'react-toastify'; import { yupResolver } from '@hookform/resolvers/yup'; import { Button, Slide, Typography } from '@mui/material'; import { isEmpty } from 'lodash'; import { AnyObjectSchema } from 'yup'; import { setReactHookFormError } from 'lib/helpers/react-hook-form-helper'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import translations from 'lib/translations/form'; import messagesTranslations from 'lib/translations/messages'; diff --git a/client/app/lib/components/wrappers/Preload.tsx b/client/app/lib/components/wrappers/Preload.tsx index 43cd7887f92..340313d3f10 100644 --- a/client/app/lib/components/wrappers/Preload.tsx +++ b/client/app/lib/components/wrappers/Preload.tsx @@ -1,8 +1,8 @@ import { DependencyList, useEffect, useState } from 'react'; -import { toast } from 'react-toastify'; import { AxiosError } from 'axios'; import ErrorCard from 'lib/components/core/ErrorCard'; +import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; import messagesTranslations from 'lib/translations/messages'; From e86e22228576d148df99570e4f98375b094b26b9 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:00:07 +0800 Subject: [PATCH 068/173] style(prettier): fix class names ordering --- .../AssessmentCategoriesManager/Category.tsx | 2 +- .../AssessmentSettings/AssessmentCategoriesManager/Tab.tsx | 4 ++-- .../admin/pages/VideosSettings/VideosTabsManager/Tab.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Category.tsx b/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Category.tsx index 27401555a8d..fa104690755 100644 --- a/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Category.tsx +++ b/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Category.tsx @@ -177,7 +177,7 @@ const Category = (props: CategoryProps): JSX.Element => { {!renaming && ( setRenaming(true)} size="small" diff --git a/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Tab.tsx b/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Tab.tsx index f468e31f34f..5e42c1ecb98 100644 --- a/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Tab.tsx +++ b/client/app/bundles/course/admin/pages/AssessmentSettings/AssessmentCategoriesManager/Tab.tsx @@ -139,7 +139,7 @@ const Tab = (props: TabProps): JSX.Element => { {!renaming && ( setRenaming(true)} size="small" @@ -151,7 +151,7 @@ const Tab = (props: TabProps): JSX.Element => { {tab.canDeleteTab && !stationary && ( { {!renaming && ( setRenaming(true)} size="small" @@ -105,7 +105,7 @@ const Tab = (props: TabProps): JSX.Element => { {tab.canDeleteTab && ( setDeleting(true)} From 3a87a2a2bb8062e2be5825b3f115314d3701d556 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:07:04 +0800 Subject: [PATCH 069/173] feat: migrate more Link `href`s to `to` --- .../components/AssessmentForm/index.tsx | 4 ++-- .../AssessmentShow/AssessmentShowPage.tsx | 6 ++--- .../pages/AssessmentShow/NewQuestionMenu.tsx | 2 +- .../pages/AssessmentShow/Question.tsx | 16 +++++++------ .../components/CommonQuestionFields.tsx | 2 +- .../answers/ForumPostResponse/ForumCard.jsx | 16 ++++++------- .../answers/ForumPostResponse/TopicCard.jsx | 16 ++++++------- .../submission/pages/LogsIndex/LogsHead.tsx | 2 +- .../SubmissionsIndex/SubmissionsTableRow.jsx | 2 +- .../buttons/SubmissionsTableButton.tsx | 24 +++++++++++-------- .../bundles/course/assessment/translations.ts | 7 ++---- .../components/tables/DisbursementTable.tsx | 2 +- .../components/buttons/SubscribeButton.tsx | 2 +- .../components/tables/LeaderboardTable.tsx | 9 +++---- .../course/StudentPerformanceTable.jsx | 8 +++---- .../students/StudentsStatisticsTable.jsx | 6 ++--- .../users/components/tables/CoursesTable.tsx | 4 ++-- .../components/extensions/StackedBadges.tsx | 6 ++--- .../conditions/AssessmentCondition.tsx | 2 +- .../conditions/conditions/SurveyCondition.tsx | 2 +- 20 files changed, 68 insertions(+), 70 deletions(-) diff --git a/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx b/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx index 758c6226421..27cbab0dd4b 100644 --- a/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx +++ b/client/app/bundles/course/assessment/components/AssessmentForm/index.tsx @@ -684,7 +684,7 @@ const AssessmentForm = (props: AssessmentFormProps): JSX.Element => { ( - + {chunk} ), @@ -729,7 +729,7 @@ const AssessmentForm = (props: AssessmentFormProps): JSX.Element => { > {t(translations.secretHint, { pulsegrid: (chunk) => ( - + {chunk} ), diff --git a/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowPage.tsx b/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowPage.tsx index 306974bc7f1..d9ad8ff607c 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowPage.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowPage.tsx @@ -55,7 +55,7 @@ const AssessmentShowPage = (props: AssessmentShowPageProps): JSX.Element => { {assessment.materialsDisabled && ( {t(translations.materialsDisabledHint)}  - + {t(translations.manageComponents)} @@ -118,10 +118,8 @@ const AssessmentShowPage = (props: AssessmentShowPageProps): JSX.Element => { {assessment.unlocks.map((condition) => ( diff --git a/client/app/bundles/course/assessment/pages/AssessmentShow/NewQuestionMenu.tsx b/client/app/bundles/course/assessment/pages/AssessmentShow/NewQuestionMenu.tsx index 92994a944a5..2f56240efe5 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentShow/NewQuestionMenu.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentShow/NewQuestionMenu.tsx @@ -50,7 +50,7 @@ const NewQuestionMenu = (props: NewQuestionMenuProps): JSX.Element => { open={creating} > {newQuestionUrls.map((url) => ( - + {t(NEW_QUESTION_LABELS[url.type])} ))} diff --git a/client/app/bundles/course/assessment/pages/AssessmentShow/Question.tsx b/client/app/bundles/course/assessment/pages/AssessmentShow/Question.tsx index 899bf9e4969..c981bb620ae 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentShow/Question.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentShow/Question.tsx @@ -4,6 +4,7 @@ import { ContentCopy, Create, DragIndicator } from '@mui/icons-material'; import { Chip, IconButton, Tooltip, Typography } from '@mui/material'; import { QuestionData } from 'types/course/assessment/questions'; +import Link from 'lib/components/core/Link'; import useTranslation from 'lib/hooks/useTranslation'; import translations from '../../translations'; @@ -124,13 +125,14 @@ const Question = (props: QuestionProps): JSX.Element => { {question.editUrl && ( - - - + + + + + )} diff --git a/client/app/bundles/course/assessment/question/components/CommonQuestionFields.tsx b/client/app/bundles/course/assessment/question/components/CommonQuestionFields.tsx index 173d262f91e..b8257abff50 100644 --- a/client/app/bundles/course/assessment/question/components/CommonQuestionFields.tsx +++ b/client/app/bundles/course/assessment/question/components/CommonQuestionFields.tsx @@ -150,7 +150,7 @@ const CommonQuestionFields = ( : translations.noSkillsCanCreateSkills, { url: (chunks) => ( - + {chunks} ), diff --git a/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/ForumCard.jsx b/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/ForumCard.jsx index 822c997685d..76f115dd746 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/ForumCard.jsx +++ b/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/ForumCard.jsx @@ -1,6 +1,5 @@ import { Component } from 'react'; import { defineMessages, FormattedMessage } from 'react-intl'; -import { OpenInNew } from '@mui/icons-material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { Accordion, @@ -16,6 +15,7 @@ import { forumTopicPostPackShape, postPackShape, } from 'course/assessment/submission/propTypes'; +import Link from 'lib/components/core/Link'; import { getForumURL } from 'lib/helpers/url-builders'; import CardTitle from './CardTitle'; @@ -114,17 +114,17 @@ export default class ForumCard extends Component { - + +
diff --git a/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/TopicCard.jsx b/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/TopicCard.jsx index 9dcf9d6e540..b26a612ff61 100644 --- a/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/TopicCard.jsx +++ b/client/app/bundles/course/assessment/submission/components/answers/ForumPostResponse/TopicCard.jsx @@ -1,6 +1,5 @@ import { Component } from 'react'; import { defineMessages, FormattedMessage } from 'react-intl'; -import { OpenInNew } from '@mui/icons-material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { Accordion, @@ -16,6 +15,7 @@ import { postPackShape, topicOverviewShape, } from 'course/assessment/submission/propTypes'; +import Link from 'lib/components/core/Link'; import { getForumTopicURL } from 'lib/helpers/url-builders'; import CardTitle from './CardTitle'; @@ -108,14 +108,14 @@ export default class TopicCard extends Component { - + +
diff --git a/client/app/bundles/course/assessment/submission/pages/LogsIndex/LogsHead.tsx b/client/app/bundles/course/assessment/submission/pages/LogsIndex/LogsHead.tsx index e468136f920..793247beb66 100644 --- a/client/app/bundles/course/assessment/submission/pages/LogsIndex/LogsHead.tsx +++ b/client/app/bundles/course/assessment/submission/pages/LogsIndex/LogsHead.tsx @@ -55,7 +55,7 @@ const LogsHead: FC = (props) => { {t(translations.studentName)} - {info.studentName} + {info.studentName} diff --git a/client/app/bundles/course/assessment/submission/pages/SubmissionsIndex/SubmissionsTableRow.jsx b/client/app/bundles/course/assessment/submission/pages/SubmissionsIndex/SubmissionsTableRow.jsx index 6c61248151c..d61362a5e10 100644 --- a/client/app/bundles/course/assessment/submission/pages/SubmissionsIndex/SubmissionsTableRow.jsx +++ b/client/app/bundles/course/assessment/submission/pages/SubmissionsIndex/SubmissionsTableRow.jsx @@ -216,7 +216,6 @@ const SubmissionsTableRow = (props) => { { textColor: 'white', color: palette.links, }} + to={getEditSubmissionURL(courseId, assessmentId, submission.id)} variant="filled" /> )} diff --git a/client/app/bundles/course/assessment/submissions/components/buttons/SubmissionsTableButton.tsx b/client/app/bundles/course/assessment/submissions/components/buttons/SubmissionsTableButton.tsx index a312ead2b18..bffa0e9b386 100644 --- a/client/app/bundles/course/assessment/submissions/components/buttons/SubmissionsTableButton.tsx +++ b/client/app/bundles/course/assessment/submissions/components/buttons/SubmissionsTableButton.tsx @@ -2,6 +2,7 @@ import { FC } from 'react'; import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; import { Button } from '@mui/material'; +import Link from 'lib/components/core/Link'; import { getEditAssessmentSubmissionURL } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; @@ -26,21 +27,24 @@ const SubmissionsTableButton: FC = (props) => { const { intl, canGrade, assessmentId, submissionId } = props; return ( - + + ); }; diff --git a/client/app/bundles/course/assessment/translations.ts b/client/app/bundles/course/assessment/translations.ts index c5525e5928c..820fcfc0841 100644 --- a/client/app/bundles/course/assessment/translations.ts +++ b/client/app/bundles/course/assessment/translations.ts @@ -361,17 +361,14 @@ const translations = defineMessages({ }, questionDuplicated: { id: 'course.assessment.show.questionDuplicated', - defaultMessage: 'Your question has been duplicated.', + defaultMessage: + 'Your question has been duplicated. Go to the assessment', }, questionDuplicatedRefreshing: { id: 'course.assessment.show.questionDuplicatedRefreshing', defaultMessage: 'Your question has been duplicated. We are refreshing to show you the latest changes.', }, - goToAssessment: { - id: 'course.assessment.show.goToAssessment', - defaultMessage: 'Go to the assessment', - }, errorDuplicatingQuestion: { id: 'course.assessment.show.errorDuplicatingQuestion', defaultMessage: 'An error occurred when duplicating your question.', diff --git a/client/app/bundles/course/experience-points/disbursement/components/tables/DisbursementTable.tsx b/client/app/bundles/course/experience-points/disbursement/components/tables/DisbursementTable.tsx index d07b2cee48f..e63156decad 100644 --- a/client/app/bundles/course/experience-points/disbursement/components/tables/DisbursementTable.tsx +++ b/client/app/bundles/course/experience-points/disbursement/components/tables/DisbursementTable.tsx @@ -79,8 +79,8 @@ const DisbursementTable: FC = (props: Props) => { }), customBodyRenderLite: (dataIndex): JSX.Element => ( {filteredUsers[dataIndex].name} diff --git a/client/app/bundles/course/forum/components/buttons/SubscribeButton.tsx b/client/app/bundles/course/forum/components/buttons/SubscribeButton.tsx index 20d8fef56fb..b8d4612449c 100644 --- a/client/app/bundles/course/forum/components/buttons/SubscribeButton.tsx +++ b/client/app/bundles/course/forum/components/buttons/SubscribeButton.tsx @@ -137,8 +137,8 @@ const SubscribeButton: FC = ({ subscribedTooltip = t(translations.userSettingSubscribed, { manageMySubscriptionLink: ( {t(commonTranslations.manageMySubscriptions)} diff --git a/client/app/bundles/course/leaderboard/components/tables/LeaderboardTable.tsx b/client/app/bundles/course/leaderboard/components/tables/LeaderboardTable.tsx index 1c49297cd72..f8a4e309712 100644 --- a/client/app/bundles/course/leaderboard/components/tables/LeaderboardTable.tsx +++ b/client/app/bundles/course/leaderboard/components/tables/LeaderboardTable.tsx @@ -139,12 +139,9 @@ const LeaderboardTable: FC = (props: Props) => { = (props: Props) => { alt={achievement.badge.name} className="achievement" component={Link} - href={getAchievementURL(getCourseId(), achievement.id)} id={`achievement_${achievement.id}`} src={achievement.badge.url} + to={getAchievementURL(getCourseId(), achievement.id)} underline="none" /> @@ -296,8 +293,8 @@ const LeaderboardTable: FC = (props: Props) => { diff --git a/client/app/bundles/course/statistics/pages/StatisticsIndex/course/StudentPerformanceTable.jsx b/client/app/bundles/course/statistics/pages/StatisticsIndex/course/StudentPerformanceTable.jsx index 3dc0abfd1ea..8b8577316c6 100644 --- a/client/app/bundles/course/statistics/pages/StatisticsIndex/course/StudentPerformanceTable.jsx +++ b/client/app/bundles/course/statistics/pages/StatisticsIndex/course/StudentPerformanceTable.jsx @@ -245,7 +245,7 @@ const StudentPerformanceTable = ({ customBodyRenderLite: (dataIndex) => { const student = displayedStudents[dataIndex]; return ( - + {student.name} ); @@ -303,7 +303,7 @@ const StudentPerformanceTable = ({ <> {groupManagers.map((m, index) => ( - + {m.name} {index < groupManagers.length - 1 && ', '} @@ -347,8 +347,8 @@ const StudentPerformanceTable = ({ return ( {student.experiencePoints} @@ -428,8 +428,8 @@ const StudentPerformanceTable = ({ return ( {student.videoSubmissionCount} diff --git a/client/app/bundles/course/statistics/pages/StatisticsIndex/students/StudentsStatisticsTable.jsx b/client/app/bundles/course/statistics/pages/StatisticsIndex/students/StudentsStatisticsTable.jsx index 191e9a269a4..9e243650864 100644 --- a/client/app/bundles/course/statistics/pages/StatisticsIndex/students/StudentsStatisticsTable.jsx +++ b/client/app/bundles/course/statistics/pages/StatisticsIndex/students/StudentsStatisticsTable.jsx @@ -168,7 +168,7 @@ const StudentsStatisticsTable = ({ metadata, students }) => { <> {groupManagers.map((m, index) => ( - + {m.name} {index < groupManagers.length - 1 && ', '} @@ -201,8 +201,8 @@ const StudentsStatisticsTable = ({ metadata, students }) => { return ( {student.experiencePoints} @@ -224,8 +224,8 @@ const StudentsStatisticsTable = ({ metadata, students }) => { return ( {student.videoSubmissionCount} diff --git a/client/app/bundles/users/components/tables/CoursesTable.tsx b/client/app/bundles/users/components/tables/CoursesTable.tsx index 37dc18e610f..173dffa975f 100644 --- a/client/app/bundles/users/components/tables/CoursesTable.tsx +++ b/client/app/bundles/users/components/tables/CoursesTable.tsx @@ -52,8 +52,8 @@ const CoursesTable: FC = ({ title, courses, intl }: Props) => { {course.title} @@ -62,8 +62,8 @@ const CoursesTable: FC = ({ title, courses, intl }: Props) => { {course.courseUserName} diff --git a/client/app/lib/components/extensions/StackedBadges.tsx b/client/app/lib/components/extensions/StackedBadges.tsx index f91a7d6e9c4..ea6abb338c3 100644 --- a/client/app/lib/components/extensions/StackedBadges.tsx +++ b/client/app/lib/components/extensions/StackedBadges.tsx @@ -36,16 +36,16 @@ const StackedBadges = (props: StackedBadgesProps): JSX.Element => ( ))} - {props.remainingCount && ( - + {Boolean(props.remainingCount) && ( + {t(translations.details)} diff --git a/client/app/lib/components/extensions/conditions/conditions/SurveyCondition.tsx b/client/app/lib/components/extensions/conditions/conditions/SurveyCondition.tsx index 9847351efe4..609320b56c5 100644 --- a/client/app/lib/components/extensions/conditions/conditions/SurveyCondition.tsx +++ b/client/app/lib/components/extensions/conditions/conditions/SurveyCondition.tsx @@ -77,8 +77,8 @@ const SurveyConditionForm = ( {t(translations.details)} From 7b60a6bb521cd76689dab811992b4ef11cb91bc2 Mon Sep 17 00:00:00 2001 From: Eko Widianto Date: Tue, 1 Aug 2023 18:09:09 +0800 Subject: [PATCH 070/173] feat(app): add AuthenticatedApp, UnauthenticatedApp --- client/app/App.tsx | 6 +- .../core/buttons/MasqueradeButton.tsx | 9 +- client/app/router.tsx | 775 ----------------- client/app/routers/AuthenticatableApp.tsx | 24 + client/app/routers/AuthenticatedApp.tsx | 809 ++++++++++++++++++ client/app/routers/UnauthenticatedApp.tsx | 109 +++ client/app/routers/index.ts | 1 + client/app/routers/redirects.tsx | 37 + client/app/routers/router.tsx | 66 ++ 9 files changed, 1054 insertions(+), 782 deletions(-) delete mode 100644 client/app/router.tsx create mode 100644 client/app/routers/AuthenticatableApp.tsx create mode 100644 client/app/routers/AuthenticatedApp.tsx create mode 100644 client/app/routers/UnauthenticatedApp.tsx create mode 100644 client/app/routers/index.ts create mode 100644 client/app/routers/redirects.tsx create mode 100644 client/app/routers/router.tsx diff --git a/client/app/App.tsx b/client/app/App.tsx index 4539ec6e7ab..efafd1321b9 100644 --- a/client/app/App.tsx +++ b/client/app/App.tsx @@ -1,13 +1,11 @@ -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; - import Providers from 'lib/components/wrappers/Providers'; -import router from './router'; +import AuthenticatableApp from './routers/AuthenticatableApp'; import { store } from './store'; const App = (): JSX.Element => ( - + ); diff --git a/client/app/lib/components/core/buttons/MasqueradeButton.tsx b/client/app/lib/components/core/buttons/MasqueradeButton.tsx index cf0d4c8d341..16cc657f01e 100644 --- a/client/app/lib/components/core/buttons/MasqueradeButton.tsx +++ b/client/app/lib/components/core/buttons/MasqueradeButton.tsx @@ -2,6 +2,7 @@ import { defineMessages } from 'react-intl'; import TheaterComedy from '@mui/icons-material/TheaterComedy'; import { IconButton, IconButtonProps, Tooltip } from '@mui/material'; +import Link from 'lib/components/core/Link'; import useTranslation from 'lib/hooks/useTranslation'; interface Props extends IconButtonProps { @@ -22,8 +23,10 @@ const translations = defineMessages({ }); const MasqueradeButton = (props: Props): JSX.Element => { - const { canMasquerade, ...otherProps } = props; + const { canMasquerade, href, ...otherProps } = props; + const { t } = useTranslation(); + return ( { : t(translations.masqueradeDisabledTooltip) } > - + - + ); }; diff --git a/client/app/router.tsx b/client/app/router.tsx deleted file mode 100644 index d4b968096d8..00000000000 --- a/client/app/router.tsx +++ /dev/null @@ -1,775 +0,0 @@ -/* eslint-disable sonarjs/no-duplicate-string */ -import { Navigate, RouteObject } from 'react-router-dom'; -import { resetStore } from 'store'; - -import GlobalAnnouncementIndex from 'bundles/announcements/GlobalAnnouncementIndex'; -import AchievementShow from 'bundles/course/achievement/pages/AchievementShow'; -import AchievementsIndex from 'bundles/course/achievement/pages/AchievementsIndex'; -import SettingsNavigation from 'bundles/course/admin/components/SettingsNavigation'; -import AnnouncementSettings from 'bundles/course/admin/pages/AnnouncementsSettings'; -import AssessmentSettings from 'bundles/course/admin/pages/AssessmentSettings'; -import CodaveriSettings from 'bundles/course/admin/pages/CodaveriSettings'; -import CommentsSettings from 'bundles/course/admin/pages/CommentsSettings'; -import ComponentSettings from 'bundles/course/admin/pages/ComponentSettings'; -import CourseSettings from 'bundles/course/admin/pages/CourseSettings'; -import ForumsSettings from 'bundles/course/admin/pages/ForumsSettings'; -import LeaderboardSettings from 'bundles/course/admin/pages/LeaderboardSettings'; -import LessonPlanSettings from 'bundles/course/admin/pages/LessonPlanSettings'; -import MaterialsSettings from 'bundles/course/admin/pages/MaterialsSettings'; -import NotificationSettings from 'bundles/course/admin/pages/NotificationSettings'; -import SidebarSettings from 'bundles/course/admin/pages/SidebarSettings'; -import VideosSettings from 'bundles/course/admin/pages/VideosSettings'; -import AnnouncementsIndex from 'bundles/course/announcements/pages/AnnouncementsIndex'; -import AssessmentEdit from 'bundles/course/assessment/pages/AssessmentEdit'; -import AssessmentMonitoring from 'bundles/course/assessment/pages/AssessmentMonitoring'; -import AssessmentShow from 'bundles/course/assessment/pages/AssessmentShow'; -import AssessmentsIndex from 'bundles/course/assessment/pages/AssessmentsIndex'; -import AssessmentStatisticsPage from 'bundles/course/assessment/pages/AssessmentStatistics'; -import EditForumPostResponsePage from 'bundles/course/assessment/question/forum-post-responses/EditForumPostResponsePage'; -import NewForumPostResponsePage from 'bundles/course/assessment/question/forum-post-responses/NewForumPostResponsePage'; -import EditMcqMrqPage from 'bundles/course/assessment/question/multiple-responses/EditMcqMrqPage'; -import NewMcqMrqPage from 'bundles/course/assessment/question/multiple-responses/NewMcqMrqPage'; -import EditProgrammingQuestionPage from 'bundles/course/assessment/question/programming/EditProgrammingQuestionPage'; -import NewProgrammingQuestionPage from 'bundles/course/assessment/question/programming/NewProgrammingQuestionPage'; -import ScribingQuestion from 'bundles/course/assessment/question/scribing/ScribingQuestion'; -import EditTextResponse from 'bundles/course/assessment/question/text-responses/EditTextResponsePage'; -import NewTextResponse from 'bundles/course/assessment/question/text-responses/NewTextResponsePage'; -import EditVoicePage from 'bundles/course/assessment/question/voice-responses/EditVoicePage'; -import NewVoicePage from 'bundles/course/assessment/question/voice-responses/NewVoicePage'; -import AssessmentSessionNew from 'bundles/course/assessment/sessions/pages/AssessmentSessionNew'; -import SkillsIndex from 'bundles/course/assessment/skills/pages/SkillsIndex'; -import LogsIndex from 'bundles/course/assessment/submission/pages/LogsIndex'; -import SubmissionEditIndex from 'bundles/course/assessment/submission/pages/SubmissionEditIndex'; -import AssessmentSubmissionsIndex from 'bundles/course/assessment/submission/pages/SubmissionsIndex'; -import SubmissionsIndex from 'bundles/course/assessment/submissions/SubmissionsIndex'; -import CourseShow from 'bundles/course/courses/pages/CourseShow'; -import CoursesIndex from 'bundles/course/courses/pages/CoursesIndex'; -import CommentIndex from 'bundles/course/discussion/topics/pages/CommentIndex'; -import Duplication from 'bundles/course/duplication/pages/Duplication'; -import UserRequests from 'bundles/course/enrol-requests/pages/UserRequests'; -import DisbursementIndex from 'bundles/course/experience-points/disbursement/pages/DisbursementIndex'; -import ForumShow from 'bundles/course/forum/pages/ForumShow'; -import ForumsIndex from 'bundles/course/forum/pages/ForumsIndex'; -import ForumTopicShow from 'bundles/course/forum/pages/ForumTopicShow'; -import GroupIndex from 'bundles/course/group/pages/GroupIndex'; -import GroupShow from 'bundles/course/group/pages/GroupShow'; -import LeaderboardIndex from 'bundles/course/leaderboard/pages/LeaderboardIndex'; -import LearningMap from 'bundles/course/learning-map/containers/LearningMap'; -import LessonPlanLayout from 'bundles/course/lesson-plan/containers/LessonPlanLayout'; -import LessonPlanEdit from 'bundles/course/lesson-plan/pages/LessonPlanEdit'; -import LessonPlanShow from 'bundles/course/lesson-plan/pages/LessonPlanShow'; -import LevelsIndex from 'bundles/course/level/pages/LevelsIndex'; -import FolderShow from 'bundles/course/material/folders/pages/FolderShow'; -import TimelineDesigner from 'bundles/course/reference-timelines/TimelineDesigner'; -import StatisticsIndex from 'bundles/course/statistics/pages/StatisticsIndex'; -import ResponseEdit from 'bundles/course/survey/pages/ResponseEdit'; -import ResponseIndex from 'bundles/course/survey/pages/ResponseIndex'; -import ResponseShow from 'bundles/course/survey/pages/ResponseShow'; -import SurveyIndex from 'bundles/course/survey/pages/SurveyIndex'; -import SurveyResults from 'bundles/course/survey/pages/SurveyResults'; -import SurveyShow from 'bundles/course/survey/pages/SurveyShow'; -import UserEmailSubscriptions from 'bundles/course/user-email-subscriptions/UserEmailSubscriptions'; -import InvitationsIndex from 'bundles/course/user-invitations/pages/InvitationsIndex'; -import InviteUsers from 'bundles/course/user-invitations/pages/InviteUsers'; -import ExperiencePointsRecords from 'bundles/course/users/pages/ExperiencePointsRecords'; -import ManageStaff from 'bundles/course/users/pages/ManageStaff'; -import ManageStudents from 'bundles/course/users/pages/ManageStudents'; -import PersonalTimes from 'bundles/course/users/pages/PersonalTimes'; -import PersonalTimesShow from 'bundles/course/users/pages/PersonalTimesShow'; -import CourseUserShow from 'bundles/course/users/pages/UserShow'; -import UsersIndex from 'bundles/course/users/pages/UsersIndex'; -import VideoShow from 'bundles/course/video/pages/VideoShow'; -import VideosIndex from 'bundles/course/video/pages/VideosIndex'; -import VideoSubmissionEdit from 'bundles/course/video/submission/pages/VideoSubmissionEdit'; -import VideoSubmissionShow from 'bundles/course/video/submission/pages/VideoSubmissionShow'; -import VideoSubmissionsIndex from 'bundles/course/video/submission/pages/VideoSubmissionsIndex'; -import UserVideoSubmissionsIndex from 'bundles/course/video-submissions/pages/UserVideoSubmissionsIndex'; -import AdminNavigator from 'bundles/system/admin/admin/AdminNavigator'; -import AnnouncementIndex from 'bundles/system/admin/admin/pages/AnnouncementsIndex'; -import CourseIndex from 'bundles/system/admin/admin/pages/CoursesIndex'; -import InstancesIndex from 'bundles/system/admin/admin/pages/InstancesIndex'; -import UserIndex from 'bundles/system/admin/admin/pages/UsersIndex'; -import InstanceAdminNavigator from 'bundles/system/admin/instance/instance/InstanceAdminNavigator'; -import InstanceAnnouncementsIndex from 'bundles/system/admin/instance/instance/pages/InstanceAnnouncementsIndex'; -import InstanceComponentsIndex from 'bundles/system/admin/instance/instance/pages/InstanceComponentsIndex'; -import InstanceCoursesIndex from 'bundles/system/admin/instance/instance/pages/InstanceCoursesIndex'; -import InstanceUserRoleRequestsIndex from 'bundles/system/admin/instance/instance/pages/InstanceUserRoleRequestsIndex'; -import InstanceUsersIndex from 'bundles/system/admin/instance/instance/pages/InstanceUsersIndex'; -import InstanceUsersInvitations from 'bundles/system/admin/instance/instance/pages/InstanceUsersInvitations'; -import InstanceUsersInvite from 'bundles/system/admin/instance/instance/pages/InstanceUsersInvite'; -import AccountSettings from 'bundles/user/AccountSettings'; -import UserShow from 'bundles/users/pages/UserShow'; -import { achievementHandle } from 'course/achievement/handles'; -import { - assessmentHandle, - assessmentsHandle, - questionHandle, -} from 'course/assessment/handles'; -import QuestionFormOutlet from 'course/assessment/question/components/QuestionFormOutlet'; -import { forumHandle, forumTopicHandle } from 'course/forum/handles'; -import { folderHandle } from 'course/material/folders/handles'; -import { videoWatchHistoryHandle } from 'course/statistics/handles'; -import { surveyHandle, surveyResponseHandle } from 'course/survey/handles'; -import { - courseUserHandle, - courseUserPersonalizedTimelineHandle, - manageUserHandles, -} from 'course/users/handles'; -import { videoHandle, videosHandle } from 'course/video/handles'; - -import { CourseContainer } from './bundles/course/container'; -import AppContainer from './lib/containers/AppContainer'; -import CourselessContainer from './lib/containers/CourselessContainer'; - -const router: RouteObject[] = [ - { - path: '/', - element: , - loader: AppContainer.loader, - shouldRevalidate: (): boolean => false, - children: [ - { - path: 'courses/:courseId', - element: , - loader: CourseContainer.loader, - handle: CourseContainer.handle, - shouldRevalidate: ({ currentParams, nextParams }): boolean => { - const isChangingCourse = - currentParams.courseId !== nextParams.courseId; - - // React Router's documentation never strictly mentioned that `shouldRevalidate` - // should be a pure function, but a good software engineer would probably expect - // it to be. Until we multi-course support in our Redux store, this is where - // we can detect the `courseId` is changing without janky `useEffect`. It should - // be safe since `resetStore` does not interfere with rendering or routing. - if (isChangingCourse) resetStore(); - - return isChangingCourse; - }, - children: [ - { - index: true, - element: , - }, - { - path: 'timelines', - handle: TimelineDesigner.handle, - element: , - }, - { - path: 'announcements', - handle: AnnouncementsIndex.handle, - element: , - }, - { - path: 'comments', - handle: CommentIndex.handle, - element: , - }, - { - path: 'leaderboard', - handle: LeaderboardIndex.handle, - element: , - }, - { - path: 'learning_map', - handle: LearningMap.handle, - element: , - }, - { - path: 'materials/folders', - handle: folderHandle, - // `:folderId` must be split this way so that `folderHandle` is matched - // to the stable (non-changing) match of `/materials/folders`. This allows - // the crumbs in the Workbin to not disappear when revalidated by the - // Dynamic Nest API's builder. - children: [ - { - path: ':folderId', - element: , - }, - ], - }, - { - path: 'levels', - handle: LevelsIndex.handle, - element: , - }, - { - path: 'statistics', - handle: StatisticsIndex.handle, - element: , - }, - { - path: 'duplication', - handle: Duplication.handle, - element: , - }, - { - path: 'enrol_requests', - handle: manageUserHandles.enrolRequests, - element: , - }, - { - path: 'user_invitations', - handle: manageUserHandles.invitations, - element: , - }, - { - path: 'students', - handle: manageUserHandles.students, - element: , - }, - { - path: 'staff', - handle: manageUserHandles.staff, - element: , - }, - { - path: 'lesson_plan', - // @ts-ignore `connect` throws error when cannot find `store` as direct parent - element: , - handle: LessonPlanLayout.handle, - children: [ - { - index: true, - element: , - }, - { - path: 'edit', - element: , - }, - ], - }, - { - path: 'users', - children: [ - { - index: true, - handle: UsersIndex.handle, - element: , - }, - { - path: 'personal_times', - handle: manageUserHandles.personalizedTimelines, - element: , - }, - { - path: 'invite', - handle: manageUserHandles.inviteUsers, - element: , - }, - { - path: 'disburse_experience_points', - handle: DisbursementIndex.handle, - element: , - }, - { - path: ':userId', - handle: courseUserHandle, - children: [ - { - index: true, - element: , - }, - { - path: 'experience_points_records', - handle: ExperiencePointsRecords.handle, - element: , - }, - ], - }, - { - path: ':userId/personal_times', - handle: courseUserPersonalizedTimelineHandle, - element: , - }, - { - path: ':userId/video_submissions', - handle: videoWatchHistoryHandle, - element: , - }, - { - path: ':userId/manage_email_subscription', - handle: UserEmailSubscriptions.handle, - element: , - }, - ], - }, - { - path: 'admin', - loader: SettingsNavigation.loader, - handle: SettingsNavigation.handle, - element: , - children: [ - { - index: true, - element: , - }, - { - path: 'components', - element: , - }, - { - path: 'sidebar', - element: , - }, - { - path: 'notifications', - element: , - }, - { - path: 'announcements', - element: , - }, - { - path: 'assessments', - element: , - }, - { - path: 'materials', - element: , - }, - { - path: 'forums', - element: , - }, - { - path: 'leaderboard', - element: , - }, - { - path: 'comments', - element: , - }, - { - path: 'videos', - element: , - }, - { - path: 'lesson_plan', - element: , - }, - { - path: 'codaveri', - element: , - }, - ], - }, - { - path: 'surveys', - handle: SurveyIndex.handle, - children: [ - { - index: true, - element: , - }, - { - path: ':surveyId', - handle: surveyHandle, - children: [ - { - index: true, - element: , - }, - { - path: 'results', - handle: SurveyResults.handle, - element: , - }, - { - path: 'responses', - children: [ - { - index: true, - handle: ResponseIndex.handle, - element: , - }, - { - path: ':responseId', - children: [ - { - index: true, - handle: surveyResponseHandle, - element: , - }, - { - path: 'edit', - handle: ResponseEdit.handle, - element: , - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - path: 'groups', - element: , - handle: GroupIndex.handle, - children: [ - { - path: ':groupCategoryId', - element: , - }, - ], - }, - { - path: 'videos', - handle: videosHandle, - children: [ - { - index: true, - element: , - }, - { - path: ':videoId', - handle: videoHandle, - children: [ - { - index: true, - element: , - }, - { - path: 'submissions', - children: [ - { - index: true, - handle: VideoSubmissionsIndex.handle, - element: , - }, - { - path: ':submissionId', - handle: VideoSubmissionShow.handle, - children: [ - { - index: true, - element: , - }, - { - path: 'edit', - element: , - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - path: 'forums', - handle: ForumsIndex.handle, - children: [ - { - index: true, - element: , - }, - { - path: ':forumId', - handle: forumHandle, - children: [ - { - index: true, - element: , - }, - { - path: 'topics/:topicId', - handle: forumTopicHandle, - element: , - }, - ], - }, - ], - }, - { - path: 'achievements', - handle: AchievementsIndex.handle, - children: [ - { - index: true, - element: , - }, - { - path: ':achievementId', - handle: achievementHandle, - element: , - }, - ], - }, - { - path: 'assessments', - handle: assessmentsHandle, - children: [ - { - index: true, - element: , - }, - { - path: 'submissions', - handle: SubmissionsIndex.handle, - element: , - }, - { - path: 'skills', - handle: SkillsIndex.handle, - element: , - }, - { - path: ':assessmentId', - handle: assessmentHandle, - children: [ - { - index: true, - element: , - }, - { - path: 'edit', - handle: AssessmentEdit.handle, - element: , - }, - { - path: 'monitoring', - handle: AssessmentMonitoring.handle, - element: , - }, - { - path: 'sessions/new', - element: , - }, - { - path: 'statistics', - handle: AssessmentStatisticsPage.handle, - // @ts-ignore `connect` throws error when cannot find `store` as direct parent - element: , - }, - { - path: 'submissions', - children: [ - { - index: true, - handle: AssessmentSubmissionsIndex.handle, - element: , - }, - { - path: ':submissionId', - children: [ - { - path: 'edit', - handle: SubmissionEditIndex.handle, - element: , - }, - { - path: 'logs', - handle: LogsIndex.handle, - element: , - }, - ], - }, - ], - }, - { - path: 'question', - element: , - handle: questionHandle, - children: [ - { - path: 'forum_post_responses', - children: [ - { - path: 'new', - handle: NewForumPostResponsePage.handle, - element: , - }, - { - path: ':questionId/edit', - element: , - }, - ], - }, - { - path: 'text_responses', - children: [ - { - path: 'new', - handle: NewTextResponse.handle, - element: , - }, - { - path: ':questionId/edit', - element: , - }, - ], - }, - { - path: 'voice_responses', - children: [ - { - path: 'new', - handle: NewVoicePage.handle, - element: , - }, - { - path: ':questionId/edit', - element: , - }, - ], - }, - { - path: 'multiple_responses', - children: [ - { - path: 'new', - handle: NewMcqMrqPage.handle, - element: , - }, - { - path: ':questionId/edit', - element: , - }, - ], - }, - { - path: 'scribing', - children: [ - { - path: 'new', - handle: ScribingQuestion.handle, - element: , - }, - { - path: ':questionId/edit', - element: , - }, - ], - }, - { - path: 'programming', - children: [ - { - path: 'new', - handle: NewProgrammingQuestionPage.handle, - element: , - }, - { - path: ':questionId/edit', - element: , - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - path: '*', - element: , - children: [ - { - path: 'courses', - handle: CoursesIndex.handle, - element: , - }, - { - path: 'admin', - handle: AdminNavigator.handle, - element: , - children: [ - { - index: true, - element: , - }, - { - path: 'announcements', - element: , - }, - { - path: 'users', - element: , - }, - { - path: 'instances', - element: , - }, - { - path: 'courses', - element: , - }, - ], - }, - { - path: 'admin/instance', - handle: InstanceAdminNavigator.handle, - element: , - children: [ - { - index: true, - element: , - }, - { - path: 'announcements', - element: , - }, - { - path: 'components', - element: , - }, - { - path: 'courses', - element: , - }, - { - path: 'users', - element: , - }, - { - path: 'users/invite', - element: , - }, - { - path: 'user_invitations', - element: , - }, - { - path: 'role_requests', - element: , - }, - ], - }, - { - path: 'announcements', - handle: GlobalAnnouncementIndex.handle, - element: , - }, - { - path: 'users/:userId', - element: , - }, - { - path: 'user/profile/edit', - handle: AccountSettings.handle, - element: , - }, - // `/role_requests` will be routed without `InstanceAdminNavigator` and its sidebar. - // Here, we redirect `/role_requests` to `/admin/instance/role_requests`. But until - // we are loading the page without Rails' router, fresh loads to `/role_requests` will - // go to 404. Once we are 100% SPA, this is a non-issue. - { - path: 'role_requests', - element: , - }, - ], - }, - ], - }, -]; - -export default router; diff --git a/client/app/routers/AuthenticatableApp.tsx b/client/app/routers/AuthenticatableApp.tsx new file mode 100644 index 00000000000..678f5225cda --- /dev/null +++ b/client/app/routers/AuthenticatableApp.tsx @@ -0,0 +1,24 @@ +import { lazy, Suspense } from 'react'; + +import { useAuthState } from 'lib/hooks/session'; + +const AuthenticatedApp = lazy( + () => import(/* webpackChunkName: "AuthenticatedApp" */ './AuthenticatedApp'), +); + +const UnauthenticatedApp = lazy( + () => + import(/* webpackChunkName: "UnauthenticatedApp" */ './UnauthenticatedApp'), +); + +const AuthenticatableApp = (): JSX.Element => { + const authenticated = useAuthState(); + + return ( + + {authenticated ? : } + + ); +}; + +export default AuthenticatableApp; diff --git a/client/app/routers/AuthenticatedApp.tsx b/client/app/routers/AuthenticatedApp.tsx new file mode 100644 index 00000000000..93df6e4c448 --- /dev/null +++ b/client/app/routers/AuthenticatedApp.tsx @@ -0,0 +1,809 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import { + createBrowserRouter, + Navigate, + RouterProvider, +} from 'react-router-dom'; +import { resetStore } from 'store'; + +import GlobalAnnouncementIndex from 'bundles/announcements/GlobalAnnouncementIndex'; +import DashboardPage from 'bundles/common/DashboardPage'; +import AchievementShow from 'bundles/course/achievement/pages/AchievementShow'; +import AchievementsIndex from 'bundles/course/achievement/pages/AchievementsIndex'; +import SettingsNavigation from 'bundles/course/admin/components/SettingsNavigation'; +import AnnouncementSettings from 'bundles/course/admin/pages/AnnouncementsSettings'; +import AssessmentSettings from 'bundles/course/admin/pages/AssessmentSettings'; +import CodaveriSettings from 'bundles/course/admin/pages/CodaveriSettings'; +import CommentsSettings from 'bundles/course/admin/pages/CommentsSettings'; +import ComponentSettings from 'bundles/course/admin/pages/ComponentSettings'; +import CourseSettings from 'bundles/course/admin/pages/CourseSettings'; +import ForumsSettings from 'bundles/course/admin/pages/ForumsSettings'; +import LeaderboardSettings from 'bundles/course/admin/pages/LeaderboardSettings'; +import LessonPlanSettings from 'bundles/course/admin/pages/LessonPlanSettings'; +import MaterialsSettings from 'bundles/course/admin/pages/MaterialsSettings'; +import NotificationSettings from 'bundles/course/admin/pages/NotificationSettings'; +import SidebarSettings from 'bundles/course/admin/pages/SidebarSettings'; +import VideosSettings from 'bundles/course/admin/pages/VideosSettings'; +import AnnouncementsIndex from 'bundles/course/announcements/pages/AnnouncementsIndex'; +import AssessmentEdit from 'bundles/course/assessment/pages/AssessmentEdit'; +import AssessmentMonitoring from 'bundles/course/assessment/pages/AssessmentMonitoring'; +import AssessmentShow from 'bundles/course/assessment/pages/AssessmentShow'; +import AssessmentsIndex from 'bundles/course/assessment/pages/AssessmentsIndex'; +import AssessmentStatisticsPage from 'bundles/course/assessment/pages/AssessmentStatistics'; +import EditForumPostResponsePage from 'bundles/course/assessment/question/forum-post-responses/EditForumPostResponsePage'; +import NewForumPostResponsePage from 'bundles/course/assessment/question/forum-post-responses/NewForumPostResponsePage'; +import EditMcqMrqPage from 'bundles/course/assessment/question/multiple-responses/EditMcqMrqPage'; +import NewMcqMrqPage from 'bundles/course/assessment/question/multiple-responses/NewMcqMrqPage'; +import EditProgrammingQuestionPage from 'bundles/course/assessment/question/programming/EditProgrammingQuestionPage'; +import NewProgrammingQuestionPage from 'bundles/course/assessment/question/programming/NewProgrammingQuestionPage'; +import ScribingQuestion from 'bundles/course/assessment/question/scribing/ScribingQuestion'; +import EditTextResponse from 'bundles/course/assessment/question/text-responses/EditTextResponsePage'; +import NewTextResponse from 'bundles/course/assessment/question/text-responses/NewTextResponsePage'; +import EditVoicePage from 'bundles/course/assessment/question/voice-responses/EditVoicePage'; +import NewVoicePage from 'bundles/course/assessment/question/voice-responses/NewVoicePage'; +import AssessmentSessionNew from 'bundles/course/assessment/sessions/pages/AssessmentSessionNew'; +import SkillsIndex from 'bundles/course/assessment/skills/pages/SkillsIndex'; +import LogsIndex from 'bundles/course/assessment/submission/pages/LogsIndex'; +import SubmissionEditIndex from 'bundles/course/assessment/submission/pages/SubmissionEditIndex'; +import AssessmentSubmissionsIndex from 'bundles/course/assessment/submission/pages/SubmissionsIndex'; +import SubmissionsIndex from 'bundles/course/assessment/submissions/SubmissionsIndex'; +import CourseShow from 'bundles/course/courses/pages/CourseShow'; +import CommentIndex from 'bundles/course/discussion/topics/pages/CommentIndex'; +import Duplication from 'bundles/course/duplication/pages/Duplication'; +import UserRequests from 'bundles/course/enrol-requests/pages/UserRequests'; +import DisbursementIndex from 'bundles/course/experience-points/disbursement/pages/DisbursementIndex'; +import ForumShow from 'bundles/course/forum/pages/ForumShow'; +import ForumsIndex from 'bundles/course/forum/pages/ForumsIndex'; +import ForumTopicShow from 'bundles/course/forum/pages/ForumTopicShow'; +import GroupIndex from 'bundles/course/group/pages/GroupIndex'; +import GroupShow from 'bundles/course/group/pages/GroupShow'; +import LeaderboardIndex from 'bundles/course/leaderboard/pages/LeaderboardIndex'; +import LearningMap from 'bundles/course/learning-map/containers/LearningMap'; +import LessonPlanLayout from 'bundles/course/lesson-plan/containers/LessonPlanLayout'; +import LessonPlanEdit from 'bundles/course/lesson-plan/pages/LessonPlanEdit'; +import LessonPlanShow from 'bundles/course/lesson-plan/pages/LessonPlanShow'; +import LevelsIndex from 'bundles/course/level/pages/LevelsIndex'; +import FolderShow from 'bundles/course/material/folders/pages/FolderShow'; +import TimelineDesigner from 'bundles/course/reference-timelines/TimelineDesigner'; +import StatisticsIndex from 'bundles/course/statistics/pages/StatisticsIndex'; +import ResponseEdit from 'bundles/course/survey/pages/ResponseEdit'; +import ResponseIndex from 'bundles/course/survey/pages/ResponseIndex'; +import ResponseShow from 'bundles/course/survey/pages/ResponseShow'; +import SurveyIndex from 'bundles/course/survey/pages/SurveyIndex'; +import SurveyResults from 'bundles/course/survey/pages/SurveyResults'; +import SurveyShow from 'bundles/course/survey/pages/SurveyShow'; +import UserEmailSubscriptions from 'bundles/course/user-email-subscriptions/UserEmailSubscriptions'; +import InvitationsIndex from 'bundles/course/user-invitations/pages/InvitationsIndex'; +import InviteUsers from 'bundles/course/user-invitations/pages/InviteUsers'; +import ExperiencePointsRecords from 'bundles/course/users/pages/ExperiencePointsRecords'; +import ManageStaff from 'bundles/course/users/pages/ManageStaff'; +import ManageStudents from 'bundles/course/users/pages/ManageStudents'; +import PersonalTimes from 'bundles/course/users/pages/PersonalTimes'; +import PersonalTimesShow from 'bundles/course/users/pages/PersonalTimesShow'; +import CourseUserShow from 'bundles/course/users/pages/UserShow'; +import UsersIndex from 'bundles/course/users/pages/UsersIndex'; +import VideoShow from 'bundles/course/video/pages/VideoShow'; +import VideosIndex from 'bundles/course/video/pages/VideosIndex'; +import VideoSubmissionEdit from 'bundles/course/video/submission/pages/VideoSubmissionEdit'; +import VideoSubmissionShow from 'bundles/course/video/submission/pages/VideoSubmissionShow'; +import VideoSubmissionsIndex from 'bundles/course/video/submission/pages/VideoSubmissionsIndex'; +import UserVideoSubmissionsIndex from 'bundles/course/video-submissions/pages/UserVideoSubmissionsIndex'; +import AdminNavigator from 'bundles/system/admin/admin/AdminNavigator'; +import AnnouncementIndex from 'bundles/system/admin/admin/pages/AnnouncementsIndex'; +import CourseIndex from 'bundles/system/admin/admin/pages/CoursesIndex'; +import InstancesIndex from 'bundles/system/admin/admin/pages/InstancesIndex'; +import UserIndex from 'bundles/system/admin/admin/pages/UsersIndex'; +import InstanceAdminNavigator from 'bundles/system/admin/instance/instance/InstanceAdminNavigator'; +import InstanceAnnouncementsIndex from 'bundles/system/admin/instance/instance/pages/InstanceAnnouncementsIndex'; +import InstanceComponentsIndex from 'bundles/system/admin/instance/instance/pages/InstanceComponentsIndex'; +import InstanceCoursesIndex from 'bundles/system/admin/instance/instance/pages/InstanceCoursesIndex'; +import InstanceUserRoleRequestsIndex from 'bundles/system/admin/instance/instance/pages/InstanceUserRoleRequestsIndex'; +import InstanceUsersIndex from 'bundles/system/admin/instance/instance/pages/InstanceUsersIndex'; +import InstanceUsersInvitations from 'bundles/system/admin/instance/instance/pages/InstanceUsersInvitations'; +import InstanceUsersInvite from 'bundles/system/admin/instance/instance/pages/InstanceUsersInvite'; +import AccountSettings from 'bundles/user/AccountSettings'; +import { + masqueradeLoader, + stopMasqueradeLoader, +} from 'bundles/users/masqueradeLoader'; +import UserShow from 'bundles/users/pages/UserShow'; +import { achievementHandle } from 'course/achievement/handles'; +import assessmentAttemptLoader from 'course/assessment/attemptLoader'; +import { + assessmentHandle, + assessmentsHandle, + questionHandle, +} from 'course/assessment/handles'; +import QuestionFormOutlet from 'course/assessment/question/components/QuestionFormOutlet'; +import { CourseContainer } from 'course/container'; +import { forumHandle, forumTopicHandle } from 'course/forum/handles'; +import { folderHandle } from 'course/material/folders/handles'; +import { videoWatchHistoryHandle } from 'course/statistics/handles'; +import { surveyHandle, surveyResponseHandle } from 'course/survey/handles'; +import { + courseUserHandle, + courseUserPersonalizedTimelineHandle, + manageUserHandles, +} from 'course/users/handles'; +import videoAttemptLoader from 'course/video/attemptLoader'; +import { videoHandle, videosHandle } from 'course/video/handles'; +import CourselessContainer from 'lib/containers/CourselessContainer'; + +import { reservedRoutes } from './redirects'; +import createAppRouter from './router'; + +const authenticatedRouter = createAppRouter([ + { + path: 'courses/:courseId', + element: , + loader: CourseContainer.loader, + handle: CourseContainer.handle, + shouldRevalidate: ({ currentParams, nextParams }): boolean => { + const isChangingCourse = currentParams.courseId !== nextParams.courseId; + + // React Router's documentation never strictly mentioned that `shouldRevalidate` + // should be a pure function, but a good software engineer would probably expect + // it to be. Until we multi-course support in our Redux store, this is where + // we can detect the `courseId` is changing without janky `useEffect`. It should + // be safe since `resetStore` does not interfere with rendering or routing. + if (isChangingCourse) resetStore(); + + return isChangingCourse; + }, + children: [ + { + index: true, + element: , + }, + { + path: 'timelines', + handle: TimelineDesigner.handle, + element: , + }, + { + path: 'announcements', + handle: AnnouncementsIndex.handle, + element: , + }, + { + path: 'comments', + handle: CommentIndex.handle, + element: , + }, + { + path: 'leaderboard', + handle: LeaderboardIndex.handle, + element: , + }, + { + path: 'learning_map', + handle: LearningMap.handle, + element: , + }, + { + path: 'materials/folders', + handle: folderHandle, + // `:folderId` must be split this way so that `folderHandle` is matched + // to the stable (non-changing) match of `/materials/folders`. This allows + // the crumbs in the Workbin to not disappear when revalidated by the + // Dynamic Nest API's builder. + children: [ + { + path: ':folderId', + element: , + }, + ], + }, + { + path: 'levels', + handle: LevelsIndex.handle, + element: , + }, + { + path: 'statistics', + handle: StatisticsIndex.handle, + element: , + }, + { + path: 'duplication', + handle: Duplication.handle, + element: , + }, + { + path: 'enrol_requests', + handle: manageUserHandles.enrolRequests, + element: , + }, + { + path: 'user_invitations', + handle: manageUserHandles.invitations, + element: , + }, + { + path: 'students', + handle: manageUserHandles.students, + element: , + }, + { + path: 'staff', + handle: manageUserHandles.staff, + element: , + }, + { + path: 'lesson_plan', + // @ts-ignore `connect` throws error when cannot find `store` as direct parent + element: , + handle: LessonPlanLayout.handle, + children: [ + { + index: true, + element: , + }, + { + path: 'edit', + element: , + }, + ], + }, + { + path: 'users', + children: [ + { + index: true, + handle: UsersIndex.handle, + element: , + }, + { + path: 'personal_times', + handle: manageUserHandles.personalizedTimelines, + element: , + }, + { + path: 'invite', + handle: manageUserHandles.inviteUsers, + element: , + }, + { + path: 'disburse_experience_points', + handle: DisbursementIndex.handle, + element: , + }, + { + path: ':userId', + handle: courseUserHandle, + children: [ + { + index: true, + element: , + }, + { + path: 'experience_points_records', + handle: ExperiencePointsRecords.handle, + element: , + }, + ], + }, + { + path: ':userId/personal_times', + handle: courseUserPersonalizedTimelineHandle, + element: , + }, + { + path: ':userId/video_submissions', + handle: videoWatchHistoryHandle, + element: , + }, + { + path: ':userId/manage_email_subscription', + handle: UserEmailSubscriptions.handle, + element: , + }, + ], + }, + { + path: 'admin', + loader: SettingsNavigation.loader, + handle: SettingsNavigation.handle, + element: , + children: [ + { + index: true, + element: , + }, + { + path: 'components', + element: , + }, + { + path: 'sidebar', + element: , + }, + { + path: 'notifications', + element: , + }, + { + path: 'announcements', + element: , + }, + { + path: 'assessments', + element: , + }, + { + path: 'materials', + element: , + }, + { + path: 'forums', + element: , + }, + { + path: 'leaderboard', + element: , + }, + { + path: 'comments', + element: , + }, + { + path: 'videos', + element: , + }, + { + path: 'lesson_plan', + element: , + }, + { + path: 'codaveri', + element: , + }, + ], + }, + { + path: 'surveys', + handle: SurveyIndex.handle, + children: [ + { + index: true, + element: , + }, + { + path: ':surveyId', + handle: surveyHandle, + children: [ + { + index: true, + element: , + }, + { + path: 'results', + handle: SurveyResults.handle, + element: , + }, + { + path: 'responses', + children: [ + { + index: true, + handle: ResponseIndex.handle, + element: , + }, + { + path: ':responseId', + children: [ + { + index: true, + handle: surveyResponseHandle, + element: , + }, + { + path: 'edit', + handle: ResponseEdit.handle, + element: , + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + path: 'groups', + element: , + handle: GroupIndex.handle, + children: [ + { + path: ':groupCategoryId', + element: , + }, + ], + }, + { + path: 'videos', + handle: videosHandle, + children: [ + { + index: true, + element: , + }, + { + path: ':videoId', + handle: videoHandle, + children: [ + { + index: true, + element: , + }, + { + path: 'submissions', + children: [ + { + index: true, + handle: VideoSubmissionsIndex.handle, + element: , + }, + { + path: ':submissionId', + handle: VideoSubmissionShow.handle, + children: [ + { + index: true, + element: , + }, + { + path: 'edit', + element: , + }, + ], + }, + ], + }, + { + path: 'attempt', + loader: videoAttemptLoader, + }, + ], + }, + ], + }, + { + path: 'forums', + handle: ForumsIndex.handle, + children: [ + { + index: true, + element: , + }, + { + path: ':forumId', + handle: forumHandle, + children: [ + { + index: true, + element: , + }, + { + path: 'topics/:topicId', + handle: forumTopicHandle, + element: , + }, + ], + }, + ], + }, + { + path: 'achievements', + handle: AchievementsIndex.handle, + children: [ + { + index: true, + element: , + }, + { + path: ':achievementId', + handle: achievementHandle, + element: , + }, + ], + }, + { + path: 'assessments', + handle: assessmentsHandle, + children: [ + { + index: true, + element: , + }, + { + path: 'submissions', + handle: SubmissionsIndex.handle, + element: , + }, + { + path: 'skills', + handle: SkillsIndex.handle, + element: , + }, + { + path: ':assessmentId', + handle: assessmentHandle, + children: [ + { + index: true, + element: , + }, + { + path: 'edit', + handle: AssessmentEdit.handle, + element: , + }, + { + path: 'attempt', + loader: assessmentAttemptLoader, + }, + { + path: 'monitoring', + handle: AssessmentMonitoring.handle, + element: , + }, + { + path: 'sessions/new', + element: , + }, + { + path: 'statistics', + handle: AssessmentStatisticsPage.handle, + // @ts-ignore `connect` throws error when cannot find `store` as direct parent + element: , + }, + { + path: 'submissions', + children: [ + { + index: true, + handle: AssessmentSubmissionsIndex.handle, + element: , + }, + { + path: ':submissionId', + children: [ + { + path: 'edit', + handle: SubmissionEditIndex.handle, + element: , + }, + { + path: 'logs', + handle: LogsIndex.handle, + element: , + }, + ], + }, + ], + }, + { + path: 'question', + element: , + handle: questionHandle, + children: [ + { + path: 'forum_post_responses', + children: [ + { + path: 'new', + handle: NewForumPostResponsePage.handle, + element: , + }, + { + path: ':questionId/edit', + element: , + }, + ], + }, + { + path: 'text_responses', + children: [ + { + path: 'new', + handle: NewTextResponse.handle, + element: , + }, + { + path: ':questionId/edit', + element: , + }, + ], + }, + { + path: 'voice_responses', + children: [ + { + path: 'new', + handle: NewVoicePage.handle, + element: , + }, + { + path: ':questionId/edit', + element: , + }, + ], + }, + { + path: 'multiple_responses', + children: [ + { + path: 'new', + handle: NewMcqMrqPage.handle, + element: , + }, + { + path: ':questionId/edit', + element: , + }, + ], + }, + { + path: 'scribing', + children: [ + { + path: 'new', + handle: ScribingQuestion.handle, + element: , + }, + { + path: ':questionId/edit', + element: , + }, + ], + }, + { + path: 'programming', + children: [ + { + path: 'new', + handle: NewProgrammingQuestionPage.handle, + element: , + }, + { + path: ':questionId/edit', + element: , + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + path: '/', + element: , + children: [ + { + index: true, + element: , + }, + ], + }, + { + path: '*', + element: , + children: [ + reservedRoutes, + { + path: 'admin', + handle: AdminNavigator.handle, + element: , + children: [ + { + index: true, + element: , + }, + { + path: 'announcements', + element: , + }, + { + path: 'users', + element: , + }, + { + path: 'instances', + element: , + }, + { + path: 'courses', + element: , + }, + ], + }, + { + path: 'admin/instance', + handle: InstanceAdminNavigator.handle, + element: , + children: [ + { + index: true, + element: , + }, + { + path: 'announcements', + element: , + }, + { + path: 'components', + element: , + }, + { + path: 'courses', + element: , + }, + { + path: 'users', + element: , + }, + { + path: 'users/invite', + element: , + }, + { + path: 'user_invitations', + element: , + }, + { + path: 'role_requests', + element: , + }, + ], + }, + { + path: 'announcements', + handle: GlobalAnnouncementIndex.handle, + element: , + }, + { + path: 'users', + children: [ + { + path: 'masquerade', + children: [ + { + index: true, + loader: masqueradeLoader, + }, + { + path: 'back', + loader: stopMasqueradeLoader, + }, + ], + }, + { + path: ':userId', + element: , + }, + ], + }, + { + path: 'user/profile/edit', + handle: AccountSettings.handle, + element: , + }, + { + path: 'role_requests', + element: , + }, + ], + }, +]); + +const AuthenticatedApp = (): JSX.Element => ( + +); + +export default AuthenticatedApp; diff --git a/client/app/routers/UnauthenticatedApp.tsx b/client/app/routers/UnauthenticatedApp.tsx new file mode 100644 index 00000000000..9f087b290f8 --- /dev/null +++ b/client/app/routers/UnauthenticatedApp.tsx @@ -0,0 +1,109 @@ +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; + +import LandingPage from 'bundles/common/LandingPage'; +import ConfirmEmailPage from 'bundles/users/pages/ConfirmEmailPage'; +import ForgotPasswordLandingPage from 'bundles/users/pages/ForgotPasswordLandingPage'; +import ForgotPasswordPage from 'bundles/users/pages/ForgotPasswordPage'; +import ResendConfirmationEmailLandingPage from 'bundles/users/pages/ResendConfirmationEmailLandingPage'; +import ResendConfirmationEmailPage from 'bundles/users/pages/ResendConfirmationEmailPage'; +import ResetPasswordPage from 'bundles/users/pages/ResetPasswordPage'; +import SignInPage from 'bundles/users/pages/SignInPage'; +import SignUpLandingPage from 'bundles/users/pages/SignUpLandingPage'; +import SignUpPage from 'bundles/users/pages/SignUpPage'; +import AuthPagesContainer from 'lib/containers/AuthPagesContainer'; +import CourselessContainer from 'lib/containers/CourselessContainer'; + +import { protectedRoutes } from './redirects'; +import createAppRouter from './router'; + +const unauthenticatedRouter = createAppRouter([ + protectedRoutes, + { + path: '*', + element: , + children: [ + { + index: true, + element: , + }, + { + path: 'users', + element: , + children: [ + { + path: 'sign_in', + element: , + }, + { + path: 'sign_up', + children: [ + { + index: true, + loader: SignUpPage.loader, + element: , + }, + { + path: 'completed', + element: , + }, + ], + }, + { + path: 'confirmation', + children: [ + { + index: true, + loader: ConfirmEmailPage.loader, + element: , + errorElement: , + }, + { + path: 'new', + children: [ + { + index: true, + element: , + }, + { + path: 'completed', + element: , + }, + ], + }, + ], + }, + { + path: 'password', + children: [ + { + path: 'new', + children: [ + { + index: true, + element: , + }, + { + path: 'completed', + element: , + }, + ], + }, + { + path: 'edit', + loader: ResetPasswordPage.loader, + errorElement: , + element: , + }, + ], + }, + ], + }, + ], + }, +]); + +const UnauthenticatedApp = (): JSX.Element => ( + +); + +export default UnauthenticatedApp; diff --git a/client/app/routers/index.ts b/client/app/routers/index.ts new file mode 100644 index 00000000000..9cd7f5017df --- /dev/null +++ b/client/app/routers/index.ts @@ -0,0 +1 @@ +export { default } from './AuthenticatableApp'; diff --git a/client/app/routers/redirects.tsx b/client/app/routers/redirects.tsx new file mode 100644 index 00000000000..17cc08d6572 --- /dev/null +++ b/client/app/routers/redirects.tsx @@ -0,0 +1,37 @@ +import { RouteObject } from 'react-router-dom'; + +import { Authenticatable, Redirectable } from 'lib/hooks/router/redirect'; + +/** + * Routes that are only available when the app is unauthenticated. + * + * For example, `users/:userId` in `AuthenticatedApp` matches the + * authentication pages' routes when it shouldn't. + */ +export const reservedRoutes: RouteObject = { + path: 'users', + element: , + children: [ + { path: 'sign_in/*' }, + { path: 'sign_up/*' }, + { path: 'confirmation/*' }, + { path: 'password/*' }, + ], +}; + +/** + * Routes that are only available when the app is authenticated. Accessing + * these routes when unauthenticated will trigger a redirectable redirect + * to the sign in page. + */ +export const protectedRoutes: RouteObject = { + path: '*', + element: , + children: [ + { path: 'courses/:courseId/*' }, + { path: 'admin/*' }, + { path: 'announcements' }, + { path: 'users/:userId' }, + { path: 'user/*' }, + ], +}; diff --git a/client/app/routers/router.tsx b/client/app/routers/router.tsx new file mode 100644 index 00000000000..3a1c9703c91 --- /dev/null +++ b/client/app/routers/router.tsx @@ -0,0 +1,66 @@ +import { RouteObject } from 'react-router-dom'; + +import ErrorPage from 'bundles/common/ErrorPage'; +import PrivacyPolicyPage from 'bundles/common/PrivacyPolicyPage'; +import TermsOfServicePage from 'bundles/common/TermsOfServicePage'; +import CoursesIndex from 'bundles/course/courses/pages/CoursesIndex'; +import AppContainer from 'lib/containers/AppContainer'; +import CourselessContainer from 'lib/containers/CourselessContainer'; + +const createAppRouter = (router: RouteObject[]): RouteObject[] => [ + { + path: '/', + element: , + loader: AppContainer.loader, + errorElement: , + shouldRevalidate: (props): boolean => { + const isChangingCourse = + props.currentParams.courseId !== props.nextParams.courseId; + if (isChangingCourse) return true; + + const currentNest = props.currentUrl.pathname.split('/')[1]; + const nextNest = props.nextUrl.pathname.split('/')[1]; + return currentNest !== nextNest; + }, + children: [ + ...router, + { + path: '*', + element: , + children: [ + { + path: 'courses', + handle: CoursesIndex.handle, + element: , + }, + { + path: 'pages', + children: [ + { + path: 'terms_of_service', + handle: TermsOfServicePage.handle, + element: , + }, + { + path: 'privacy_policy', + handle: PrivacyPolicyPage.handle, + element: , + }, + ], + }, + { + path: 'forbidden', + loader: ErrorPage.Forbidden.loader, + element: , + }, + { + path: '*', + element: , + }, + ], + }, + ], + }, +]; + +export default createAppRouter; From 23f7f4874f0df8464cb050e2f7ee62c24bcdcf44 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:09:28 +0800 Subject: [PATCH 071/173] feat: add Privacy Policy page --- .../PrivacyPolicyPage/PrivacyPolicyPage.tsx | 9 +++++ .../common/PrivacyPolicyPage/index.tsx | 24 ++++++++++++ .../PrivacyPolicyPage/privacy-policy.md | 38 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx create mode 100644 client/app/bundles/common/PrivacyPolicyPage/index.tsx create mode 100644 client/app/bundles/common/PrivacyPolicyPage/privacy-policy.md diff --git a/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx b/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx new file mode 100644 index 00000000000..4d655be2330 --- /dev/null +++ b/client/app/bundles/common/PrivacyPolicyPage/PrivacyPolicyPage.tsx @@ -0,0 +1,9 @@ +import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; + +import privacyPolicy from './privacy-policy.md'; + +const PrivacyPolicyPage = (): JSX.Element => ( + +); + +export default PrivacyPolicyPage; diff --git a/client/app/bundles/common/PrivacyPolicyPage/index.tsx b/client/app/bundles/common/PrivacyPolicyPage/index.tsx new file mode 100644 index 00000000000..58ce9822021 --- /dev/null +++ b/client/app/bundles/common/PrivacyPolicyPage/index.tsx @@ -0,0 +1,24 @@ +import { lazy, Suspense } from 'react'; +import { defineMessages } from 'react-intl'; + +const PrivacyPolicyPage = lazy( + () => + import(/* webpackChunkName: "PrivacyPolicyPage" */ './PrivacyPolicyPage'), +); + +const translations = defineMessages({ + privacyPolicy: { + id: 'app.PrivacyPolicyPage.privacyPolicy', + defaultMessage: 'Privacy Policy', + }, +}); + +const SuspensedPrivacyPolicyPage = (): JSX.Element => ( + + + +); + +const handle = translations.privacyPolicy; + +export default Object.assign(SuspensedPrivacyPolicyPage, { handle }); diff --git a/client/app/bundles/common/PrivacyPolicyPage/privacy-policy.md b/client/app/bundles/common/PrivacyPolicyPage/privacy-policy.md new file mode 100644 index 00000000000..9ea3de0ad66 --- /dev/null +++ b/client/app/bundles/common/PrivacyPolicyPage/privacy-policy.md @@ -0,0 +1,38 @@ +## Privacy Policy + +Effective 24 May 2022. + +This privacy policy sets out how Coursemology uses and protects any information that you give Coursemology when you use this website. Coursemology is committed to ensuring that your privacy is protected. Should we ask you to provide certain information by which you can be identified when using this website, then you can be assured that it will only be used in accordance with this privacy statement. Coursemology may change this policy from time to time by updating this page. You should check this page from time to time to ensure that you are happy with any changes. + +### What we collect + +We may collect the following information: + +- Name +- Contact information including email address +- IP address + +### What we do with the information we gather + +We require this information to understand your needs and provide you with a better service, and in particular for the following reasons: + +- Internal record keeping. +- We may use the information to improve our services. +- We may send emails about new courses, notification of your enrolled courses. +- From time to time, we may also use your information to contact you for research purposes. We may contact you by email. We may use the information to customise the website according to your interests. + +### Security + +We are committed to ensuring that your information is secure. In order to prevent unauthorised access or disclosure we have put in place suitable physical, electronic and managerial procedures to safeguard and secure the information we collect online. + +### How we use cookies + +A cookie is a small file which asks permission to be placed on your computer’s hard drive. Once you agree, the file is added and the cookie helps analyse web traffic or lets you know when you visit a particular site. + +Cookies allow web applications to respond to you as an individual. The web application can tailor its operations to your needs, likes and dislikes by gathering and remembering information about your preferences. We use traffic log cookies to identify which pages are being used. This helps us analyse data about webpage traffic and improve our website in order to tailor it to customer needs. + +We only use this information for statistical analysis purposes and then the data is removed from the system. Overall, cookies help us provide you with a better website, by enabling us to monitor which pages you find useful and which you do not. A cookie in no way gives us access to your computer or any information about you, other than the data you choose to share with us. You can choose to accept or decline cookies. Most web browsers automatically accept cookies, but you can usually modify your browser setting to decline cookies if you prefer. This may prevent you from taking full advantage of the website. + +### Controlling your personal information + +We will not sell, distribute or lease your personal information to third parties. From 87cac39135bbaa787f99b5d39e2dde3bdc325774 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:09:40 +0800 Subject: [PATCH 072/173] feat: add Terms of Service page --- .../TermsOfServicePage/TermsOfServicePage.tsx | 9 ++ .../common/TermsOfServicePage/index.tsx | 24 ++++ .../TermsOfServicePage/terms-of-service.md | 114 ++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx create mode 100644 client/app/bundles/common/TermsOfServicePage/index.tsx create mode 100644 client/app/bundles/common/TermsOfServicePage/terms-of-service.md diff --git a/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx b/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx new file mode 100644 index 00000000000..938510a6efb --- /dev/null +++ b/client/app/bundles/common/TermsOfServicePage/TermsOfServicePage.tsx @@ -0,0 +1,9 @@ +import MarkdownPage from 'lib/components/core/layouts/MarkdownPage'; + +import termsOfService from './terms-of-service.md'; + +const TermsOfServicePage = (): JSX.Element => ( + +); + +export default TermsOfServicePage; diff --git a/client/app/bundles/common/TermsOfServicePage/index.tsx b/client/app/bundles/common/TermsOfServicePage/index.tsx new file mode 100644 index 00000000000..594e2c2c111 --- /dev/null +++ b/client/app/bundles/common/TermsOfServicePage/index.tsx @@ -0,0 +1,24 @@ +import { lazy, Suspense } from 'react'; +import { defineMessages } from 'react-intl'; + +const TermsOfServicePage = lazy( + () => + import(/* webpackChunkName: "TermsOfServicePage" */ './TermsOfServicePage'), +); + +const translations = defineMessages({ + termsOfService: { + id: 'app.TermsOfServicePage.termsOfService', + defaultMessage: 'Terms of Service', + }, +}); + +const SuspensedTermsOfServicePage = (): JSX.Element => ( + + + +); + +const handle = translations.termsOfService; + +export default Object.assign(SuspensedTermsOfServicePage, { handle }); diff --git a/client/app/bundles/common/TermsOfServicePage/terms-of-service.md b/client/app/bundles/common/TermsOfServicePage/terms-of-service.md new file mode 100644 index 00000000000..213fefa886e --- /dev/null +++ b/client/app/bundles/common/TermsOfServicePage/terms-of-service.md @@ -0,0 +1,114 @@ +## Terms of Service + +Effective 12 July 2023. + +**PLEASE READ THIS TERMS OF SERVICE AGREEMENT (THE "TERMS OF SERVICE") CAREFULLY BEFORE ACCESSING OR PARTICIPATING IN ANY CHATROOM, NEWSGROUP, BULLETIN BOARD, MAILING LIST, WEBSITE, TRANSACTION OR OTHER ON-LINE FORUM, COURSE, OR SERVICE MADE AVAILABLE BY Coursemology.org. (“Coursemology") AT ENTRY-POINT URL (http://www.coursemology.org) AND ITS RELATED WEBSITES ("SITE" OR "SITES"). BY USING AND PARTICIPATING IN THE SITES, YOU SIGNIFY AND ACKNOWLEDGE THAT YOU HAVE READ THE TERMS OF SERVICE AND AGREE THAT THE TERMS OF SERVICE CONSTITUTES A BINDING LEGAL AGREEMENT BETWEEN YOU AND Coursemology, AND THAT YOU AGREE TO BE BOUND BY AND COMPLY WITH THE TERMS OF SERVICE. IF YOU DO NOT AGREE TO BE BOUND BY THE TERMS OF SERVICE, PLEASE DO NOT ACCESS THE SITES. THE PARTICIPATING INSTITUTIONS ARE THIRD PARTY BENEFICIARIES OF THE AGREEMENT AND MAY ENFORCE THOSE PROVISIONS BELOW THAT RELATE TO THE PARTICIPATING INSTITUTIONS.** + +### Age Restrictions + +Registration and participation on the Sites is restricted to those individuals over 18 years of age, emancipated minors, or those who possess legal parental or guardian consent, and are fully able and competent to enter into the terms, conditions, obligations, affirmations, representations and warranties herein. By registering or participating in services or functions on the Sites, you hereby represent that you are over 18 years of age, an emancipated minor or in possession of consent by a legal parent or guardian and have the authority to enter into the terms herein. In any case, you affirm that you are over the age of 12 as the Site is not intended for children under 12. If you are under 12 years of age, do not use this site. In addition, those who wish to register and participate must meet the minimum requirements laid out in the Terms of Service (this document) and abide by the Honor Code herein. In addition, certain Courses may have additional eligibility requirements, as specified on the Course website. If you do not qualify or do not agree to these terms, you may not use the Site. + +### Right of Modification + +We reserve the right to change or modify the Terms of Service at our sole discretion at any time. Any change or modification to the Terms of Service will be effective immediately upon posting by us. For any material changes to the Terms, we will take reasonable steps to notify you of such changes. In all cases, your continued use of the Sites after publication of such modifications, with or without notification, constitutes binding acceptance of these modified Terms of Service. + +### Disclaimer + +Sites may include forums containing the personal opinions and other expressions of the persons who post entries on a wide range of topics. Neither the User Content (as defined below) on these Sites, nor any links to other websites, are screened, moderated, approved, reviewed or endorsed by Coursemology or its participating institutions. By posting to or viewing such forums, you agree that Coursemology and any of its participating institutions are not responsible or liable for the content of any postings therein. Coursemology reserves the right (but not the obligation) to remove any content from such forums in its discretion. + +### Rules for Online Conduct + +You agree to use the Sites in accordance with all applicable laws. Further, you agree that you will not use the Site for organized partisan political activities. You further agree that you will not e-mail or post any of the following content (“Prohibited Content”) anywhere on the Site, or on any other Coursemology computing resources: + +- Content that defames, harasses or threatens others +- Content that discusses illegal activities with the intent to commit such activities, or encourages others to commit such activities +- Content that infringes or misappropriates another's intellectual property rights, including, but not limited to, copyrights, trademarks or trade secrets +- Content that you do not have the right to disclose under contractual confidentiality obligations or fiduciary duties +- Material that contains obscene (i.e., pornographic) language or images +- Advertising, promotional materials, or any form of commercial solicitation +- Content that otherwise harms other users or visitors to the Sites +- Content that is otherwise unlawful or that violates any applicable local, state, national or international law. +- Content that probes, scans, or tests the vulnerability of any system or network +- Content that breaches or otherwise circumvents any security measures +- Content that interferes with or disrupts any user, host, or network, for example by sending a virus, overloading, flooding, spamming, or mail-bombing any other user or part of the Sites +- Content that plants malware or otherwise uses the Sites to distribute malware + +Although Coursemology does not routinely screen or monitor content posted by users to the Site, Coursemology reserves the right to remove Prohibited Content of which it becomes aware, but is under no obligation to do so. + +Copyrighted material, including without limitation software, graphics, text, photographs, sound, video and musical recordings, may not be placed on the Site without the express permission of the owner of the copyright in the material, or other legal entitlement to use the material. + +In addition, as a condition of accessing the Sites, you agree not to (a) reproduce, duplicate, copy, sell, resell or exploit any portion of the Sites other than as expressly allowed under these Terms of Service; (b) use Coursemology’s or any Participating Institution's name, trademarks, server or other materials in connection with, or to transmit, any unsolicited communications or emails; (c) use any high-volume, automated or electronic means to access the Sites (including without limitation, robots, spiders, scripts or web-scraping tools); (d) frame the Sites, place pop-up windows over its pages or otherwise affect the display of its page; or (e) interfere with or disrupt the Sites or servers or networks connected to the Sites, or disobey any requirements, procedures, policies or regulations of networks connected to the Sites. + +Finally, you agree that you will not access or attempt to access any other user's account, or misrepresent or attempt to misrepresent your identity while using the Sites. + +### User Accounts + +In order to fully participate in all Site activities, you must register for a personal account on the Site (a “User Account”) by providing an email address and a password for your User Account. You agree that you will never divulge or share access or access information to your User Account with any third party for any reason. You also agree to that you will create, use, and access only one User Account, and that you will not access the Site using multiple User Accounts. + +In setting up your User Account, you may be prompted or required to enter additional information, including but not limited to your name and location. Additional information may be required to confirm your identity. You represent that all information provided by you is accurate, current and complete and you agree that you will maintain and update your information to keep it accurate, current and complete. You acknowledge that if any information provided by you is untrue, inaccurate, not current or incomplete, we reserve the right to terminate your use of the Sites. + +### Privacy Policy + +You understand that any personal information you submit to Coursemology on the Sites will be treated by Coursemology in the manner described in the Privacy Policy. + +### Online Course and Certifications + +The Sites will, from time to time, offer online courses in a specific area of study or on a particular topic (an “Online Course”). Coursemology and the instructors of the Online Courses reserve the right to cancel, interrupt or reschedule any Online Course or modify its content as well as the point value or weight of any assignment, exam or other evaluation of progress. Online Courses offered are subject to the Disclaimer of Warranties / Limitation of Liabilities section below. + +For some courses, subject to your satisfactory performance in the Online Course as determined in the sole discretion of the instructors and the Participating Institutions, you may be awarded experience points acknowledging your completion of class components ("EXP"). This EXP, if provided to you, would be from Coursemology and/or from the instructors. You acknowledge that this EXP, if provided to you, may not be affiliated with Coursemology or any college or university. Further, Coursemology offers the right to offer or not offer any such EXP for a class or course component. You acknowledge that EXP, and Coursemology’s Online Courses, will not stand in the place of a course taken at an accredited institution, and do not convey academic credit. You acknowledge that neither the instructors of any Online Course nor the associated Participating Institutions will be involved in any attempts to get the course recognized by any educational or accredited institution, unless explicitly stated otherwise by Coursemology. The format of awarding EXP will be determined at the discretion of Coursemology and the instructors, and may vary by class in terms of formatting, e.g., whether or not it reports your detailed levels or EXP in the class, and in other ways. + +You may not take any Online Course offered by Coursemology or use any EXP as part of any tuition-based or for-credit certification or program for any college, university, or other academic institution without the express written permission from Coursemology. Such use of an Online Course or EXP is a violation of these Terms of Service. + +### Permission to Use Materials + +All content or other materials available on the Sites, including but not limited to code, images, text, layouts, arrangements, displays, illustrations, audio and video clips, HTML files and other content are the property of Coursemology and/or its affiliates or licensors and are protected by copyright, patent and/or other proprietary intellectual property rights under the Singapore and foreign laws. In consideration for your agreement to the terms and conditions contained here, Coursemology grants you a personal, non-exclusive, non-transferable license to access and use the Sites. You may download material from the Sites only for your own personal, non-commercial use. You may not otherwise copy, reproduce, retransmit, distribute, publish, commercially exploit or otherwise transfer any material, nor may you modify or create derivatives works of the material. The burden of determining that your use of any information, software or any other content on the Site is permissible rests with you. + +In connection with your participation in an Online Course, you will have the ability to access or download content or other course-related materials provided by other Users taking the course. While Coursemology requires Users to comply with the Coursemology Terms of Service in providing User Content, Coursemology cannot guarantee that any such User Content will be free of viruses, worms, back doors, Trojan horses or other contaminants which may harm your computer, tablet, hand-held device or any programs or files therein. Coursemology disclaims any responsibility or liability relating to your access or download of such User Content. Accordingly, Coursemology recommends that you only download or access files from a trusted source and implement security measures to scan downloaded files for contaminants. + +### User Material Submission + +The Sites may provide you with the ability to upload certain information, text, or materials, including without limitation, any information, text or materials you post on the Sites’ public forums such as the wiki or the discussion forums (“User Content”). With respect to User Content you submit or otherwise make available in connection with your use of the Site, and subject to the Privacy Policy, you grant Coursemology and the Participating Institutions a fully transferable, worldwide, perpetual, royalty-free and non-exclusive license to use, distribute, sublicense, reproduce, modify, adapt, publicly perform and publicly display such User Content. To the extent that you provide User Content, you represent and warrant to Coursemology and the Participating Institutions that (a) you have all necessary rights, licenses and/or clearances to provide and use User Content and permit Coursemology and the Participating Institutions to use such User Content as provided above; (b) such User Content is accurate and reasonably complete; (c) as between you and Coursemology, you shall be responsible for the payment of any third party fees related to the provision and use of such User Content and (d) such User Content does not and will not infringe or misappropriate any third party rights (including without limitation privacy, publicity, intellectual property and any other proprietary rights, such as copyright, trademark and patent rights) or constitute a fraudulent statement or misrepresentation or unfair business practice. + +The Sites may also provide you with ability to upload or send information to Coursemology regarding the Sites or related services (“Feedback”). By submitting the Feedback, you hereby grant Coursemology and the Participating Institutions an irrevocable license to use, disclose, reproduce, distribute, sublicense, prepare derivative works of, publicly perform and publicly display any such submission. + +### Links to Other Sites + +The Sites may include hyperlinks to sites maintained or controlled by others. Neither Coursemology nor the Participating Institutions are responsible for nor do they routinely screen, approve, review or endorse the contents of or use of any of the products or services that may be offered at these sites. + +### Online Education and Gamification Research + +Records of your participation in Online Courses may be used for researching online education and/or gamification. In the interests of this research, you may be exposed to slight variations in the course materials that will not substantially alter your learning experience. All research findings will be reported at the aggregate level and will not expose your personal identity. + +### Choice of Law/Forum Selection + +Sites are managed by Coursemology, located in Singapore. You agree that any dispute arising out of or relating to these Terms of Service or any content posted to a Site, including copies and republication thereof, whether based in contract, tort, statutory or other law, will be governed by the constitution of the Republic of Singapore. You further consent to the personal jurisdiction of and exclusive venue in the supreme and high courts located in and serving the Republic of Singapore as the legal forum for any such dispute. +Excluding claims for injunctive or other equitable relief, for claims related to the Coursemology Sites where the total amount sought is less than ten thousand Singapore Dollars ($10,000.00 SGD), either Coursemology or You may elect at any point during the dispute to resolve the claim through binding, non-appearance-based arbitration. The dispute will then be resolved using an established alternative dispute resolution ("ADR") provider, mutually agreed upon by You and Coursemology. The parties and the selected ADR provider shall not involve any personal appearance by the parties or witnesses, unless otherwise mutually agreed by the parties; rather, the arbitration shall be conducted, at the option of the party seeking relief, online, by telephone, online, or via written submissions alone. Any judgment rendered by the arbitrator may be entered in any court of competent jurisdiction. + +### Disclaimer of Warranty / Limitation of Liabilities + +**THE SITES AND ANY INFORMATION, PRODUCTS OR SERVICES THEREIN ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. COURSEMOLOGY AND ITS PARTICIPATING INSTITUTIONS, THEIR INSTRUCTORS AND THEIR STAFF (THE “COURSEMOLOGY PARTIES”) DO NOT WARRANT, AND HEREBY DISCLAIM ANY WARRANTIES, EITHER EXPRESS OR IMPLIED, WITH RESPECT TO THE ACCURACY, ADEQUACY OR COMPLETENESS OF ANY ONLINE COURSE, SITE, INFORMATION OBTAINED FROM A SITE, OR LINK TO A SITE. THE COURSEMOLOGY PARTIES DO NOT WARRANT THAT SITES WILL OPERATE IN AN UNINTERRUPTED OR ERROR-FREE MANNER OR THAT SITES ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. WITHOUT LIMITING THE FOREGOING, THE COURSEMOLOGY PARTIES DO NOT WARRANT THAT (A) THE ONLINE COURSES OR SITES WILL MEET YOUR REQUIREMENTS OR EXPECTATIONS OR ACHIEVE THE INTENDED PURPOSES, (B) THE ONLINE COURSES OR SITES WILL NOT EXPERIENCE OUTAGES OR OTHERWISE BE UNINTERRUPTED, TIMELY, SECURE OR ERROR-FREE, (C) THE INFORMATION OR SERVICES OBTAINED THROUGH OR FROM THE ONLINE COURSES OR SITES WILL BE ACCURATE, COMPLETE, CURRENT, ERROR-FREE, COMPLETELY SECURE OR RELIABLE, OR (D) THAT DEFECTS IN OR ON THE ONLINE COURSES OR SITES WILL BE CORRECTED. NONE OF THE COURSEMOLOGY PARTIES MAKE ANY REPRESENTATION REGARDING YOUR ABILITY TO TRANSMIT AND RECEIVE INFORMATION FROM OR THROUGH THE SITES, AND YOU AGREE AND ACKNOWLEDGE THAT YOUR ABILITY TO ACCESS THE ONLINE COURSES AND SITES MAY BE IMPAIRED. THE COURSEMOLOGY PARTIES DISCLAIM ANY AND ALL LIABILITY RESULTING FROM OR RELATED TO SUCH EVENTS OR THE ACCESS OR USE OF THE ONLINE COURSES OR SITES OR ANY INFORMATION OR SERVICES RELATED TO THEM. YOU ACKNOWLEDGE AND AGREE THAT ANY ACCESS TO OR USE OF THE ONLINE COURSES AND SITES OR SUCH INFORMATION OR SERVICES IS AT YOUR OWN RISK. EXCEPT AS PROHIBITED BY LAW, YOU AGREE THAT THE COURSEMOLOGY PARTIES WILL NOT BE LIABLE TO YOU FOR ANY LOSS OR DAMAGES ARISING OUT OF OR RELATING TO THESE TERMS OF SERVICE OR YOUR (OR ANY THIRD PARTY'S) USE OR INABILITY TO USE AN ONLINE COURSE, SITE, DATA LOSS, YOUR PLACEMENT OF CONTENT ON A SITE, YOUR RELIANCE UPON INFORMATION OBTAINED FROM OR THROUGH AN ONLINE COURSE OR SITE, OR ANY OTHER POTENTIAL CLAIMS RELATED TO THE ONLINE COURSES OR SITES. EXCEPT AS PROHIBITED BY LAW, THE COURSEMOLOGY PARTIES WILL NOT HAVE LIABILITY FOR ANY CONSEQUENTIAL, INDIRECT, PUNITIVE, SPECIAL OR INCIDENTAL DAMAGES, WHETHER FORESEEABLE OR UNFORESEEABLE, (INCLUDING, BUT NOT LIMITED TO, CLAIMS FOR DEFAMATION, ERRORS, LOSS OF DATA, OR INTERRUPTION IN AVAILABILITY OF DATA), ARISING OUT OF OR RELATING TO THESE TERMS OF SERVICE, YOUR USE OR INABILITY TO USE ANY ONLINE COURSE OR SITE, DATA LOSS, ANY PURCHASES ON THIS SITE, YOUR PLACEMENT OF CONTENT ON A SITE, OR YOUR RELIANCE UPON INFORMATION OBTAINED FROM OR THROUGH ANY ONLINE COURSE OR SITE, WHETHER BASED IN CONTRACT, TORT, STATUTORY OR OTHER LAW, EXCEPT ONLY IN THE CASE OF DEATH OR PERSONAL INJURY WHERE AND ONLY TO THE EXTENT THAT APPLICABLE LAW REQUIRES SUCH LIABILITY. COURSEMOLOGY'S TOTAL CUMULATIVE LIABILITY ARISING OUT OF OR RELATED TO THE USER'S USE OF THE COURSEMOLOGY SITES WILL NOT EXCEED TWENTY U.S. DOLLARS ($20) OR THE TOTAL AMOUNT OF FEES RECEIVED BY COURSEMOLOGY FROM THE USER FOR THE USE OF THE COURSEMOLOGY SITES DURING THE PAST 12 MONTHS OF USE, WHICHEVER IS GREATER. YOU ACKNOWLEDGE AND AGREE THAT THE WARRANTY DISCLAIMERS AND THE LIMITATIONS OF LIABILITY SET FORTH IN THIS TERMS OF SERVICE REFLECT A REASONABLE AND FAIR ALLOCATION OF RISK BETWEEN YOU AND THE COURSEMOLOGY PARTIES, AND THAT THESE LIMITATIONS ARE AN ESSENTIAL BASIS TO COURSEMOLOGY'S ABILITY TO MAKE THE COURSEMOLOGY SITES AVAILABLE TO YOU ON AN ECONOMICALLY FEASIBLE BASIS. YOU AGREE THAT ANY CAUSE OF ACTION RELATED TO THE COURSEMOLOGY SITES MUST COMMENCE WITHIN ONE (1) YEAR AFTER THE CAUSE OF ACTION ACCRUES. OTHERWISE, SUCH CAUSE OF ACTION IS PERMANENTLY BARRED.** + +### Copyright Policy + +The Copyright Act (the “CA”) provides recourse for copyright owners who believe that material appearing on the Internet infringes their rights under Singapore copyright law. +If you believe in good faith that materials on the Coursemology Sites infringe your copyright, you (or your agent) may send us a notice requesting that the material be removed, or access to it blocked. +The notice must include the following information: (a) a physical or electronic signature of a person authorized to act on behalf of the owner of an exclusive right that is allegedly infringed; (b) identification of the copyrighted work claimed to have been infringed (or if multiple copyrighted works located on the Site are covered by a single notification, a representative list of such works); (c) identification of the material that is claimed to be infringing or the subject of infringing activity, and information reasonably sufficient to allow Coursemology to locate the material on the Site; (d) the name, address, telephone number, and email address (if available) of the complaining party; (e) a statement that the complaining party has a good faith belief that use of the material in the manner complained of is not authorized by the copyright owner, its agent, or the law; and (f) a statement that the information in the notification is accurate and, under penalty of perjury, that the complaining party is authorized to act on behalf of the owner of an exclusive right that is allegedly infringed. +Notices must meet the then-current statutory requirements imposed by the DMCA; see [http://www.loc.gov/copyright](http://www.loc.gov/copyright) for details. Notices and counter-notices with respect to the Site should be sent to [coursemology@gmail.com](mailto:coursemology@gmail.com). +We suggest that you consult your legal advisor before filing a notice. Also, be aware that there can be penalties for false claims under the CA. + +### Indemnification + +You agree to indemnify, defend and hold harmless Coursemology and the Participating Institutions, their respective subsidiaries and affiliates, and each of their respective officers, directors, agents, employees, and assignees, including the instructors of the Participating Institutions, from any and all claims, liabilities, expenses and damages, including reasonable attorneys’ fees and costs, made by any third party relating to or arising out of (a) your use or attempted use of the Sites or Online Course in violation of the Terms of Service; (b) your violation of any law or rights of any third party, or (c) information that you post or otherwise make available on the Sites or through the Online Course, including without limitation any claim of infringement or misappropriation of intellectual property or other proprietary rights. + +### Termination Rights + +You agree that each of Coursemology and the Participating Institutions, in their sole discretion, may terminate your use of the Site or your participation in it thereof, for any reason or no reason and that none of the Coursemology Parties shall have any liability to you for any such action. You further acknowledge that for the purpose of any Coursemology course your sole relationship with Coursemology and the Participating Institution is as defined in these Terms of Service; for the avoidance of doubt, you do not have student status at any Participating Institution through a Coursemology course and you are not entitled to any grievance or other resolution process for student disputes at any Participating Institution. You further agree that Coursemology has the right to cancel, delay, reschedule or alter the format of any Online Course at any time, and that none of the Coursemology Parties shall have any liability to you for any such action. If you no longer desire to participate in the Site, you may terminate your participation therein upon notice to Coursemology. + +### Honor Code + +All students participating in the class must agree to abide by the following code of conduct: + +- I will register for only one account. +- My answers to homework, quizzes and exams will be my own work (except for assignments that explicitly permit collaboration). +- I will not make solutions to homework, quizzes or exams available to anyone else. This includes both solutions written by me, as well as any official solutions provided by the course staff. +- I will not engage in any other activities that will dishonestly improve my results or dishonestly improve/hurt the results of others. From f08579f5bcbe01f88714b146904a584fa96a7101 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:09:58 +0800 Subject: [PATCH 073/173] feat: add DashboardPage --- client/app/bundles/common/DashboardPage.tsx | 127 ++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 client/app/bundles/common/DashboardPage.tsx diff --git a/client/app/bundles/common/DashboardPage.tsx b/client/app/bundles/common/DashboardPage.tsx new file mode 100644 index 00000000000..b2dc8d586c1 --- /dev/null +++ b/client/app/bundles/common/DashboardPage.tsx @@ -0,0 +1,127 @@ +import { useMemo, useState } from 'react'; +import { defineMessages } from 'react-intl'; +import { Navigate } from 'react-router-dom'; +import { ArrowForward } from '@mui/icons-material'; +import { Avatar, Typography } from '@mui/material'; +import moment from 'moment'; + +import SearchField from 'lib/components/core/fields/SearchField'; +import Page from 'lib/components/core/layouts/Page'; +import Link from 'lib/components/core/Link'; +import { useAppContext } from 'lib/containers/AppContainer'; +import useTranslation from 'lib/hooks/useTranslation'; + +const translations = defineMessages({ + searchCourses: { + id: 'lib.components.navigation.CourseSwitcherPopupMenu.searchCourses', + defaultMessage: 'Search your courses', + }, + jumpBackIn: { + id: 'lib.components.navigation.CourseSwitcherPopupMenu.jumpBackIn', + defaultMessage: 'Jump back in', + }, + lastAccessed: { + id: 'lib.components.navigation.CourseSwitcherPopupMenu.lastAccessed', + defaultMessage: 'Last accessed {at}', + }, + noCoursesMatch: { + id: 'lib.components.navigation.CourseSwitcherPopupMenu.noCoursesMatch', + defaultMessage: "Oops, no courses matched ''{keyword}''.", + }, +}); + +const DashboardPage = (): JSX.Element => { + const { courses } = useAppContext(); + + const { t } = useTranslation(); + + const [filterKeyword, setFilterKeyword] = useState(''); + + const sortedCourses = useMemo(() => { + return courses?.slice().sort((a, b) => { + return moment(b.lastActiveAt).diff(moment(a.lastActiveAt)); + }); + }, [courses]); + + const filteredCourses = useMemo(() => { + if (!filterKeyword) return sortedCourses; + + return sortedCourses?.filter((course) => + course.title.toLowerCase().includes(filterKeyword.toLocaleLowerCase()), + ); + }, [filterKeyword]); + + return ( + + + {t(translations.jumpBackIn)} + + + + + {Boolean(courses?.length) && ( +
+ {filteredCourses?.map((course) => ( + +
+ + +
+ {course.title} + + {course.lastActiveAt && ( + + {t(translations.lastAccessed, { + at: moment(course.lastActiveAt).fromNow(), + })} + + )} +
+
+ + + + ))} + + {!filteredCourses?.length && ( + + {t(translations.noCoursesMatch, { + keyword: filterKeyword, + })} + + )} +
+ )} +
+ ); +}; + +const DashboardPageRedirects = (): JSX.Element => { + const { courses } = useAppContext(); + + if (!courses?.length) return ; + + if (courses?.length === 1) return ; + + return ; +}; + +export default DashboardPageRedirects; From b4e09511cc1247bd3401e6fd4dc49e5fb3ad415a Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:10:07 +0800 Subject: [PATCH 074/173] feat: add LandingPage --- client/app/bundles/common/LandingPage.tsx | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 client/app/bundles/common/LandingPage.tsx diff --git a/client/app/bundles/common/LandingPage.tsx b/client/app/bundles/common/LandingPage.tsx new file mode 100644 index 00000000000..3e664d724e3 --- /dev/null +++ b/client/app/bundles/common/LandingPage.tsx @@ -0,0 +1,67 @@ +import { defineMessages } from 'react-intl'; +import { Button, Typography } from '@mui/material'; + +import Page from 'lib/components/core/layouts/Page'; +import Link from 'lib/components/core/Link'; +import useTranslation from 'lib/hooks/useTranslation'; + +const translations = defineMessages({ + signInToCoursemology: { + id: 'landing_page.sign_in_to_coursemology', + defaultMessage: 'Sign in to Coursemology', + }, + createAnAccount: { + id: 'landing_page.create_an_account', + defaultMessage: 'Create an account', + }, + newToCoursemology: { + id: 'landing_page.new_to_coursemology', + defaultMessage: 'New to Coursemology?', + }, + title: { + id: 'landing_page.title', + defaultMessage: 'Making your class a world of games in a universe of fun.', + }, + subtitle: { + id: 'landing_page.subtitle', + defaultMessage: + 'Coursemology adds fun elements, such as experience points, levels, and achievements to your classroom. ' + + 'These gamification elements motivate students to power through lessons and their assignments.', + }, +}); + +const LandingPage = (): JSX.Element => { + const { t } = useTranslation(); + + return ( + + + {t(translations.title)} + + + + {t(translations.subtitle)} + + + + + + + + {t(translations.newToCoursemology)} + + + + + + + ); +}; + +export default LandingPage; From ba1d5470a7933d749ea3713713a59659938d7799 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:10:37 +0800 Subject: [PATCH 075/173] feat: add 403, 404 error pages --- client/app/assets/error-illustration.svg | 297 ++++++ client/app/assets/forbidden-illustration.svg | 943 +++++++++++++++++++ client/app/assets/not-found-illustration.svg | 453 +++++++++ client/app/bundles/common/ErrorPage.tsx | 216 +++++ 4 files changed, 1909 insertions(+) create mode 100644 client/app/assets/error-illustration.svg create mode 100644 client/app/assets/forbidden-illustration.svg create mode 100644 client/app/assets/not-found-illustration.svg create mode 100644 client/app/bundles/common/ErrorPage.tsx diff --git a/client/app/assets/error-illustration.svg b/client/app/assets/error-illustration.svg new file mode 100644 index 00000000000..4de41f744fa --- /dev/null +++ b/client/app/assets/error-illustration.svg @@ -0,0 +1,297 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/app/assets/forbidden-illustration.svg b/client/app/assets/forbidden-illustration.svg new file mode 100644 index 00000000000..7f44b780b73 --- /dev/null +++ b/client/app/assets/forbidden-illustration.svg @@ -0,0 +1,943 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/app/assets/not-found-illustration.svg b/client/app/assets/not-found-illustration.svg new file mode 100644 index 00000000000..ac77e60572b --- /dev/null +++ b/client/app/assets/not-found-illustration.svg @@ -0,0 +1,453 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/app/bundles/common/ErrorPage.tsx b/client/app/bundles/common/ErrorPage.tsx new file mode 100644 index 00000000000..c1deb69761b --- /dev/null +++ b/client/app/bundles/common/ErrorPage.tsx @@ -0,0 +1,216 @@ +import { ReactNode } from 'react'; +import { defineMessages } from 'react-intl'; +import { LoaderFunction, redirect, useLoaderData } from 'react-router-dom'; +import { Typography } from '@mui/material'; +import forbiddenIllustration from 'assets/forbidden-illustration.svg?url'; +import notFoundIllustration from 'assets/not-found-illustration.svg?url'; + +import Page from 'lib/components/core/layouts/Page'; +import Link from 'lib/components/core/Link'; +import { + Attributions, + useSetAttributions, +} from 'lib/components/wrappers/AttributionsProvider'; +import { getForbiddenSourceURL } from 'lib/hooks/router/redirect'; +import useEffectOnce from 'lib/hooks/useEffectOnce'; +import useTranslation from 'lib/hooks/useTranslation'; + +const translations = defineMessages({ + notFound: { + id: 'app.ErrorPage.notFound', + defaultMessage: "That location doesn't exist in this universe...", + }, + notFoundSubtitle: { + id: 'app.ErrorPage.notFoundSubtitle', + defaultMessage: + "Check if you've typed the correct address, try again later, or go back home.", + }, + notFoundIllustrationAttribution: { + id: 'app.ErrorPage.notFoundIllustrationAttribution', + defaultMessage: + 'Graphic of a dog floating in space is created by Storyset from ' + + 'www.storyset.com, with modifications.', + }, + forbidden: { + id: 'app.ErrorPage.forbidden', + defaultMessage: 'Hold up, this galaxy is off-limits to you!', + }, + forbiddenSubtitle: { + id: 'app.ErrorPage.forbiddenSubtitle', + defaultMessage: + "You don't have permission to access the information behind this page. If you believe this is a mistake, " + + 'contact your administrator.', + }, + forbiddenIllustrationAttribution: { + id: 'app.ErrorPage.forbiddenIllustrationAttribution', + defaultMessage: + 'Graphic of an astronaut floating in space is created by Storyset from ' + + 'www.storyset.com, with modifications. Otherwise, go back.', + }, + error: { + id: 'app.ErrorPage.error', + defaultMessage: 'KABOOM, a meteor has just crashed.', + }, + errorSubtitle: { + id: 'app.ErrorPage.errorSubtitle', + defaultMessage: + 'A fatal error has occurred. You may try again later. If the problem persists, contact us.', + }, + errorIllustrationAttribution1: { + id: 'app.ErrorPage.errorIllustrationAttribution1', + defaultMessage: + 'Graphic of a planet earth in space is created by Storyset from ' + + 'www.storyset.com, with modifications. Otherwise, go back.', + }, + errorIllustrationAttribution2: { + id: 'app.ErrorPage.errorIllustrationAttribution2', + defaultMessage: + 'Graphic of a fire ball is created by Storyset from ' + + 'www.storyset.com, with modifications. Otherwise, go back.', + }, +}); + +interface ErrorPageProps { + illustrationSrc: string; + illustrationAlt: string; + title: ReactNode; + subtitle: ReactNode; + attributions?: Attributions; + tip?: ReactNode | false; + children?: ReactNode; +} + +const ErrorPage = (props: ErrorPageProps): JSX.Element => { + useSetAttributions(props.attributions); + + return ( + + {props.illustrationAlt} + + {props.tip !== false && ( + + {props.tip ?? window.location.pathname} + + )} + + + {props.title} + + + + {props.subtitle} + + + {props.children} + + ); +}; + +const NotFoundPage = (): JSX.Element => { + const { t } = useTranslation(); + + return ( + ( + + {chunk} + + ), + source: (chunk) => ( + + {chunk} + + ), + }), + }, + ]} + illustrationAlt="Not found illustration" + illustrationSrc={notFoundIllustration} + subtitle={t(translations.notFoundSubtitle, { + home: (chunk) => ( + + {chunk} + + ), + })} + title={t(translations.notFound)} + /> + ); +}; + +const ForbiddenPage = (): JSX.Element => { + const { t } = useTranslation(); + + const sourceURL = useLoaderData() as string | null; + + useEffectOnce(() => { + if (sourceURL) window.history.replaceState(null, '', sourceURL); + }); + + return ( + ( + + {chunk} + + ), + source: (chunk) => ( + + {chunk} + + ), + }), + }, + ]} + illustrationAlt="Forbidden illustration" + illustrationSrc={forbiddenIllustration} + subtitle={t(translations.forbiddenSubtitle)} + tip={sourceURL} + title={t(translations.forbidden)} + /> + ); +}; + +const forbiddenPageLoader: LoaderFunction = async ({ request }) => { + const sourceURL = getForbiddenSourceURL(request.url); + if (!sourceURL) return redirect('/'); + + return sourceURL; +}; + +export default { + NotFound: NotFoundPage, + Forbidden: Object.assign(ForbiddenPage, { loader: forbiddenPageLoader }), +}; From 221b4ebb2b41176979c65c2aaad833d0b55069d9 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:11:36 +0800 Subject: [PATCH 076/173] style(AppContainer): remove unused IfRailsSaysSafeToRender --- .../containers/AppContainer/AppContainer.tsx | 5 +- .../AppContainer/IfRailsSaysSafeToRender.tsx | 162 ------------------ 2 files changed, 1 insertion(+), 166 deletions(-) delete mode 100644 client/app/lib/containers/AppContainer/IfRailsSaysSafeToRender.tsx diff --git a/client/app/lib/containers/AppContainer/AppContainer.tsx b/client/app/lib/containers/AppContainer/AppContainer.tsx index 896073ccbc6..d56f61d8cb6 100644 --- a/client/app/lib/containers/AppContainer/AppContainer.tsx +++ b/client/app/lib/containers/AppContainer/AppContainer.tsx @@ -4,7 +4,6 @@ import NotificationPopup from 'lib/containers/NotificationPopup'; import { loader, useAppLoader } from './AppLoader'; import GlobalAnnouncements from './GlobalAnnouncements'; -import IfRailsSaysSafeToRender from './IfRailsSaysSafeToRender'; import MasqueradeBanner from './MasqueradeBanner'; const AppContainer = (): JSX.Element => { @@ -24,9 +23,7 @@ const AppContainer = (): JSX.Element => { )} - - - +
diff --git a/client/app/lib/containers/AppContainer/IfRailsSaysSafeToRender.tsx b/client/app/lib/containers/AppContainer/IfRailsSaysSafeToRender.tsx deleted file mode 100644 index 70c2a1e0d3e..00000000000 --- a/client/app/lib/containers/AppContainer/IfRailsSaysSafeToRender.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { ComponentType, ReactNode, useEffect } from 'react'; -import { defineMessages } from 'react-intl'; -import { - ArrowBack, - ConfirmationNumberOutlined, - DoNotDisturbOnOutlined, - SvgIconComponent, -} from '@mui/icons-material'; -import { Typography } from '@mui/material'; - -import Link from 'lib/components/core/Link'; -import useTranslation from 'lib/hooks/useTranslation'; - -const translations = defineMessages({ - accessDenied: { - id: 'app.containers.AppContainer.AppErrorPage.accessDenied', - defaultMessage: 'Oops, access denied!', - }, - accessDeniedDescription: { - id: 'app.containers.AppContainer.AppErrorPage.accessDeniedDescription', - defaultMessage: 'You are not authorised to access this page.', - }, - goBack: { - id: 'app.containers.AppContainer.AppErrorPage.goBack', - defaultMessage: 'Go back', - }, - sessionExpired: { - id: 'app.containers.AppContainer.AppErrorPage.sessionExpired', - defaultMessage: "You'll need to sign in again.", - }, - sessionExpiredDescription: { - id: 'app.containers.AppContainer.AppErrorPage.sessionExpiredDescription', - defaultMessage: - "Your previous session is expired. We're redirecting you to the sign in page.", - }, - goToSignInPage: { - id: 'app.containers.AppContainer.AppErrorPage.goToSignInPage', - defaultMessage: 'Go to the sign in page now', - }, -}); - -interface MessageBodyProps { - Icon: SvgIconComponent; - iconClassName?: string; - title: string; - description: string; - children?: ReactNode; -} - -const MessageBody = ({ Icon, ...props }: MessageBodyProps): JSX.Element => ( -
- - -
- {props.title} - - {props.description} -
- - {props.children} -
-); - -const ForbiddenMessageBody = (): JSX.Element => { - const { t } = useTranslation(); - - return ( - - { - window.history.back(); - }} - underline="hover" - > - - {t(translations.goBack)} - - - ); -}; - -const SessionExpiredMessageBody = (): JSX.Element => { - const { t } = useTranslation(); - - useEffect(() => { - const redirectTimeout = setTimeout(() => { - window.location.href = '/users/sign_in'; - }, 2000); - - return () => clearTimeout(redirectTimeout); - }, []); - - return ( - - - {t(translations.goToSignInPage)} - - - ); -}; - -const messageBodies: Record = { - 403: ForbiddenMessageBody, - 401: SessionExpiredMessageBody, -}; - -/** - * Returns the HTTP status code from the Rails' router, if found. - * - * This works in unison with `default.slim.html` defining a `meta` tag with the response - * HTTP status code as integer. - */ -const getRailsResponseStatusCode = (): number | undefined => { - const statusMeta = document.querySelector('meta[name="status"]'); - const content = statusMeta?.getAttribute('content'); - if (!content) return undefined; - - return parseInt(content, 10); -}; - -/** - * Renders the `children` if Rails' router says that the page is safe to render. - * - * Since Rails' router still renders parts of the page when we get a 403, the mounted React - * app will still load the React page and make a HTTP request (if needed), and would probably - * fail. This component is a TEMPORARY workaround to prevent the (rest of the) React app from - * rendering if Rails' deem our request NOT to be safe. - * - * "Safe" here means that Rails' HTML render response has an HTTP status code listed in - * `keyof messageBodies`. - */ -const IfRailsSaysSafeToRender = (props: { - children: JSX.Element; -}): JSX.Element => { - const status = getRailsResponseStatusCode(); - - const StatusMessageBody = status && messageBodies[status]; - if (!StatusMessageBody) return props.children; - - return ( -
- -
- ); -}; - -export default IfRailsSaysSafeToRender; From a9f48cbf2c4a1fd00ceea4b77520d163b0445e45 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:13:19 +0800 Subject: [PATCH 077/173] feat(AppLoader): sets i18n configs, authenticates app --- .../lib/components/wrappers/I18nProvider.tsx | 39 +++++++++++++------ .../lib/containers/AppContainer/AppLoader.ts | 31 +++++++++++++-- client/app/lib/moment.ts | 4 -- 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/client/app/lib/components/wrappers/I18nProvider.tsx b/client/app/lib/components/wrappers/I18nProvider.tsx index 4c8f2f0d134..368c9cd044c 100644 --- a/client/app/lib/components/wrappers/I18nProvider.tsx +++ b/client/app/lib/components/wrappers/I18nProvider.tsx @@ -1,7 +1,12 @@ -import { ReactNode } from 'react'; +import { ReactNode, useEffect } from 'react'; import { IntlProvider } from 'react-intl'; -import { i18nLocale } from 'lib/helpers/server-context'; +import { + DEFAULT_LOCALE, + DEFAULT_TIME_ZONE, +} from 'lib/constants/sharedConstants'; +import { useI18nConfig } from 'lib/hooks/session'; +import moment from 'lib/moment'; import translations from '../../../../build/locales/locales.json'; @@ -9,19 +14,31 @@ interface I18nProviderProps { children: ReactNode; } -const I18nProvider = (props: I18nProviderProps): JSX.Element => { - if (!i18nLocale) throw new Error(`Illegal i18nLocale: ${i18nLocale}`); +const getLocaleWithoutRegionCode = (locale: string): string => + locale.toLowerCase().split(/[_-]+/)[0]; + +const getMessages = (locale: string): Record | undefined => { + const localeWithoutRegionCode = getLocaleWithoutRegionCode(locale); - const localeWithoutRegionCode = i18nLocale.toLowerCase().split(/[_-]+/)[0]; + return localeWithoutRegionCode !== DEFAULT_LOCALE + ? translations[localeWithoutRegionCode] || translations[locale] + : undefined; +}; + +const I18nProvider = (props: I18nProviderProps): JSX.Element => { + const { locale, timeZone } = useI18nConfig(); - let messages; - if (localeWithoutRegionCode !== 'en') { - messages = - translations[localeWithoutRegionCode] || translations[i18nLocale]; - } + useEffect(() => { + moment.tz.setDefault(timeZone?.trim() || DEFAULT_TIME_ZONE); + }, [timeZone]); return ( - + {props.children} ); diff --git a/client/app/lib/containers/AppContainer/AppLoader.ts b/client/app/lib/containers/AppContainer/AppLoader.ts index db439035ac2..30947fb7d0f 100644 --- a/client/app/lib/containers/AppContainer/AppLoader.ts +++ b/client/app/lib/containers/AppContainer/AppLoader.ts @@ -1,18 +1,41 @@ import { useLoaderData, useOutletContext } from 'react-router-dom'; +import { AxiosError } from 'axios'; import { AnnouncementMiniEntity } from 'types/course/announcements'; import { HomeLayoutData } from 'types/home'; import GlobalAPI from 'api'; +import { redirectToDefaultNotFound } from 'lib/hooks/router/redirect'; +import { imperativeAuthenticator, setI18nConfig } from 'lib/hooks/session'; interface AppLoaderData { home: HomeLayoutData; announcements: AnnouncementMiniEntity[]; } -export const loader = async (): Promise => ({ - home: (await GlobalAPI.home.fetch()).data, - announcements: (await GlobalAPI.announcements.index(true)).data.announcements, -}); +export const loader = async (): Promise => { + try { + const { data: home } = await GlobalAPI.home.fetch(); + const { data: announcements } = await GlobalAPI.announcements.index(true); + + setI18nConfig({ + locale: home.locale, + timeZone: home.timeZone ?? undefined, + }); + + if (home.user) { + imperativeAuthenticator.authenticate(); + } else { + imperativeAuthenticator.deauthenticate(); + } + + return { home, announcements: announcements.announcements }; + } catch (error) { + if (error instanceof AxiosError && error.response?.status === 404) + redirectToDefaultNotFound(); + + throw error; + } +}; export const useAppLoader = (): AppLoaderData => useLoaderData() as AppLoaderData; diff --git a/client/app/lib/moment.ts b/client/app/lib/moment.ts index 1327ec3f524..d63b9cd3c3f 100644 --- a/client/app/lib/moment.ts +++ b/client/app/lib/moment.ts @@ -1,7 +1,5 @@ import moment from 'moment-timezone'; -import { timeZone } from 'lib/helpers/server-context'; - const LONG_DATE_FORMAT = 'DD MMM YYYY' as const; const LONG_TIME_FORMAT = 'h:mma' as const; const LONG_DATE_TIME_FORMAT = @@ -20,8 +18,6 @@ const FULL_DATE_TIME_FORMAT = 'dddd, MMMM D YYYY, HH:mm' as const; const MINI_DATE_TIME_FORMAT = 'D MMM YYYY HH:mm' as const; const MINI_DATE_TIME_YEARLESS_FORMAT = 'D MMM HH:mm' as const; -moment.tz.setDefault(timeZone ?? undefined); - // TODO: Do not export moment and create the helpers here export default moment; From d3c5a0671e50bbd1ed1fdb56d87d3d8dd79443a4 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:14:48 +0800 Subject: [PATCH 078/173] feat: add React authentication pages --- client/app/api/Users.ts | 88 +++++- .../app/bundles/users/components/Widget.tsx | 47 +++ client/app/bundles/users/masqueradeLoader.ts | 13 + .../bundles/users/pages/ConfirmEmailPage.tsx | 63 ++++ .../users/pages/ForgotPasswordLandingPage.tsx | 36 +++ .../users/pages/ForgotPasswordPage.tsx | 117 ++++++++ .../ResendConfirmationEmailLandingPage.tsx | 56 ++++ .../pages/ResendConfirmationEmailPage.tsx | 122 ++++++++ .../bundles/users/pages/ResetPasswordPage.tsx | 192 ++++++++++++ client/app/bundles/users/pages/SignInPage.tsx | 146 ++++++++++ .../bundles/users/pages/SignUpLandingPage.tsx | 46 +++ client/app/bundles/users/pages/SignUpPage.tsx | 275 ++++++++++++++++++ client/app/bundles/users/pages/UserShow.tsx | 7 +- client/app/bundles/users/translations.ts | 235 +++++++++++++++ client/app/bundles/users/validations.ts | 53 ++++ .../AppContainer/MasqueradeBanner.tsx | 6 +- .../app/lib/containers/AuthPagesContainer.tsx | 29 ++ client/app/types/users.ts | 7 + 18 files changed, 1527 insertions(+), 11 deletions(-) create mode 100644 client/app/bundles/users/components/Widget.tsx create mode 100644 client/app/bundles/users/masqueradeLoader.ts create mode 100644 client/app/bundles/users/pages/ConfirmEmailPage.tsx create mode 100644 client/app/bundles/users/pages/ForgotPasswordLandingPage.tsx create mode 100644 client/app/bundles/users/pages/ForgotPasswordPage.tsx create mode 100644 client/app/bundles/users/pages/ResendConfirmationEmailLandingPage.tsx create mode 100644 client/app/bundles/users/pages/ResendConfirmationEmailPage.tsx create mode 100644 client/app/bundles/users/pages/ResetPasswordPage.tsx create mode 100644 client/app/bundles/users/pages/SignInPage.tsx create mode 100644 client/app/bundles/users/pages/SignUpLandingPage.tsx create mode 100644 client/app/bundles/users/pages/SignUpPage.tsx create mode 100644 client/app/bundles/users/translations.ts create mode 100644 client/app/bundles/users/validations.ts create mode 100644 client/app/lib/containers/AuthPagesContainer.tsx diff --git a/client/app/api/Users.ts b/client/app/api/Users.ts index c12ee2c36a8..39bde343183 100644 --- a/client/app/api/Users.ts +++ b/client/app/api/Users.ts @@ -4,6 +4,7 @@ import { EmailData, EmailPostData, EmailsData, + InvitedSignUpData, PasswordPostData, ProfileData, ProfilePostData, @@ -72,13 +73,94 @@ export default class UsersAPI extends BaseAPI { return this.client.post(url); } - resendConfirmationEmail( + resendConfirmationEmailByURL( url: NonNullable, ): APIResponse { return this.client.post(url); } - signOut(url: string): APIResponse { - return this.client.delete(url); + masquerade(url: string): APIResponse { + return this.client.get(url); + } + + stopMasquerade(url: string): APIResponse { + return this.client.get(url); + } + + signOut(): APIResponse { + return this.client.delete(`${this.#urlPrefix}/sign_out`); + } + + signIn(email: string, password: string, rememberMe: boolean): APIResponse { + const formData = new FormData(); + + formData.append('user[email]', email); + formData.append('user[password]', password); + formData.append('user[remember_me]', rememberMe ? '1' : '0'); + + return this.client.post(`${this.#urlPrefix}/sign_in`, formData); + } + + signUp( + name: string, + email: string, + password: string, + captchaResponse: string, + invitation?: string, + ): APIResponse<{ id: number | null; confirmed: boolean }> { + const formData = new FormData(); + + formData.append('user[name]', name); + formData.append('user[email]', email); + formData.append('user[password]', password); + formData.append('user[password_confirmation]', password); + formData.append('g-recaptcha-response', captchaResponse); + if (invitation) formData.append('invitation', invitation); + + return this.client.post(this.#urlPrefix, formData); + } + + verifyInvitationToken(token: string): APIResponse { + return this.client.get(`${this.#urlPrefix}/sign_up`, { + params: { invitation: token }, + }); + } + + requestResetPassword(email: string): APIResponse { + const formData = new FormData(); + + formData.append('user[email]', email); + + return this.client.post(`${this.#urlPrefix}/password`, formData); + } + + resendConfirmationEmail(email: string): APIResponse { + const formData = new FormData(); + + formData.append('user[email]', email); + + return this.client.post(`${this.#urlPrefix}/confirmation`, formData); + } + + verifyResetPasswordToken(token: string): APIResponse<{ email: string }> { + return this.client.get(`${this.#urlPrefix}/password/edit`, { + params: { reset_password_token: token }, + }); + } + + resetPassword(token: string, password: string): APIResponse { + const formData = new FormData(); + + formData.append('user[reset_password_token]', token); + formData.append('user[password]', password); + formData.append('user[password_confirmation]', password); + + return this.client.patch(`${this.#urlPrefix}/password`, formData); + } + + confirmEmail(token: string): APIResponse<{ email: string }> { + return this.client.get(`${this.#urlPrefix}/confirmation`, { + params: { confirmation_token: token }, + }); } } diff --git a/client/app/bundles/users/components/Widget.tsx b/client/app/bundles/users/components/Widget.tsx new file mode 100644 index 00000000000..430be424f7d --- /dev/null +++ b/client/app/bundles/users/components/Widget.tsx @@ -0,0 +1,47 @@ +import { ReactNode } from 'react'; +import { Typography } from '@mui/material'; + +interface ContainerProps { + children?: ReactNode; + className?: string; +} + +interface WidgetProps extends ContainerProps { + title: string; + subtitle?: string; +} + +const Widget = (props: WidgetProps): JSX.Element => ( +
e.preventDefault()} + > +
+ {props.title} + + {props.subtitle && ( + {props.subtitle} + )} +
+ + {props.children} +
+); + +const WidgetBody = (props: ContainerProps): JSX.Element => ( +
+ {props.children} +
+); + +const WidgetFoot = (props: ContainerProps): JSX.Element => ( +
+ {props.children} +
+); + +export default Object.assign(Widget, { Body: WidgetBody, Foot: WidgetFoot }); diff --git a/client/app/bundles/users/masqueradeLoader.ts b/client/app/bundles/users/masqueradeLoader.ts new file mode 100644 index 00000000000..c3a4f2b453b --- /dev/null +++ b/client/app/bundles/users/masqueradeLoader.ts @@ -0,0 +1,13 @@ +import { LoaderFunction, redirect } from 'react-router-dom'; + +import GlobalAPI from 'api'; + +export const masqueradeLoader: LoaderFunction = async ({ request }) => { + await GlobalAPI.users.masquerade(request.url); + return redirect('/'); +}; + +export const stopMasqueradeLoader: LoaderFunction = async ({ request }) => { + await GlobalAPI.users.stopMasquerade(request.url); + return redirect('/admin/users'); +}; diff --git a/client/app/bundles/users/pages/ConfirmEmailPage.tsx b/client/app/bundles/users/pages/ConfirmEmailPage.tsx new file mode 100644 index 00000000000..8ee33aafd02 --- /dev/null +++ b/client/app/bundles/users/pages/ConfirmEmailPage.tsx @@ -0,0 +1,63 @@ +import { + LoaderFunction, + Navigate, + redirect, + useLoaderData, +} from 'react-router-dom'; +import { Typography } from '@mui/material'; + +import GlobalAPI from 'api'; +import Link from 'lib/components/core/Link'; +import toast from 'lib/hooks/toast'; +import useEffectOnce from 'lib/hooks/useEffectOnce'; +import useTranslation from 'lib/hooks/useTranslation'; + +import Widget from '../components/Widget'; +import translations from '../translations'; + +const ConfirmEmailPage = (): JSX.Element => { + const { t } = useTranslation(); + + const email = useLoaderData() as string; + + return ( + {chunk}, + })} + title={t(translations.emailConfirmed)} + > + + + {t(translations.manageAllEmailsInAccountSettings, { + link: (chunk) => {chunk}, + })} + + + + ); +}; + +const loader: LoaderFunction = async ({ request }) => { + const token = new URL(request.url).searchParams.get('confirmation_token'); + if (!token) return redirect('/users/confirmation/new'); + + const { data } = await GlobalAPI.users.confirmEmail(token); + return data.email; +}; + +const ConfirmEmailInvalidRedirect = (): JSX.Element => { + const { t } = useTranslation(); + + useEffectOnce(() => { + toast.error(t(translations.confirmEmailLinkInvalidOrExpired)); + }); + + return ; +}; + +export default Object.assign(ConfirmEmailPage, { + loader, + InvalidRedirect: ConfirmEmailInvalidRedirect, +}); diff --git a/client/app/bundles/users/pages/ForgotPasswordLandingPage.tsx b/client/app/bundles/users/pages/ForgotPasswordLandingPage.tsx new file mode 100644 index 00000000000..86419a3fbbf --- /dev/null +++ b/client/app/bundles/users/pages/ForgotPasswordLandingPage.tsx @@ -0,0 +1,36 @@ +import { Navigate } from 'react-router-dom'; +import { Typography } from '@mui/material'; + +import Link from 'lib/components/core/Link'; +import { useEmailFromLocationState } from 'lib/containers/AuthPagesContainer'; +import useTranslation from 'lib/hooks/useTranslation'; + +import Widget from '../components/Widget'; +import translations from '../translations'; + +const ForgotPasswordLandingPage = (): JSX.Element => { + const { t } = useTranslation(); + + const email = useEmailFromLocationState(); + if (!email) return ; + + return ( + {chunk}, + })} + title={t(translations.checkYourEmail)} + > + + + {t(translations.suddenlyRememberPassword)} + + + {t(translations.signIn)} + + + ); +}; + +export default ForgotPasswordLandingPage; diff --git a/client/app/bundles/users/pages/ForgotPasswordPage.tsx b/client/app/bundles/users/pages/ForgotPasswordPage.tsx new file mode 100644 index 00000000000..cef73e4fd36 --- /dev/null +++ b/client/app/bundles/users/pages/ForgotPasswordPage.tsx @@ -0,0 +1,117 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { LoadingButton } from '@mui/lab'; +import { Typography } from '@mui/material'; +import { AxiosError } from 'axios'; +import { ValidationError } from 'yup'; + +import GlobalAPI from 'api'; +import TextField from 'lib/components/core/fields/TextField'; +import Link from 'lib/components/core/Link'; +import { useEmailFromAuthPagesContext } from 'lib/containers/AuthPagesContainer'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; + +import Widget from '../components/Widget'; +import translations from '../translations'; +import { emailValidationSchema } from '../validations'; + +const ForgotPasswordPage = (): JSX.Element => { + const { t } = useTranslation(); + + const [email, setEmail] = useEmailFromAuthPagesContext(); + + const [submitting, setSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(); + + const navigate = useNavigate(); + + const handleRequestResetPassword = async (): Promise => { + setSubmitting(true); + setErrorMessage(undefined); + + try { + const validatedEmail = await emailValidationSchema(t).validate(email); + if (!validatedEmail) + throw new Error(`validatedEmail is ${validatedEmail}`); + + await GlobalAPI.users.requestResetPassword(validatedEmail); + navigate('completed', { state: validatedEmail }); + } catch (error) { + if (error instanceof ValidationError) { + setErrorMessage(error.message); + return; + } + + if (error instanceof AxiosError) { + setErrorMessage(error.response?.data?.errors?.email); + toast.error(t(translations.errorRequestingResetPassword)); + return; + } + + throw error; + } finally { + setSubmitting(false); + } + }; + + return ( + + + setEmail(e.target.value)} + onPressEnter={handleRequestResetPassword} + required + trims + type="email" + value={email} + variant="filled" + /> + + + + {t(translations.requestToResetPassword)} + + + + + {t(translations.suddenlyRememberPassword)} + + + + {t(translations.signInAgain)} + + + + + + {t(translations.dontYetHaveAnAccount)} + + + + {t(translations.signUp)} + + + + ); +}; + +export default ForgotPasswordPage; diff --git a/client/app/bundles/users/pages/ResendConfirmationEmailLandingPage.tsx b/client/app/bundles/users/pages/ResendConfirmationEmailLandingPage.tsx new file mode 100644 index 00000000000..b77fbbdadb5 --- /dev/null +++ b/client/app/bundles/users/pages/ResendConfirmationEmailLandingPage.tsx @@ -0,0 +1,56 @@ +import { Navigate } from 'react-router-dom'; +import { Typography } from '@mui/material'; + +import Link from 'lib/components/core/Link'; +import { SUPPORT_EMAIL } from 'lib/constants/sharedConstants'; +import { useEmailFromLocationState } from 'lib/containers/AuthPagesContainer'; +import useTranslation from 'lib/hooks/useTranslation'; + +import Widget from '../components/Widget'; +import translations from '../translations'; + +const ResendConfirmationEmailLandingPage = (): JSX.Element => { + const { t } = useTranslation(); + + const email = useEmailFromLocationState(); + if (!email) return ; + + return ( + {chunk}, + })} + title={t(translations.checkYourEmail)} + > + + {t(translations.resendConfirmationEmailIfIssuePersistsContactUs, { + supportEmail: SUPPORT_EMAIL, + link: (chunk) => ( + + {chunk} + + ), + })} + + + + + {t(translations.confirmedYourEmail)} + + + {t(translations.signIn)} + + + + + {t(translations.dontYetHaveAnAccount)} + + + {t(translations.signUp)} + + + ); +}; + +export default ResendConfirmationEmailLandingPage; diff --git a/client/app/bundles/users/pages/ResendConfirmationEmailPage.tsx b/client/app/bundles/users/pages/ResendConfirmationEmailPage.tsx new file mode 100644 index 00000000000..e3fee0de2fc --- /dev/null +++ b/client/app/bundles/users/pages/ResendConfirmationEmailPage.tsx @@ -0,0 +1,122 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { LightbulbOutlined } from '@mui/icons-material'; +import { LoadingButton } from '@mui/lab'; +import { Alert, Typography } from '@mui/material'; +import { AxiosError } from 'axios'; +import { ValidationError } from 'yup'; + +import GlobalAPI from 'api'; +import TextField from 'lib/components/core/fields/TextField'; +import Link from 'lib/components/core/Link'; +import { useEmailFromAuthPagesContext } from 'lib/containers/AuthPagesContainer'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; + +import Widget from '../components/Widget'; +import translations from '../translations'; +import { emailValidationSchema } from '../validations'; + +const ResendConfirmationEmailPage = (): JSX.Element => { + const { t } = useTranslation(); + + const [email, setEmail] = useEmailFromAuthPagesContext(); + + const [submitting, setSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(); + + const navigate = useNavigate(); + + const handleResendConfirmationEmail = async (): Promise => { + setSubmitting(true); + setErrorMessage(undefined); + + try { + const validatedEmail = await emailValidationSchema(t).validate(email); + if (!validatedEmail) + throw new Error(`validatedEmail is ${validatedEmail}`); + + await GlobalAPI.users.resendConfirmationEmail(validatedEmail); + navigate('completed', { state: validatedEmail }); + } catch (error) { + if (error instanceof ValidationError) { + setErrorMessage(error.message); + return; + } + + if (error instanceof AxiosError) { + setErrorMessage(error.response?.data?.errors?.email); + toast.error(t(translations.errorRequestingResetPassword)); + return; + } + + throw error; + } finally { + setSubmitting(false); + } + }; + + return ( + + + } severity="info"> + {t(translations.checkSpamBeforeRequestNewConfirmationEmail)} + + + setEmail(e.target.value)} + onPressEnter={handleResendConfirmationEmail} + required + trims + type="email" + value={email} + variant="filled" + /> + + + + {t(translations.resendConfirmationEmail)} + + + + + {t(translations.alreadyHaveAnAccount)} + + + + {t(translations.signIn)} + + + + + + {t(translations.dontYetHaveAnAccount)} + + + + {t(translations.signUp)} + + + + ); +}; + +export default ResendConfirmationEmailPage; diff --git a/client/app/bundles/users/pages/ResetPasswordPage.tsx b/client/app/bundles/users/pages/ResetPasswordPage.tsx new file mode 100644 index 00000000000..4c3d7801ae5 --- /dev/null +++ b/client/app/bundles/users/pages/ResetPasswordPage.tsx @@ -0,0 +1,192 @@ +import { useState } from 'react'; +import { + LoaderFunction, + Navigate, + redirect, + useLoaderData, + useNavigate, +} from 'react-router-dom'; +import { LoadingButton } from '@mui/lab'; +import { Typography } from '@mui/material'; +import { AxiosError } from 'axios'; +import { ValidationError } from 'yup'; + +import GlobalAPI from 'api'; +import PasswordTextField from 'lib/components/core/fields/PasswordTextField'; +import TextField from 'lib/components/core/fields/TextField'; +import Link from 'lib/components/core/Link'; +import toast from 'lib/hooks/toast'; +import useEffectOnce from 'lib/hooks/useEffectOnce'; +import useTranslation from 'lib/hooks/useTranslation'; + +import Widget from '../components/Widget'; +import translations from '../translations'; +import { getValidationErrors, passwordValidationSchema } from '../validations'; + +interface ResetPasswordLoaderData { + email: string; + token: string; +} + +const ResetPasswordPage = (): JSX.Element => { + const { t } = useTranslation(); + + const { email, token } = useLoaderData() as ResetPasswordLoaderData; + + const [password, setPassword] = useState(''); + const [passwordConfirmation, setPasswordConfirmation] = useState(''); + const [requirePasswordConfirmation, setRequirePasswordConfirmation] = + useState(true); + + const [submitting, setSubmitting] = useState(false); + const [errors, setErrors] = useState>({}); + + const navigate = useNavigate(); + + const handleResetPassword = async (): Promise => { + setSubmitting(true); + setErrors({}); + + const data = { password, passwordConfirmation }; + try { + const validatedData = await passwordValidationSchema(t).validate(data, { + abortEarly: false, + context: { requirePasswordConfirmation }, + }); + + await GlobalAPI.users.resetPassword(token, validatedData.password); + + toast.success(t(translations.passwordSuccessfullyReset)); + navigate('/users/sign_in'); + } catch (error) { + if (error instanceof ValidationError) { + setErrors(getValidationErrors(error)); + return; + } + + if (error instanceof AxiosError && error.response?.status === 422) { + const responseErrors = error.response.data?.errors; + + if (responseErrors?.reset_password_token) { + toast.error(t(translations.resetPasswordLinkInvalidOrExpired)); + navigate('/users/password/new'); + } else { + toast.error(t(translations.errorResettingPassword)); + } + + return; + } + + throw error; + } finally { + setSubmitting(false); + } + }; + + return ( + + + + + setPassword(e.target.value)} + onChangePasswordVisibility={(visible): void => + setRequirePasswordConfirmation(!visible) + } + onPressEnter={handleResetPassword} + required + type="password" + value={password} + variant="filled" + /> + + {requirePasswordConfirmation && ( + setPasswordConfirmation(e.target.value)} + onCopy={(e): void => e.preventDefault()} + onCut={(e): void => e.preventDefault()} + onPaste={(e): void => e.preventDefault()} + onPressEnter={handleResetPassword} + required + type="password" + value={passwordConfirmation} + variant="filled" + /> + )} + + + + {t(translations.resetPassword)} + + + + + {t(translations.suddenlyRememberPassword)} + + + + {t(translations.signInAgain)} + + + + ); +}; + +const loader: LoaderFunction = async ({ request }) => { + const token = new URL(request.url).searchParams.get('reset_password_token'); + if (!token) return redirect('/users/password/new'); + + const { data } = await GlobalAPI.users.verifyResetPasswordToken(token); + return { email: data.email, token } satisfies ResetPasswordLoaderData; +}; + +const ResetPasswordInvalidRedirect = (): JSX.Element => { + const { t } = useTranslation(); + + useEffectOnce(() => { + toast.error(t(translations.resetPasswordLinkInvalidOrExpired)); + }); + + return ; +}; + +export default Object.assign(ResetPasswordPage, { + loader, + InvalidRedirect: ResetPasswordInvalidRedirect, +}); diff --git a/client/app/bundles/users/pages/SignInPage.tsx b/client/app/bundles/users/pages/SignInPage.tsx new file mode 100644 index 00000000000..f191a8982e8 --- /dev/null +++ b/client/app/bundles/users/pages/SignInPage.tsx @@ -0,0 +1,146 @@ +import { useState } from 'react'; +import { LoadingButton } from '@mui/lab'; +import { Alert, Typography } from '@mui/material'; +import { AxiosError } from 'axios'; + +import GlobalAPI from 'api'; +import Checkbox from 'lib/components/core/buttons/Checkbox'; +import PasswordTextField from 'lib/components/core/fields/PasswordTextField'; +import TextField from 'lib/components/core/fields/TextField'; +import Link from 'lib/components/core/Link'; +import { useEmailFromAuthPagesContext } from 'lib/containers/AuthPagesContainer'; +import { useRedirectable } from 'lib/hooks/router/redirect'; +import { useAuthenticator } from 'lib/hooks/session'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; + +import Widget from '../components/Widget'; +import translations from '../translations'; + +const SignInPage = (): JSX.Element => { + const { t } = useTranslation(); + + const [email, setEmail] = useEmailFromAuthPagesContext(); + + const [password, setPassword] = useState(''); + const [rememberMe, setRememberMe] = useState(false); + const [errored, setErrored] = useState(false); + const [submitting, setSubmitting] = useState(false); + + const { redirectable, expired } = useRedirectable(); + const { authenticate } = useAuthenticator(); + + const handleSignIn = async (): Promise => { + setSubmitting(true); + setErrored(false); + + try { + await GlobalAPI.users.signIn(email, password, rememberMe); + authenticate(); + } catch (error) { + if (!(error instanceof AxiosError)) throw error; + + if (error.response?.status === 401) { + setErrored(true); + toast.error(t(translations.invalidEmailOrPassword)); + } + } finally { + setSubmitting(false); + } + }; + + return ( + + + {redirectable && ( + + {expired + ? t(translations.sessionExpiredSignInToContinue) + : t(translations.mustSignInToAccessPage)} + + )} + + setEmail(e.target.value)} + onPressEnter={handleSignIn} + trims + type="email" + value={email} + variant="filled" + /> + + setPassword(e.target.value)} + onPressEnter={handleSignIn} + trims + type="password" + value={password} + variant="filled" + /> + + setRememberMe(value)} + size="small" + value={rememberMe} + /> + + + + {t(translations.signIn)} + + + + + {t(translations.dontYetHaveAnAccount)} + + + + {t(translations.signUp)} + + + + + + {t(translations.troubleSigningIn)} + + +
+ + {t(translations.forgotPassword)} + + + + {t(translations.resendConfirmationEmail)} + +
+
+
+ ); +}; + +export default SignInPage; diff --git a/client/app/bundles/users/pages/SignUpLandingPage.tsx b/client/app/bundles/users/pages/SignUpLandingPage.tsx new file mode 100644 index 00000000000..8b5161f7555 --- /dev/null +++ b/client/app/bundles/users/pages/SignUpLandingPage.tsx @@ -0,0 +1,46 @@ +import { Navigate } from 'react-router-dom'; +import { Typography } from '@mui/material'; + +import Link from 'lib/components/core/Link'; +import { useEmailFromLocationState } from 'lib/containers/AuthPagesContainer'; +import useTranslation from 'lib/hooks/useTranslation'; + +import Widget from '../components/Widget'; +import translations from '../translations'; + +const SignUpLandingPage = (): JSX.Element | null => { + const { t } = useTranslation(); + + const email = useEmailFromLocationState(); + if (!email) return ; + + return ( + {chunk}, + })} + title={t(translations.checkYourEmail)} + > + + + {t(translations.confirmedYourEmail)} + + + {t(translations.signIn)} + + + + + {t(translations.didntReceiveConfirmationEmail)} + + + + {t(translations.resendConfirmationEmail)} + + + + ); +}; + +export default SignUpLandingPage; diff --git a/client/app/bundles/users/pages/SignUpPage.tsx b/client/app/bundles/users/pages/SignUpPage.tsx new file mode 100644 index 00000000000..711d385155e --- /dev/null +++ b/client/app/bundles/users/pages/SignUpPage.tsx @@ -0,0 +1,275 @@ +import { ComponentRef, useRef, useState } from 'react'; +import { LoaderFunction, useLoaderData, useNavigate } from 'react-router-dom'; +import { LoadingButton } from '@mui/lab'; +import { Alert, Typography } from '@mui/material'; +import { AxiosError } from 'axios'; +import { InvitedSignUpData } from 'types/users'; +import { ValidationError } from 'yup'; + +import GlobalAPI from 'api'; +import CAPTCHAField from 'lib/components/core/fields/CAPTCHAField'; +import PasswordTextField from 'lib/components/core/fields/PasswordTextField'; +import TextField from 'lib/components/core/fields/TextField'; +import Link from 'lib/components/core/Link'; +import { useEmailFromAuthPagesContext } from 'lib/containers/AuthPagesContainer'; +import { useAuthenticator } from 'lib/hooks/session'; +import toast from 'lib/hooks/toast'; +import useTranslation from 'lib/hooks/useTranslation'; + +import Widget from '../components/Widget'; +import translations from '../translations'; +import { getValidationErrors, signUpValidationSchema } from '../validations'; + +type InvitedSignUpLoaderData = null | (InvitedSignUpData & { token: string }); + +const SignUpPage = (): JSX.Element => { + const { t } = useTranslation(); + + const [email, setEmail] = useEmailFromAuthPagesContext(); + + const invitation = useLoaderData() as InvitedSignUpLoaderData; + if (invitation?.email) setEmail(invitation.email); + + const [name, setName] = useState(invitation?.name ?? ''); + const [password, setPassword] = useState(''); + const [passwordConfirmation, setPasswordConfirmation] = useState(''); + const [requirePasswordConfirmation, setRequirePasswordConfirmation] = + useState(true); + + const captchaRef = useRef>(null); + const [captchaResponse, setCaptchaResponse] = useState(null); + + const [submitting, setSubmitting] = useState(false); + const [errors, setErrors] = useState>({}); + + const navigate = useNavigate(); + const { authenticate } = useAuthenticator(); + + const resetCaptcha = (): void => { + captchaRef.current?.reset(); + }; + + const handleSignUp = async (): Promise => { + if (!captchaResponse) + throw new Error(`received ${captchaResponse} captchaResponse`); + + setErrors({}); + setSubmitting(true); + + const data = { name, email, password, passwordConfirmation }; + + try { + const validatedData = await signUpValidationSchema(t).validate(data, { + abortEarly: false, + context: { requirePasswordConfirmation }, + }); + + const { data: result } = await GlobalAPI.users.signUp( + validatedData.name, + validatedData.email, + validatedData.password, + captchaResponse, + invitation?.token, + ); + + if (!result.id) { + toast.error(t(translations.errorSigningUp)); + return; + } + + if (invitation) { + authenticate(); + navigate(`/courses/${invitation.courseId}`); + toast.success( + t(translations.signUpWelcomeToCourse, { + course: invitation.courseTitle, + }), + ); + + return; + } + + if (!result.confirmed) { + navigate('completed', { state: validatedData.email }); + } else { + navigate('/'); + toast.success(t(translations.signUpSuccessful)); + } + } catch (error) { + if (error instanceof ValidationError) { + setErrors(getValidationErrors(error)); + return; + } + + if (error instanceof AxiosError && error.response?.status === 422) { + toast.error(t(translations.errorSigningUp)); + setErrors(error.response.data?.errors); + return; + } + + throw error; + } finally { + resetCaptcha(); + setSubmitting(false); + } + }; + + return ( + + + {invitation && ( + + {t(translations.completeSignUpToJoinCourse, { + course: invitation.courseTitle, + strong: (chunk) => {chunk}, + })} + + )} + + setName(e.target.value)} + onPressEnter={handleSignUp} + required + trims + type="text" + value={name} + variant="filled" + /> + + setEmail(e.target.value)} + onPressEnter={handleSignUp} + required + trims + type="email" + value={email} + variant="filled" + /> + + setPassword(e.target.value)} + onChangePasswordVisibility={(visible): void => + setRequirePasswordConfirmation(!visible) + } + onPressEnter={handleSignUp} + required + type="password" + value={password} + variant="filled" + /> + + {requirePasswordConfirmation && ( + setPasswordConfirmation(e.target.value)} + onCopy={(e): void => e.preventDefault()} + onCut={(e): void => e.preventDefault()} + onPaste={(e): void => e.preventDefault()} + onPressEnter={handleSignUp} + required + type="password" + value={passwordConfirmation} + variant="filled" + /> + )} + + + + + + {t(translations.signUp)} + + + + {t(translations.signUpAgreement, { + tos: (chunk) => ( + + {chunk} + + ), + pp: (chunk) => ( + + {chunk} + + ), + })} + + + + + {t(translations.alreadyHaveAnAccount)} + + + + {t(translations.signIn)} + + + + ); +}; + +const loader: LoaderFunction = async ({ request }) => { + const token = new URL(request.url).searchParams.get('invitation'); + if (!token) return null; + + try { + const { data } = await GlobalAPI.users.verifyInvitationToken(token); + if (!data) return null; + + return { ...data, token } satisfies InvitedSignUpLoaderData; + } catch (error) { + if (error instanceof AxiosError && error.response?.status === 409) + toast.error(error.response.data?.message); + + return null; + } +}; + +export default Object.assign(SignUpPage, { loader }); diff --git a/client/app/bundles/users/pages/UserShow.tsx b/client/app/bundles/users/pages/UserShow.tsx index c2fdd1381d5..6ccbfaf1a6e 100644 --- a/client/app/bundles/users/pages/UserShow.tsx +++ b/client/app/bundles/users/pages/UserShow.tsx @@ -3,6 +3,7 @@ import { defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; import { useParams } from 'react-router-dom'; import { Avatar, Grid, Typography } from '@mui/material'; +import Page from 'lib/components/core/layouts/Page'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; import { useAppDispatch, useAppSelector } from 'lib/hooks/store'; @@ -63,7 +64,7 @@ const UserShow: FC = (props) => { } return ( - <> + = (props) => { item justifyContent={{ xs: 'center', sm: 'start' }} > - {user.name} + {user.name} {currentCourses.length > 0 && ( @@ -112,7 +113,7 @@ const UserShow: FC = (props) => { title={intl.formatMessage(translations.otherInstances)} /> )} - + ); }; diff --git a/client/app/bundles/users/translations.ts b/client/app/bundles/users/translations.ts new file mode 100644 index 00000000000..a3c7b72ebd7 --- /dev/null +++ b/client/app/bundles/users/translations.ts @@ -0,0 +1,235 @@ +import { defineMessages } from 'react-intl'; + +const translations = defineMessages({ + emailAddress: { + id: 'users.emailAddress', + defaultMessage: 'Email address', + }, + password: { + id: 'users.password', + defaultMessage: 'Password', + }, + signInToYourAccount: { + id: 'users.signInToYourAccount', + defaultMessage: 'Sign in to Coursemology', + }, + signIn: { + id: 'users.signIn', + defaultMessage: 'Sign in', + }, + dontYetHaveAnAccount: { + id: 'users.dontYetHaveAnAccount', + defaultMessage: "Don't yet have an account?", + }, + signUp: { + id: 'users.signUp', + defaultMessage: 'Sign up', + }, + forgotPassword: { + id: 'users.forgotPassword', + defaultMessage: 'Forgot password', + }, + resendConfirmationEmail: { + id: 'users.resendConfirmationEmail', + defaultMessage: 'Resend confirmation email', + }, + troubleSigningIn: { + id: 'users.troubleSigningIn', + defaultMessage: 'Trouble signing in?', + }, + alreadyHaveAnAccount: { + id: 'users.alreadyHaveAnAccount', + defaultMessage: 'Already have an account?', + }, + createAnAccount: { + id: 'users.createAnAccount', + defaultMessage: 'Create a new account', + }, + createAnAccountSubtitle: { + id: 'users.createAnAccountSubtitle', + defaultMessage: + 'Join students and teachers in a universe of fun online education!', + }, + name: { + id: 'users.name', + defaultMessage: 'Name', + }, + confirmPassword: { + id: 'users.confirmPassword', + defaultMessage: 'Confirm password', + }, + rememberMe: { + id: 'users.rememberMe', + defaultMessage: 'Remember me on this device', + }, + rememberMeHint: { + id: 'users.rememberMeHint', + defaultMessage: 'Only use this on your personal devices.', + }, + signUpAgreement: { + id: 'users.signUpAgreement', + defaultMessage: + 'By signing up, you agree to our Terms of Service and that you have read our Privacy Policy.', + }, + requestToResetPassword: { + id: 'users.requestToResetPassword', + defaultMessage: 'Request to reset password', + }, + forgotPasswordSubtitle: { + id: 'users.forgotPasswordSubtitle', + defaultMessage: + 'Recover access to your account by resetting your password.', + }, + suddenlyRememberPassword: { + id: 'users.suddenlyRememberPassword', + defaultMessage: 'Suddenly remembered?', + }, + signInAgain: { + id: 'users.signInAgain', + defaultMessage: 'Try signing in again', + }, + resetPassword: { + id: 'users.resetPassword', + defaultMessage: 'Reset password', + }, + resetPasswordSubtitle: { + id: 'users.resetPasswordSubtitle', + defaultMessage: + 'One more step: choose a new password for your account. Better remember it this time!', + }, + resendConfirmationEmailSubtitle: { + id: 'users.resendConfirmationEmailSubtitle', + defaultMessage: + "If you have created an account but haven't received a confirmation email, you can request a new one here.", + }, + checkSpamBeforeRequestNewConfirmationEmail: { + id: 'users.checkSpamBeforeRequestNewConfirmationEmail', + defaultMessage: + 'You may want to check your spam folder for the email before requesting a new one.', + }, + invalidEmailOrPassword: { + id: 'users.invalidEmailOrPassword', + defaultMessage: + 'Oops, invalid email or password. Check your email or password and try again.', + }, + checkYourEmail: { + id: 'users.checkYourEmail', + defaultMessage: 'Almost there; check your email!', + }, + signUpCheckYourEmailSubtitle: { + id: 'users.signUpCheckYourEmailSubtitle', + defaultMessage: + "Your account has been created, but you'll need to confirm your email before you can use it. Follow the " + + "instructions we've sent to {email} to proceed.", + }, + confirmedYourEmail: { + id: 'users.confirmedYourEmail', + defaultMessage: 'Confirmed your email?', + }, + didntReceiveConfirmationEmail: { + id: 'users.didntReceiveConfirmationEmail', + defaultMessage: "Didn't receive the email?", + }, + passwordMinCharacters: { + id: 'users.passwordMinCharacters', + defaultMessage: 'Your password must be at least 8 characters long.', + }, + passwordConfirmationRequired: { + id: 'users.passwordConfirmationRequired', + defaultMessage: 'Please confirm your password here.', + }, + passwordConfirmationMustMatch: { + id: 'users.passwordConfirmationMustMatch', + defaultMessage: + 'Your password confirmation does not match your password above.', + }, + errorSigningUp: { + id: 'users.errorSigningUp', + defaultMessage: 'An error occurred while creating your account.', + }, + errorRequestingResetPassword: { + id: 'users.errorRequestingResetPassword', + defaultMessage: 'An error occurred while requesting a password reset.', + }, + forgotPasswordCheckYourEmailSubtitle: { + id: 'users.forgotPasswordCheckYourEmailSubtitle', + defaultMessage: + "Follow the instructions we've sent to {email} to reset your password. " + + 'Until then, you can still use your old password if you still remember it.', + }, + errorResendConfirmationEmail: { + id: 'users.errorResendConfirmationEmail', + defaultMessage: + 'An error occurred while requesting to resend confirmation email.', + }, + resendConfirmationEmailCheckYourEmailSubtitle: { + id: 'users.resendConfirmationEmailCheckYourEmailSubtitle', + defaultMessage: + "Follow the instructions we've sent to {email} to confirm your email. " + + 'Remember to check your spam folder before requesting another one.', + }, + resendConfirmationEmailIfIssuePersistsContactUs: { + id: 'users.resendConfirmationEmailIfIssuePersistsContactUs', + defaultMessage: + "If you still consistently don't receive the email, please contact us at {supportEmail}.", + }, + resetPasswordLinkInvalidOrExpired: { + id: 'users.resetPasswordTokenInvalidOrExpired', + defaultMessage: + "The password reset link you've used is either has expired or is invalid. Please use the correct one from " + + 'your email or request to reset again.', + }, + errorResettingPassword: { + id: 'users.errorResettingPassword', + defaultMessage: 'An error occurred while resetting your password.', + }, + passwordSuccessfullyReset: { + id: 'users.passwordSuccessfullyReset', + defaultMessage: + 'Your password was successfully reset. You may now sign in with your new password.', + }, + confirmEmailLinkInvalidOrExpired: { + id: 'users.confirmEmailLinkInvalidOrExpired', + defaultMessage: + "The email confirmation link you've used is either has expired or is invalid. Please use the correct one from " + + 'your email or request to resend another confirmation email.', + }, + emailConfirmed: { + id: 'users.emailConfirmed', + defaultMessage: 'Email has been confirmed!', + }, + emailConfirmedSubtitle: { + id: 'users.emailConfirmedSubtitle', + defaultMessage: + 'You can now sign in to your account with {email}.', + }, + manageAllEmailsInAccountSettings: { + id: 'users.manageAllEmailsInAccountSettings', + defaultMessage: + 'Manage all your email addresses in Account Settings.', + }, + completeSignUpToJoinCourse: { + id: 'users.completeSignUpToJoinCourse', + defaultMessage: + 'Almost there! Complete your sign up to join {course}.', + }, + signUpWelcomeToCourse: { + id: 'users.signUpWelcomeToCourse', + defaultMessage: 'Welcome to {course}!', + }, + signUpSuccessful: { + id: 'users.signUpSuccessful', + defaultMessage: 'Your account was successfully created.', + }, + mustSignInToAccessPage: { + id: 'users.mustSignInToAccessPage', + defaultMessage: "You'll need to sign in to access this page.", + }, + sessionExpiredSignInToContinue: { + id: 'users.sessionExpiredSignInToContinue', + defaultMessage: + 'Your session has expired. Please sign in again to continue.', + }, +}); + +export default translations; diff --git a/client/app/bundles/users/validations.ts b/client/app/bundles/users/validations.ts new file mode 100644 index 00000000000..96e81b1f55a --- /dev/null +++ b/client/app/bundles/users/validations.ts @@ -0,0 +1,53 @@ +import { + AnyObjectSchema, + object, + ref, + string, + StringSchema, + ValidationError, +} from 'yup'; + +import { Translated } from 'lib/hooks/useTranslation'; +import formTranslations from 'lib/translations/form'; + +import translations from './translations'; + +export const emailValidationSchema: Translated = (t) => + string() + .email(t(formTranslations.email)) + .required(t(formTranslations.required)); + +const passwordValidationSchemaObject: Translated< + Record +> = (t) => ({ + password: string() + .required(t(formTranslations.required)) + .min(8, t(translations.passwordMinCharacters)), + passwordConfirmation: string().when('$requirePasswordConfirmation', { + is: true, + then: string() + .equals([ref('password')], t(translations.passwordConfirmationMustMatch)) + .required(t(translations.passwordConfirmationRequired)), + otherwise: string().optional(), + }), +}); + +export const passwordValidationSchema: Translated = (t) => + object(passwordValidationSchemaObject(t)); + +export const signUpValidationSchema: Translated = (t) => + object({ + name: string().required(t(formTranslations.required)), + email: emailValidationSchema(t), + ...passwordValidationSchemaObject(t), + }); + +export const getValidationErrors = ( + errors: ValidationError, +): Record => + errors.inner.reduce>((result, { path, message }) => { + if (!path) return result; + + result[path] = message; + return result; + }, {}); diff --git a/client/app/lib/containers/AppContainer/MasqueradeBanner.tsx b/client/app/lib/containers/AppContainer/MasqueradeBanner.tsx index 9dd01bc390b..4fbb7121ca6 100644 --- a/client/app/lib/containers/AppContainer/MasqueradeBanner.tsx +++ b/client/app/lib/containers/AppContainer/MasqueradeBanner.tsx @@ -41,11 +41,7 @@ const MasqueradeBanner = (props: MasqueradeBannerProps): JSX.Element => {
- + {t(translations.stopMasquerading)}
diff --git a/client/app/lib/containers/AuthPagesContainer.tsx b/client/app/lib/containers/AuthPagesContainer.tsx new file mode 100644 index 00000000000..17bfdc1cb78 --- /dev/null +++ b/client/app/lib/containers/AuthPagesContainer.tsx @@ -0,0 +1,29 @@ +import { Dispatch, SetStateAction, useState } from 'react'; +import { Outlet, useLocation, useOutletContext } from 'react-router-dom'; +import { isString } from 'lodash'; + +import Page from 'lib/components/core/layouts/Page'; + +const AuthPagesContainer = (): JSX.Element => { + const emailState = useState(''); + + return ( + + + + ); +}; + +export const useEmailFromAuthPagesContext = (): [ + string, + Dispatch>, +] => useOutletContext(); + +export const useEmailFromLocationState = (): string | null => { + const location = useLocation(); + const maybeEmail = location.state; + + return isString(maybeEmail) ? maybeEmail.trim() : null; +}; + +export default AuthPagesContainer; diff --git a/client/app/types/users.ts b/client/app/types/users.ts index 48488fa1dad..4cf2bf7a8da 100644 --- a/client/app/types/users.ts +++ b/client/app/types/users.ts @@ -133,3 +133,10 @@ export interface PasswordPostData { password_confirmation?: PasswordData['passwordConfirmation']; }; } + +export interface InvitedSignUpData { + name: string; + email: string; + courseTitle: string; + courseId: string; +} From 67bef5a7b8c58b9ceb17584ad3f4064434a35d91 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:17:14 +0800 Subject: [PATCH 079/173] feat: lib/axios -> BaseAPI --- client/app/api/Attachments.js | 15 ----- client/app/api/Attachments.ts | 23 +++++++ client/app/api/course/Achievements.ts | 62 +++++++------------ .../app/api/course/Assessment/Assessments.js | 6 +- client/app/api/course/Video/Submissions.ts | 23 +++++-- .../submission/actions/attachments.js | 5 +- .../components/buttons/TodoAccessButton.tsx | 48 ++++---------- .../components/tables/PendingTodosTable.tsx | 18 +----- .../VideoControls/NextVideoButton.jsx | 6 +- client/app/lib/axios.js | 9 --- .../core/fields/CKEditorRichText.tsx | 12 ++-- 11 files changed, 87 insertions(+), 140 deletions(-) delete mode 100644 client/app/api/Attachments.js create mode 100644 client/app/api/Attachments.ts delete mode 100644 client/app/lib/axios.js diff --git a/client/app/api/Attachments.js b/client/app/api/Attachments.js deleted file mode 100644 index cc2eb06c0d0..00000000000 --- a/client/app/api/Attachments.js +++ /dev/null @@ -1,15 +0,0 @@ -import BaseAPI from './Base'; - -class AttachmentsAPI extends BaseAPI { - delete(attachmentId) { - return this.client.delete(`${AttachmentsAPI.#urlPrefix}/${attachmentId}`); - } - - static get #urlPrefix() { - return '/attachments'; - } -} - -const attachmentsAPI = new AttachmentsAPI(); - -export default attachmentsAPI; diff --git a/client/app/api/Attachments.ts b/client/app/api/Attachments.ts new file mode 100644 index 00000000000..484e3f3c830 --- /dev/null +++ b/client/app/api/Attachments.ts @@ -0,0 +1,23 @@ +import BaseAPI from './Base'; +import { APIResponse } from './types'; + +class AttachmentsAPI extends BaseAPI { + #urlPrefix = '/attachments'; + + delete(attachmentId: number): APIResponse { + return this.client.delete(`${this.#urlPrefix}/${attachmentId}`); + } + + upload(file: File): APIResponse<{ success: boolean; id?: number }> { + const formData = new FormData(); + + formData.append('file', file); + formData.append('name', file.name); + + return this.client.post(this.#urlPrefix, formData); + } +} + +const attachmentsAPI = new AttachmentsAPI(); + +export default attachmentsAPI; diff --git a/client/app/api/course/Achievements.ts b/client/app/api/course/Achievements.ts index e459966dab6..843755306d0 100644 --- a/client/app/api/course/Achievements.ts +++ b/client/app/api/course/Achievements.ts @@ -6,6 +6,8 @@ import { AchievementPermissions, } from 'types/course/achievements'; +import { APIResponse } from 'api/types'; + import BaseCourseAPI from './Base'; export default class AchievementsAPI extends BaseCourseAPI { @@ -16,40 +18,27 @@ export default class AchievementsAPI extends BaseCourseAPI { /** * Fetches a list of achievements in a course. */ - index(): Promise< - AxiosResponse<{ - achievements: AchievementListData[]; - permissions: AchievementPermissions; - }> - > { + index(): APIResponse<{ + achievements: AchievementListData[]; + permissions: AchievementPermissions; + }> { return this.client.get(this.#urlPrefix); } /** * Fetches an achievement. */ - fetch(achievementId: number): Promise< - AxiosResponse<{ - achievement: AchievementData; - }> - > { - return this.client.get(`${this.#urlPrefix}/${achievementId}`); + fetch(id: number): APIResponse<{ achievement: AchievementData }> { + return this.client.get(`${this.#urlPrefix}/${id}`); } /** * Fetches course users related to an achievement. - * - * @param {number} achievementId - * @return {Promise} */ - fetchAchievementCourseUsers(achievementId: number): Promise< - AxiosResponse<{ - achievementCourseUsers: AchievementCourseUserData[]; - }> - > { - return this.client.get( - `${this.#urlPrefix}/${achievementId}/achievement_course_users`, - ); + fetchAchievementCourseUsers(id: number): APIResponse<{ + achievementCourseUsers: AchievementCourseUserData[]; + }> { + return this.client.get(`${this.#urlPrefix}/${id}/achievement_course_users`); } /** @@ -59,43 +48,34 @@ export default class AchievementsAPI extends BaseCourseAPI { * { * achievement: { :title, :description, etc } * } - * @return {Promise} - * success response: { :id } - ID of created achievement. - * error response: { errors: [] } - An array of errors will be returned upon validation error. */ - create(params: FormData): Promise< - AxiosResponse<{ - id: number; - }> - > { + create(params: FormData): APIResponse<{ id: number }> { return this.client.post(this.#urlPrefix, params); } /** * Updates the achievement. * - * @param {number} achievementId + * @param {number} id * @param {object} params - params in the format of { achievement: { :title, :description, etc } } - * @return {Promise} - * success response: {} - * error response: { errors: [] } - An array of errors will be returned upon validation error. */ update( - achievementId: number, + id: number, params: FormData | object, - ): Promise { - return this.client.patch(`${this.#urlPrefix}/${achievementId}`, params); + ): APIResponse<{ achievement: AchievementData }> { + return this.client.patch(`${this.#urlPrefix}/${id}`, params); } /** * Deletes an achievement. * * @param {number} achievementId - * @return {Promise} - * success response: {} - * error response: {} */ delete(achievementId: number): Promise { return this.client.delete(`${this.#urlPrefix}/${achievementId}`); } + + reorder(ordering: string): APIResponse { + return this.client.post(`${this.#urlPrefix}/reorder`, ordering); + } } diff --git a/client/app/api/course/Assessment/Assessments.js b/client/app/api/course/Assessment/Assessments.js index 72651c9f30d..11cd56516c5 100644 --- a/client/app/api/course/Assessment/Assessments.js +++ b/client/app/api/course/Assessment/Assessments.js @@ -79,12 +79,10 @@ export default class AssessmentsAPI extends BaseCourseAPI { } /** - * Create an assessment attempt. + * Creates an assessment attempt. * * @param {number} assessmentId - * @return {Promise} - * success response: { redirectUrl: string } - * error response: { error: string } + * @returns {import('api/types').APIResponse} */ attempt(assessmentId) { return this.client.get(`${this.#urlPrefix}/${assessmentId}/attempt`); diff --git a/client/app/api/course/Video/Submissions.ts b/client/app/api/course/Video/Submissions.ts index 714c4026936..522109d670a 100644 --- a/client/app/api/course/Video/Submissions.ts +++ b/client/app/api/course/Video/Submissions.ts @@ -1,10 +1,12 @@ -import { AxiosResponse } from 'axios'; import { VideoEditSubmissionData, VideoSubmission, + VideoSubmissionAttemptData, VideoSubmissionData, } from 'types/course/video/submissions'; +import { APIResponse } from 'api/types'; + import BaseVideoAPI from './Base'; export default class SubmissionsAPI extends BaseVideoAPI { @@ -16,28 +18,39 @@ export default class SubmissionsAPI extends BaseVideoAPI { /** * Fetches a list of video submissions for a video in a course. */ - index(): Promise> { + index(): APIResponse { return this.client.get(this.#getUrlPrefix()); } /** * Fetch video submission in a course. */ - fetch(submissionId): Promise> { + fetch(submissionId: number): APIResponse { return this.client.get(`${this.#getUrlPrefix()}/${submissionId}`); } /** * Create a video submission in a course. */ - create(videoId: number): Promise> { + create(videoId: number): APIResponse { return this.client.post(`${this.#getUrlPrefix(videoId)}`); } /** * Fetch edit video submission in a course. */ - edit(submissionId): Promise> { + edit(submissionId: number): APIResponse { return this.client.get(`${this.#getUrlPrefix()}/${submissionId}/edit`); } + + /** + * Programmatically attempts to watch a video and get the submission URL. + * Created as a compatibility method for `NextVideoButton`. + * + * @param url URL in the form of `courses/:id/videos/:id/attempt` + * @returns + */ + attempt(url: string): APIResponse { + return this.client.get(url); + } } diff --git a/client/app/bundles/course/assessment/submission/actions/attachments.js b/client/app/bundles/course/assessment/submission/actions/attachments.js index aab34f0639e..c13990b6e47 100644 --- a/client/app/bundles/course/assessment/submission/actions/attachments.js +++ b/client/app/bundles/course/assessment/submission/actions/attachments.js @@ -1,4 +1,4 @@ -import AttachmentsAPI from 'api/Attachments'; +import attachmentsAPI from 'api/Attachments'; import actionTypes from '../constants'; @@ -6,7 +6,8 @@ export default function destroy(questionId, attachmentId) { return (dispatch) => { dispatch({ type: actionTypes.DELETE_ATTACHMENT_REQUEST }); - return AttachmentsAPI.delete(attachmentId) + return attachmentsAPI + .delete(attachmentId) .then((response) => response.data) .then(() => { dispatch({ diff --git a/client/app/bundles/course/courses/components/buttons/TodoAccessButton.tsx b/client/app/bundles/course/courses/components/buttons/TodoAccessButton.tsx index 28426c5dfaf..e1945d4c4bc 100644 --- a/client/app/bundles/course/courses/components/buttons/TodoAccessButton.tsx +++ b/client/app/bundles/course/courses/components/buttons/TodoAccessButton.tsx @@ -1,48 +1,22 @@ -import { FC } from 'react'; -import { injectIntl, WrappedComponentProps } from 'react-intl'; import { Button } from '@mui/material'; -import axios from 'lib/axios'; +import Link from 'lib/components/core/Link'; -interface Props extends WrappedComponentProps { +interface TodoAccessButtonProps { accessButtonText: string; accessButtonLink: string; - submissionUrl: string; - isVideo: boolean; - isNewAttempt: boolean; } -const TodoAccessButton: FC = (props) => { - const { - accessButtonText, - accessButtonLink, - submissionUrl, - isVideo, - isNewAttempt, - } = props; +const TodoAccessButton = (props: TodoAccessButtonProps): JSX.Element => { + const { accessButtonText, accessButtonLink } = props; + return ( - + + + ); }; -export default injectIntl(TodoAccessButton); +export default TodoAccessButton; diff --git a/client/app/bundles/course/courses/components/tables/PendingTodosTable.tsx b/client/app/bundles/course/courses/components/tables/PendingTodosTable.tsx index 63f78ff25ac..24daf3ba5a5 100644 --- a/client/app/bundles/course/courses/components/tables/PendingTodosTable.tsx +++ b/client/app/bundles/course/courses/components/tables/PendingTodosTable.tsx @@ -25,7 +25,6 @@ import { getSurveyResponseURL, getSurveyURL, getVideoAttemptURL, - getVideoSubmissionsURL, } from 'lib/helpers/url-builders'; import { getCourseId } from 'lib/helpers/url-helpers'; @@ -105,7 +104,7 @@ const PendingTodosTable: FC = (props) => { const renderButtons = (todo: TodoData): JSX.Element => { let accessButtonText = ''; let accessButtonLink = ''; - let submissionUrl; + // TODO: Refactor below by changing switch to dictionary switch (todoType) { case 'surveys': @@ -150,10 +149,6 @@ const PendingTodosTable: FC = (props) => { )}/${todo.itemActableSpecificId}/edit`; } - submissionUrl = getAssessmentSubmissionURL( - getCourseId(), - todo.itemActableId, - ); break; case 'videos': @@ -163,10 +158,6 @@ const PendingTodosTable: FC = (props) => { todo.itemActableId, ); - submissionUrl = getVideoSubmissionsURL( - getCourseId(), - todo.itemActableId, - ); break; default: break; @@ -182,13 +173,6 @@ const PendingTodosTable: FC = (props) => { { const [isLoading, setIsLoading] = useState(false); + if (!props.url) { return ( @@ -39,7 +40,8 @@ const NextVideoButton = (props) => { onClick={() => { setIsLoading(true); if (!props.nextVideoSubmissionExists) { - axios.get(props.url).then((response) => { + CourseAPI.video.submissions.attempt(props.url).then((response) => { + // TODO: Use `navigate` from `useNavigate` once bundle is ready. window.location.href = response.data.submissionUrl; }); } else { diff --git a/client/app/lib/axios.js b/client/app/lib/axios.js deleted file mode 100644 index 3aa2d4665ef..00000000000 --- a/client/app/lib/axios.js +++ /dev/null @@ -1,9 +0,0 @@ -import originAxios from 'axios'; - -import { csrfToken } from 'lib/helpers/server-context'; - -const headers = { Accept: 'application/json', 'X-CSRF-Token': csrfToken }; -const params = { format: 'json' }; - -const axios = originAxios.create({ headers, params }); -export default axios; diff --git a/client/app/lib/components/core/fields/CKEditorRichText.tsx b/client/app/lib/components/core/fields/CKEditorRichText.tsx index a7005ba9f48..327d00c8f97 100644 --- a/client/app/lib/components/core/fields/CKEditorRichText.tsx +++ b/client/app/lib/components/core/fields/CKEditorRichText.tsx @@ -5,7 +5,7 @@ import { CKEditor } from '@ckeditor/ckeditor5-react'; import { FormHelperText, InputLabel } from '@mui/material'; import { cyan } from '@mui/material/colors'; -import axios from 'lib/axios'; +import attachmentsAPI from 'api/Attachments'; import './CKEditor.css'; @@ -28,13 +28,9 @@ const uploadAdapter = (loader) => { return { upload: () => new Promise((resolve, reject) => { - const formData = new FormData(); - - loader.file.then((file) => { - formData.append('file', file); - formData.append('name', file.name); - axios - .post('/attachments', formData) + loader.file.then((file: File) => { + attachmentsAPI + .upload(file) .then((response) => response.data) .then((data) => { if (data.success) { From ad64b68adbbb5e119bfc23c04e564dc356385792 Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:17:53 +0800 Subject: [PATCH 080/173] feat(router): add video attempt loader --- .../app/bundles/course/video/attemptLoader.ts | 29 +++++++++++++++++++ client/app/types/course/video/submissions.ts | 5 ++++ 2 files changed, 34 insertions(+) create mode 100644 client/app/bundles/course/video/attemptLoader.ts diff --git a/client/app/bundles/course/video/attemptLoader.ts b/client/app/bundles/course/video/attemptLoader.ts new file mode 100644 index 00000000000..8694d7e40d5 --- /dev/null +++ b/client/app/bundles/course/video/attemptLoader.ts @@ -0,0 +1,29 @@ +import { LoaderFunction, Params, redirect } from 'react-router-dom'; +import { getIdFromUnknown } from 'utilities'; + +import CourseAPI from 'api/course'; + +const getSubmissionURL = ( + params: Params, + submissionId: number, +): string | null => { + const courseId = getIdFromUnknown(params?.courseId); + const videoId = getIdFromUnknown(params?.videoId); + if (!courseId || !videoId) return null; + + return `/courses/${courseId}/videos/${videoId}/submissions/${submissionId}/edit`; +}; + +const videoAttemptLoader: LoaderFunction = async ({ params }) => { + const videoId = getIdFromUnknown(params?.videoId); + if (!videoId) return redirect('/'); + + const { data } = await CourseAPI.video.submissions.create(videoId); + + const url = data.submissionUrl ?? getSubmissionURL(params, data.submissionId); + if (!url) return redirect('/'); + + return redirect(url); +}; + +export default videoAttemptLoader; diff --git a/client/app/types/course/video/submissions.ts b/client/app/types/course/video/submissions.ts index f55be382953..d8445caa34d 100644 --- a/client/app/types/course/video/submissions.ts +++ b/client/app/types/course/video/submissions.ts @@ -29,3 +29,8 @@ export interface VideoEditSubmissionData { videoDescription: string; videoData: object; } + +export interface VideoSubmissionAttemptData { + submissionId: number; + submissionUrl?: string; +} From 42336d03d30f6f011c260ac1557ad41b8999b9bc Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:18:06 +0800 Subject: [PATCH 081/173] feat(router): add assessment attempt loader --- .../app/bundles/course/assessment/attemptLoader.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 client/app/bundles/course/assessment/attemptLoader.ts diff --git a/client/app/bundles/course/assessment/attemptLoader.ts b/client/app/bundles/course/assessment/attemptLoader.ts new file mode 100644 index 00000000000..df95202d63d --- /dev/null +++ b/client/app/bundles/course/assessment/attemptLoader.ts @@ -0,0 +1,14 @@ +import { LoaderFunction, redirect } from 'react-router-dom'; +import { getIdFromUnknown } from 'utilities'; + +import CourseAPI from 'api/course'; + +const assessmentAttemptLoader: LoaderFunction = async ({ params }) => { + const assessmentId = getIdFromUnknown(params?.assessmentId); + if (!assessmentId) return redirect('/'); + + const { data } = await CourseAPI.assessment.assessments.attempt(assessmentId); + return redirect(data.redirectUrl); +}; + +export default assessmentAttemptLoader; From 81292a46c410a12a927306e71a1db10a7857188b Mon Sep 17 00:00:00 2001 From: Phillmont Muktar <51525686+purfectliterature@users.noreply.github.com> Date: Tue, 1 Aug 2023 18:19:41 +0800 Subject: [PATCH 082/173] feat(assessments): attempt buttons link to `/attempt` route --- .../AssessmentShow/AssessmentShowHeader.tsx | 34 ++-------------- .../pages/AssessmentsIndex/ActionButtons.tsx | 39 ++----------------- 2 files changed, 8 insertions(+), 65 deletions(-) diff --git a/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx b/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx index ecc4171f65e..f0ac60b7564 100644 --- a/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx +++ b/client/app/bundles/course/assessment/pages/AssessmentShow/AssessmentShowHeader.tsx @@ -1,4 +1,4 @@ -import { MouseEventHandler, useState } from 'react'; +import { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { Assessment, @@ -17,7 +17,7 @@ import { PromptText } from 'lib/components/core/dialogs/Prompt'; import toast from 'lib/hooks/toast'; import useTranslation from 'lib/hooks/useTranslation'; -import { attemptAssessment, deleteAssessment } from '../../operations'; +import { deleteAssessment } from '../../operations'; import translations from '../../translations'; import { ACTION_LABELS } from '../AssessmentsIndex/ActionButtons'; @@ -31,32 +31,8 @@ const AssessmentShowHeader = ( const { with: assessment } = props; const { t } = useTranslation(); const [deleting, setDeleting] = useState(false); - const [attempting, setAttempting] = useState(false); const navigate = useNavigate(); - const actionButtonUrl = - assessment.status === 'open' ? '#' : assessment.actionButtonUrl; - - const handleActionButton: MouseEventHandler = (e) => { - if (assessment.status !== 'open') return; - setAttempting(true); - e.preventDefault(); - e.stopPropagation(); - toast - .promise(attemptAssessment(assessment.id), { - pending: t(translations.attemptingAssessment), - success: t(translations.createSubmissionSuccessful), - error: { - render: ({ data }) => { - const error = (data as Error)?.message; - return t(translations.createSubmissionFailed, { error }); - }, - }, - }) - .then((data) => navigate(data.redirectUrl)) - .catch(() => setAttempting(false)); - }; - const handleDelete = (): Promise => { const deleteUrl = assessment.deleteUrl; if (!deleteUrl) @@ -139,13 +115,11 @@ const AssessmentShowHeader = ( )} - {actionButtonUrl && ( - + {assessment.actionButtonUrl && ( +