From 4ead9d5bad822214ff2e767b9266cc47552d96bf Mon Sep 17 00:00:00 2001 From: Splines <37160523+Splines@users.noreply.github.com> Date: Thu, 30 May 2024 21:47:51 +0200 Subject: [PATCH] Redesign lecture edit page for lecturers (#628) * Upgrade Rails to v7.1 and run `bundle update` See the upgrade guide here: https://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html * Use older version of `html-parser` for `thredded` See https://github.com/thredded/thredded/issues/979 * Use new `config.autoload_lib` in Rails 7.1 See https://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#config-autoload-lib-and-config-autoload-lib-once Eager loading is on by default for production. * Remove unused app environment variables usage The file `config/app_environment_variables.rb` does not exist in our codebase anymore. * Run `bin/rails app:update` to update configurations * Add new framework defaults for Rails 7.1 file * Update `listen` gem version This was done because `bin/rails app:update` failed with: ** Execute app:update:active_storage rails active_storage:update bin/rails aborted! Gem::LoadError: can't activate listen (~> 3.5), already activated listen-3.0.8. Make sure all dependencies are added to Gemfile. * Add TODO note for upcoming serialize change * Reduce new framework defaults list * Add migrations introduced by rails update task * Remove unneeded ActiveStorage migrations * Remove defaults for sha-256 as we are unaffected * Use new Rails 7.1 defaults * Fix TODO rubocop warning * Update bundler version to 2.5.9 You can do so locally via `bundle update --bundler` * Remove unnecessary entries in `Gemfile.lock` Performed automatically via `bundle install`. * Address `Passing the coder as positional arg` deprecation This is a followup to https://github.com/rails/rails/pull/47463 * add yaml coder explicitly for serializing arrays * Migrate from globalize to mobility due to serialization warnings * Update gem lockfile to include `mobility` `bundle install` also removed globalize automatically for us. * Add `I18nLocaleAccessors` as replacement for `globalize_attribute_names` * Remove obsolete comment regarding `globalize` * Fix Rails `secrets` deprecation warning (Devise) This is due to https://github.com/heartcombo/devise/issues/5644. * Use `install_folder` in cypress on rails `cypress_folder` is deprecated as config option * Init dummy Bootstrap nav pills * Group accordion items into nav pane * Style pillars & improve accessibility * Remove accordion wrappers & design lecture content pane * Center lectures header & improve vertical alignment * Add margin to bottom of lecture pane * Internationalize lectures navbar headers * Decaffeinate `lectures.coffee` via local CLI of decaffeinate see https://decaffeinate-project.org/ * Format `lectures.js` according to ESLint * Remove unnecessary use of Array.from * Use shorter variations of null checks * Remove unnecessary Coffeescript comment * Fix ESLint errors * Make better use of JS function syntax * Configure url hashes for bootstrap tabs Might also be known as "deep linking". * Simplify url hash update logic * Remove unused variable `s` * Use focus listener (not click listener) for accessibility * Implement many small UI improvements in lectures * Re-initialize masonry grid system for lecture content * Remove unnecessary spacing * Add confirmation dialog to delete forum * Add scrollbar to announcements list if too long * Redirect to correct page after creating a new announcement * Redirect to correct page after "Forum" actions * Redirect to correct page after "Comments" actions * Increase bottom margin of lecture pane * Check if errors are present to avoid nil error * Fix valid_annotations_status include check * Only load lectures_admin js related code when needed We also perform an early return if we no erdbeere examples are searched for, i.e. when the element is not yet visible on the page. * Use icons for save/cancel in assignments table * Fix structures cancel button (erdbeere) * Improve positioning of "structures" text * Get rid of unused debug message * Fix import of media for lectures not working * Remove TODO note * Delete unused tags/modal partial rendering * Stay on subpage upon save action * Fix broken browser navigation * Fix weird masonry grid system bug * Wait until tab content is shown before setting up grid system --------- Co-authored-by: fosterfarrell9 <28628554+fosterfarrell9@users.noreply.github.com> --- .config/eslint.mjs | 1 + app/assets/javascripts/application.js | 1 + app/assets/javascripts/lectures.coffee | 351 --------------- app/assets/javascripts/lectures.js | 414 ++++++++++++++++++ app/assets/javascripts/lectures_admin.js | 45 ++ app/assets/javascripts/masonry_grid.js | 12 + app/assets/stylesheets/lectures.scss | 51 ++- app/controllers/announcements_controller.rb | 2 +- app/controllers/lectures_controller.rb | 23 +- app/javascript/packs/application.js | 7 - app/models/lecture.rb | 2 +- app/views/assignments/_form.html.erb | 18 +- .../lectures/edit/_announcements.html.erb | 63 +-- app/views/lectures/edit/_assignments.html.erb | 209 ++++----- app/views/lectures/edit/_comments.html.erb | 165 +++---- app/views/lectures/edit/_erdbeere.html.erb | 39 +- app/views/lectures/edit/_form.html.erb | 215 +++++++-- app/views/lectures/edit/_forum.html.erb | 86 ++-- app/views/lectures/edit/_header.html.erb | 10 +- .../lectures/edit/_imported_media.html.erb | 87 ++-- .../edit/_organizational_concept.html.erb | 140 +++--- app/views/lectures/edit/_people.html.erb | 203 ++++----- app/views/lectures/edit/_preferences.html.erb | 320 +++++++------- app/views/lectures/edit/_structures.html.erb | 21 +- app/views/lectures/edit/_tags.html.erb | 107 ++--- app/views/lectures/edit/_tutorials.html.erb | 104 ++--- app/views/lectures/edit_structures.coffee | 1 + app/views/lectures/update.coffee | 4 + app/views/tutorials/_form.html.erb | 2 +- config/locales/de.yml | 4 + config/locales/en.yml | 4 + 31 files changed, 1385 insertions(+), 1326 deletions(-) delete mode 100644 app/assets/javascripts/lectures.coffee create mode 100644 app/assets/javascripts/lectures.js create mode 100644 app/assets/javascripts/lectures_admin.js create mode 100644 app/assets/javascripts/masonry_grid.js diff --git a/.config/eslint.mjs b/.config/eslint.mjs index f75205cf7..008e58374 100644 --- a/.config/eslint.mjs +++ b/.config/eslint.mjs @@ -33,6 +33,7 @@ const customGlobals = { // Common global methods initBootstrapPopovers: "readable", + initMasonryGridSystem: "readable", // Thyme & Annotation tool globals // TODO: This is a "hack" right now to get rid of "xy is not defined" error diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 49b7e76e6..88e205048 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -35,6 +35,7 @@ //= require lectures //= require lessons //= require main +//= require masonry_grid //= require media //= require notifications //= require profile diff --git a/app/assets/javascripts/lectures.coffee b/app/assets/javascripts/lectures.coffee deleted file mode 100644 index 6e8d60759..000000000 --- a/app/assets/javascripts/lectures.coffee +++ /dev/null @@ -1,351 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ - -disableExceptOrganizational = -> - $('#lecture-organizational-warning').show() - $('.fa-edit').hide() - $('.new-in-lecture').hide() - $('[data-bs-toggle="collapse"]').prop('disabled', true).removeClass('clickable') - return - -$(document).on 'turbolinks:load', -> - initBootstrapPopovers() - # if any input is given to the lecture form (for people in lecture), - # disable other input - $('#lecture-form :input').on 'change', -> - $('#lecture-basics-warning').show() - $('.fa-edit:not(#update-teacher-button,#update-editors-button)').hide() - $('.new-in-lecture').hide() - $('[data-bs-toggle="collapse"]').prop('disabled', true).removeClass('clickable') - return - - # if any input is given to the preferences form, disable other input - $('#lecture-preferences-form :input').on 'change', -> - $('#lecture-preferences-warning').show() - $('[data-bs-toggle="collapse"]').prop('disabled', true).removeClass('clickable') - $('.fa-edit').hide() - $('.new-in-lecture').hide() - return - - # if any input is given to the comments form, disable other input - $('#lecture-comments-form :input').on 'change', -> - $('#lecture-comments-warning').show() - $('[data-bs-toggle="collapse"]').prop('disabled', true).removeClass('clickable') - $('.fa-edit').hide() - $('.new-in-lecture').hide() - return - - # if any input is given to the assignments form, disable other input - $('#lecture-assignments-form :input').on 'change', -> - $('#lecture-assignments-warning').show() - $('[data-bs-toggle="collapse"]').prop('disabled', true).removeClass('clickable') - $('.new-in-lecture').hide() - return - - # if any input is given to the organizational form, disable other input - $('#lecture-organizational-form :input').on 'change', -> - disableExceptOrganizational() - return - - trixElement = document.querySelector('#lecture-concept-trix') - if trixElement? - content = trixElement.dataset.content - editor = trixElement.editor - editor.setSelectedRange([0,65535]) - editor.deleteInDirection("forward") - editor.insertHTML(content) - document.activeElement.blur() - trixElement.addEventListener 'trix-change', -> - disableExceptOrganizational() - return - - # if absolute numbering box is checked/unchecked, enable/disable selection of - # start section - $('#lecture_absolute_numbering').on 'change', -> - if $(this).prop('checked') - $('#lecture_start_section').prop('disabled', false) - else - $('#lecture_start_section').prop('disabled', true) - return - - # reload current page if lecture basics editing is cancelled - $('#lecture-basics-cancel').on 'click', -> - location.reload(true) - return - - # reload current page if lecture preferences editing is cancelled - $('#cancel-lecture-preferences').on 'click', -> - location.reload(true) - return - - # reload current page if lecture comments editing is cancelled - $('#cancel-lecture-comments').on 'click', -> - location.reload(true) - return - - # reload current page if lecture preferences editing is cancelled - $('#cancel-lecture-organizational').on 'click', -> - location.reload(true) - return - - # restore assignments form if lecture assignments editing is cancelled - $('#cancel-lecture-assignments').on 'click', -> - $('#lecture-assignments-warning').hide() - $('[data-bs-toggle="collapse"]').prop('disabled', false).addClass('clickable') - $('.new-in-lecture').show() - maxSize = $('#lecture_submission_max_team_size').data('value') - $('#lecture_submission_max_team_size').val(maxSize) - gracePeriod = $('#lecture_submission_grace_period').data('value') - $('#lecture_submission_grace_period').val(gracePeriod) - return - - # hide the media tab if hide media button is clicked - $('#hide-media-button').on 'click', -> - $('#lecture-media-card').hide() - $('#lecture-content-card').removeClass('col-xxl-9') - $('#show-media-button').show() - return - - # display the media tab if show media button is clicked - $('#show-media-button').on 'click', -> - $('#lecture-content-card').addClass('col-xxl-9') - $('#lecture-media-card').show() - $('#show-media-button').hide() - return - - # mousenter over a medium -> colorize lessons and tags - $('[id^="lecture-medium_"]').on 'mouseenter', -> - if this.dataset.type == 'Lesson' - lessonId = this.dataset.id - $('.lecture-lesson[data-id="'+lessonId+'"]') - .removeClass('bg-secondary') - .addClass('bg-info') - tags = $(this).data('tags') - for t in tags - $('.lecture-tag[data-id="'+t+'"]').removeClass('bg-light') - .addClass('bg-warning') - return - - # mouseleave over lesson -> restore original color of lessons and tags - $('[id^="lecture-medium_"]').on 'mouseleave', -> - if this.dataset.type == 'Lesson' - lessonId = this.dataset.id - $('.lecture-lesson[data-id="'+lessonId+'"]').removeClass('bg-info') - .addClass('bg-secondary') - tags = $(this).data('tags') - for t in tags - $('.lecture-tag[data-id="'+t+'"]').removeClass('bg-warning') - return - - # mouseenter over lesson -> colorize tags - $('[id^="lecture-lesson_"]').on 'mouseenter', -> - tags = $(this).data('tags') - for t in tags - $('.lecture-tag[data-id="'+t+'"]').addClass('bg-warning') - return - - # mouseleave over lesson -> restore original color of tags - $('[id^="lecture-lesson_"]').on 'mouseleave', -> - tags = $(this).data('tags') - for t in tags - $('.lecture-tag[data-id="'+t+'"]').removeClass('bg-warning') - return - - # mouseenter over tag -> colorize lessons - $('[id^="lecture-tag_"]').on 'mouseenter', -> - lessons = $(this).data('lessons') - for l in lessons - $('.lecture-lesson[data-id="'+l+'"]').removeClass('bg-secondary') - .addClass('bg-info') - return - - # mouseleave over tag -> restore original color of lessons - $('[id^="lecture-tag_"]').on 'mouseleave', -> - lessons = $(this).data('lessons') - for l in lessons - $('.lecture-lesson[data-id="'+l+'"]').removeClass('bg-info') - .addClass('bg-secondary') - return - - $('#edited-media-tab a[data-bs-toggle="tab"]').on 'shown.bs.tab', (e) -> - sort = e.target.dataset.sort # newly activated tab - path = $('#create-new-medium').prop('href') - if path - new_path = path.replace(/\?sort=.+?&/, '?sort=' + sort + '&') - $('#create-new-medium').prop('href', new_path) - return - - userModalContent = document.getElementById('lectureUserModalContent') - if userModalContent? and userModalContent.dataset.filled == 'false' - lectureId = userModalContent.dataset.lecture - $.ajax Routes.show_subscribers_path(lectureId), - type: 'GET' - dataType: 'json' - data: { - lecture: lectureId - } - success: (result) -> - $('#lectureUserModalButton').hide() if result.length == 0 - for u in result - row = document.createElement('div') - row.className = 'row mx-2 border-left border-right border-bottom' - colName = document.createElement('div') - colName.className = 'col-6' - colName.innerHTML = u[0] - row.appendChild(colName) - colMail = document.createElement('div') - colMail.className = 'col-6' - colMail.innerHTML = u[1] - row.appendChild(colMail) - userModalContent.appendChild(row) - userModalContent.dataset.filled = 'true' - return - - # on small mobile display, use shortened tag badges and - # shortened course titles - mobileDisplay = -> - $('.tagbadge').hide() - $('.courseMenuItem').hide() - $('.tagbadgeshort').show() - $('.courseMenuItemShort').show() - $('#secondnav').show() - $('#lecturesDropdown').appendTo($('#secondnav')) - $('#notificationDropdown').appendTo($('#secondnav')) - $('#feedback-btn').appendTo($('#secondnav')) - $('#searchField').appendTo($('#secondnav')) - $('#second-admin-nav').show() - $('#adminDetails').appendTo($('#second-admin-nav')) - $('#adminUsers').appendTo($('#second-admin-nav')) - $('#adminProfile').appendTo($('#second-admin-nav')) - $('#teachableDrop').prependTo($('#second-admin-nav')) - $('#adminMain').css('flex-direction', 'row') - $('#adminHome').css('padding-right', '0.5rem') - $('#adminCurrentLecture').css('padding-right', '0.5rem') - $('#adminSearch').css('padding-right', '0.5rem') - $('#mampfbrand').hide() - return - - # on large display, use normal tag badges and course titles - largeDisplay = -> - $('.tagbadge').show() - $('.courseMenuItem').show() - $('.tagbadgeshort').hide() - $('.courseMenuItemShort').hide() - $('#secondnav').hide() - $('#lecturesDropdown').appendTo($('#firstnav')) - $('#notificationDropdown').appendTo($('#firstnav')) - $('#feedback-btn').appendTo($('#firstnav')) - $('#searchField').appendTo($('#firstnav')) - $('#second-admin-nav').hide() - $('#teachableDrop').appendTo($('#first-admin-nav')) - $('#adminDetails').appendTo($('#first-admin-nav')) - $('#adminUsers').appendTo($('#first-admin-nav')) - $('#adminProfile').appendTo($('#first-admin-nav')) - $('#adminMain').removeAttr('style') - $('#adminHome').removeAttr('style') - $('#adminCurrentLecture').removeAttr('style') - $('#adminSearch').removeAttr('style') - $('#mampfbrand').show() - return - - # highlight tagbadges if screen is very small - if window.matchMedia("screen and (max-width: 767px)").matches - mobileDisplay() - - if window.matchMedia("screen and (max-device-width: 767px)").matches - mobileDisplay() - - # mediaQuery listener for very small screens - match_verysmall = window.matchMedia("screen and (max-width: 767px)") - match_verysmall.addListener (result) -> - if result.matches - mobileDisplay() - return - - match_verysmalldevice = window.matchMedia("screen and (max-device-width: 767px)") - match_verysmalldevice.addListener (result) -> - if result.matches - mobileDisplay() - return - - # mediaQuery listener for normal screens - match_normal = window.matchMedia("screen and (min-width: 768px)") - match_normal.addListener (result) -> - if result.matches - largeDisplay() - return - - match_normal = window.matchMedia("screen and (min-device-width: 768px)") - match_normal.addListener (result) -> - if result.matches - largeDisplay() - return - - $('#erdbeere_structures_heading').on 'click', -> - lectureId = $(this).data('lecture') - loading = $(this).data('loading') - $('#erdbeereStructuresBody').empty().append(loading) - $.ajax Routes.edit_structures_path(lectureId), - type: 'GET' - dataType: 'script' - return - - $lectureStructures = $('#lectureStructuresInfo') - if $lectureStructures.length > 0 - structures = $lectureStructures.data('structures') - for s in structures - $('#structure-item-' + s).show() - - $('#switchGlobalStructureSearch').on 'click', -> - if $(this).is(':checked') - $('[id^="structure-item-"]').show() - else - $('[id^="structure-item-"]').hide() - structures = $lectureStructures.data('structures') - for s in structures - $('#structure-item-' + s).show() - return - - $(document).on 'change', '#lecture_course_id', -> - $('#lecture_term_id').removeClass('is-invalid') - $('#new-lecture-term-error').empty() - courseId = parseInt($(this).val()) - termInfo = $(this).data('terminfo').filter (x) -> x[0] == courseId - console.log termInfo[0] - if (termInfo[0]?) - if termInfo[0][1] - $('#newLectureTerm').hide() - $('#lecture_term_id').prop('disabled', true) - $('#newLectureSort').hide() - else - $('#newLectureTerm').show() - $('#lecture_term_id').prop('disabled', false) - $('#newLectureSort').show() - return - - $(document).on 'change', '#medium_publish_media_0', -> - $('[id^="medium_released_"]').attr('disabled', true) - $('#access-text').css('color','grey') - return - - $(document).on 'change', '#medium_publish_media_1', -> - $('[id^="medium_released_"]').attr('disabled', false) - $('#access-text').css('color','') - return - - $('#import_sections').on 'change', -> - if $(this).prop('checked') - $('#import_tags').prop('disabled', false) - else - $('#import_tags').prop('disabled', true).prop('checked', false) - return - - return - -# clean up everything before turbolinks caches -$(document).on 'turbolinks:before-cache', -> - $('.lecture-tag').removeClass('bg-warning') - $('.lecture-lesson').removeClass('bg-info').addClass('bg-secondary') - $(document).off 'change', '#lecture_course_id' - return diff --git a/app/assets/javascripts/lectures.js b/app/assets/javascripts/lectures.js new file mode 100644 index 000000000..ab7ba4479 --- /dev/null +++ b/app/assets/javascripts/lectures.js @@ -0,0 +1,414 @@ +// Load example data (erdbeere) for structures +function loadExampleStructures() { + const structuresBody = $("#erdbeereStructuresBody"); + const lectureId = structuresBody.data("lecture"); + if (lectureId === undefined) { + return; + } + const loading = structuresBody.data("loading"); + structuresBody.empty().append(loading); + $.ajax(Routes.edit_structures_path(lectureId), { + type: "GET", + dataType: "script", + complete: () => { + $("#erdbeere-structures-cancel").on("click", () => { + loadExampleStructures(); + }); + }, + }); +}; + +// eslint-disable-next-line no-unused-vars +function registerErdbeereExampleChanges() { + // Erdbeere Examples unsaved changes warning + $("#lecture-structures-form").on("input", function () { + $("#lecture-erdbeere-examples-warning").show(); + }); + + $("#erdbeere-structures-cancel").on("click", function () { + $("#lecture-erdbeere-examples-warning").hide(); + }); +} + +function disableExceptOrganizational() { + $("#lecture-organizational-warning").show(); + $(".fa-edit").hide(); + $(".new-in-lecture").hide(); + $('[data-bs-toggle="collapse"]').prop("disabled", true).removeClass("clickable"); +}; + +$(document).on("turbolinks:load", function () { + $("#delete-forum").on("click", () => { + const sureToDeleteMsg = $("#delete-forum").data("sureToDelete"); + const reallyDelete = confirm(sureToDeleteMsg); + return reallyDelete; + }); + + // if any input is given to the lecture form (for people in lecture), + // disable other input + $("#lecture-form :input").on("change", function () { + $("#lecture-basics-warning").show(); + $(".fa-edit:not(#update-teacher-button,#update-editors-button)").hide(); + $(".new-in-lecture").hide(); + $('[data-bs-toggle="collapse"]').prop("disabled", true).removeClass("clickable"); + }); + + // if any input is given to the preferences form, disable other input + $("#lecture-preferences-form :input").on("change", function () { + $("#lecture-preferences-warning").show(); + $('[data-bs-toggle="collapse"]').prop("disabled", true).removeClass("clickable"); + $(".fa-edit").hide(); + $(".new-in-lecture").hide(); + }); + + // if any input is given to the comments form, disable other input + $("#lecture-comments-form :input").on("change", function () { + $("#lecture-comments-warning").show(); + $('[data-bs-toggle="collapse"]').prop("disabled", true).removeClass("clickable"); + $(".fa-edit").hide(); + $(".new-in-lecture").hide(); + }); + + // if any input is given to the assignments form, disable other input + $("#lecture-assignments-form :input").on("change", function () { + $("#lecture-assignments-warning").show(); + $('[data-bs-toggle="collapse"]').prop("disabled", true).removeClass("clickable"); + $(".new-in-lecture").hide(); + }); + + // if any input is given to the organizational form, disable other input + $("#lecture-organizational-form :input").on("change", function () { + disableExceptOrganizational(); + }); + + const trixElement = document.querySelector("#lecture-concept-trix"); + if (trixElement) { + const { content } = trixElement.dataset; + const { editor } = trixElement; + editor.setSelectedRange([0, 65535]); + editor.deleteInDirection("forward"); + editor.insertHTML(content); + document.activeElement.blur(); + trixElement.addEventListener("trix-change", function () { + disableExceptOrganizational(); + }); + } + + // if absolute numbering box is checked/unchecked, enable/disable selection of + // start section + $("#lecture_absolute_numbering").on("change", function () { + if ($(this).prop("checked")) { + $("#lecture_start_section").prop("disabled", false); + } + else { + $("#lecture_start_section").prop("disabled", true); + } + }); + + // reload current page if lecture basics editing is cancelled + $("#lecture-basics-cancel").on("click", function () { + location.reload(true); + }); + + // reload current page if lecture preferences editing is cancelled + $("#cancel-lecture-preferences").on("click", function () { + location.reload(true); + }); + + // reload current page if lecture comments editing is cancelled + $("#cancel-lecture-comments").on("click", function () { + location.reload(true); + }); + + // reload current page if lecture preferences editing is cancelled + $("#cancel-lecture-organizational").on("click", function () { + location.reload(true); + }); + + // restore assignments form if lecture assignments editing is cancelled + $("#cancel-lecture-assignments").on("click", function () { + $("#lecture-assignments-warning").hide(); + $('[data-bs-toggle="collapse"]').prop("disabled", false).addClass("clickable"); + $(".new-in-lecture").show(); + const maxSize = $("#lecture_submission_max_team_size").data("value"); + $("#lecture_submission_max_team_size").val(maxSize); + const gracePeriod = $("#lecture_submission_grace_period").data("value"); + $("#lecture_submission_grace_period").val(gracePeriod); + }); + + // hide the media tab if hide media button is clicked + $("#hide-media-button").on("click", function () { + $("#lecture-media-card").hide(); + $("#lecture-content-card").removeClass("col-xxl-9"); + $("#show-media-button").show(); + }); + + // display the media tab if show media button is clicked + $("#show-media-button").on("click", function () { + $("#lecture-content-card").addClass("col-xxl-9"); + $("#lecture-media-card").show(); + $("#show-media-button").hide(); + }); + + // mousenter over a medium -> colorize lessons and tags + $('[id^="lecture-medium_"]').on("mouseenter", function () { + if (this.dataset.type === "Lesson") { + const lessonId = this.dataset.id; + $('.lecture-lesson[data-id="' + lessonId + '"]') + .removeClass("bg-secondary") + .addClass("bg-info"); + } + const tags = $(this).data("tags"); + for (const t of tags) { + $('.lecture-tag[data-id="' + t + '"]').removeClass("bg-light") + .addClass("bg-warning"); + } + }); + + // mouseleave over lesson -> restore original color of lessons and tags + $('[id^="lecture-medium_"]').on("mouseleave", function () { + if (this.dataset.type === "Lesson") { + const lessonId = this.dataset.id; + $('.lecture-lesson[data-id="' + lessonId + '"]').removeClass("bg-info") + .addClass("bg-secondary"); + } + const tags = $(this).data("tags"); + for (const t of tags) { + $('.lecture-tag[data-id="' + t + '"]').removeClass("bg-warning"); + } + }); + + // mouseenter over lesson -> colorize tags + $('[id^="lecture-lesson_"]').on("mouseenter", function () { + const tags = $(this).data("tags"); + for (const t of tags) { + $('.lecture-tag[data-id="' + t + '"]').addClass("bg-warning"); + } + }); + + // mouseleave over lesson -> restore original color of tags + $('[id^="lecture-lesson_"]').on("mouseleave", function () { + const tags = $(this).data("tags"); + for (const t of tags) { + $('.lecture-tag[data-id="' + t + '"]').removeClass("bg-warning"); + } + }); + + // mouseenter over tag -> colorize lessons + $('[id^="lecture-tag_"]').on("mouseenter", function () { + const lessons = $(this).data("lessons"); + for (const l of lessons) { + $('.lecture-lesson[data-id="' + l + '"]').removeClass("bg-secondary") + .addClass("bg-info"); + } + }); + + // mouseleave over tag -> restore original color of lessons + $('[id^="lecture-tag_"]').on("mouseleave", function () { + const lessons = $(this).data("lessons"); + for (const l of lessons) { + $('.lecture-lesson[data-id="' + l + '"]').removeClass("bg-info") + .addClass("bg-secondary"); + } + }); + + $('#edited-media-tab a[data-bs-toggle="tab"]').on("shown.bs.tab", function (e) { + const { + sort, + } = e.target.dataset; // newly activated tab + const path = $("#create-new-medium").prop("href"); + if (path) { + const new_path = path.replace(/\?sort=.+?&/, "?sort=" + sort + "&"); + $("#create-new-medium").prop("href", new_path); + } + }); + + const userModalContent = document.getElementById("lectureUserModalContent"); + if (userModalContent && (userModalContent.dataset.filled === "false")) { + const lectureId = userModalContent.dataset.lecture; + $.ajax(Routes.show_subscribers_path(lectureId), { + type: "GET", + dataType: "json", + data: { + lecture: lectureId, + }, + success(result) { + if (result.length === 0) { + $("#lectureUserModalButton").hide(); + } + for (const res of result) { + var row = document.createElement("div"); + row.className = "row mx-2 border-left border-right border-bottom"; + var colName = document.createElement("div"); + colName.className = "col-6"; + colName.innerHTML = res[0]; + row.appendChild(colName); + var colMail = document.createElement("div"); + colMail.className = "col-6"; + colMail.innerHTML = res[1]; + row.appendChild(colMail); + userModalContent.appendChild(row); + userModalContent.dataset.filled = "true"; + } + }, + }, + ); + } + + // on small mobile display, use shortened tag badges and + // shortened course titles + const mobileDisplay = function () { + $(".tagbadge").hide(); + $(".courseMenuItem").hide(); + $(".tagbadgeshort").show(); + $(".courseMenuItemShort").show(); + $("#secondnav").show(); + $("#lecturesDropdown").appendTo($("#secondnav")); + $("#notificationDropdown").appendTo($("#secondnav")); + $("#feedback-btn").appendTo($("#secondnav")); + $("#searchField").appendTo($("#secondnav")); + $("#second-admin-nav").show(); + $("#adminDetails").appendTo($("#second-admin-nav")); + $("#adminUsers").appendTo($("#second-admin-nav")); + $("#adminProfile").appendTo($("#second-admin-nav")); + $("#teachableDrop").prependTo($("#second-admin-nav")); + $("#adminMain").css("flex-direction", "row"); + $("#adminHome").css("padding-right", "0.5rem"); + $("#adminCurrentLecture").css("padding-right", "0.5rem"); + $("#adminSearch").css("padding-right", "0.5rem"); + $("#mampfbrand").hide(); + }; + + // on large display, use normal tag badges and course titles + const largeDisplay = function () { + $(".tagbadge").show(); + $(".courseMenuItem").show(); + $(".tagbadgeshort").hide(); + $(".courseMenuItemShort").hide(); + $("#secondnav").hide(); + $("#lecturesDropdown").appendTo($("#firstnav")); + $("#notificationDropdown").appendTo($("#firstnav")); + $("#feedback-btn").appendTo($("#firstnav")); + $("#searchField").appendTo($("#firstnav")); + $("#second-admin-nav").hide(); + $("#teachableDrop").appendTo($("#first-admin-nav")); + $("#adminDetails").appendTo($("#first-admin-nav")); + $("#adminUsers").appendTo($("#first-admin-nav")); + $("#adminProfile").appendTo($("#first-admin-nav")); + $("#adminMain").removeAttr("style"); + $("#adminHome").removeAttr("style"); + $("#adminCurrentLecture").removeAttr("style"); + $("#adminSearch").removeAttr("style"); + $("#mampfbrand").show(); + }; + + // highlight tagbadges if screen is very small + if (window.matchMedia("screen and (max-width: 767px)").matches) { + mobileDisplay(); + } + + if (window.matchMedia("screen and (max-device-width: 767px)").matches) { + mobileDisplay(); + } + + // mediaQuery listener for very small screens + const match_verysmall = window.matchMedia("screen and (max-width: 767px)"); + match_verysmall.addListener(function (result) { + if (result.matches) { + mobileDisplay(); + } + }); + + const match_verysmalldevice = window.matchMedia("screen and (max-device-width: 767px)"); + match_verysmalldevice.addListener(function (result) { + if (result.matches) { + mobileDisplay(); + } + }); + + // mediaQuery listener for normal screens + let match_normal = window.matchMedia("screen and (min-width: 768px)"); + match_normal.addListener(function (result) { + if (result.matches) { + largeDisplay(); + } + }); + + match_normal = window.matchMedia("screen and (min-device-width: 768px)"); + match_normal.addListener(function (result) { + if (result.matches) { + largeDisplay(); + } + }); + + let structures; + loadExampleStructures(); + + const $lectureStructures = $("#lectureStructuresInfo"); + if ($lectureStructures.length > 0) { + structures = $lectureStructures.data("structures"); + for (const s of structures) { + $("#structure-item-" + s).show(); + } + } + + $("#switchGlobalStructureSearch").on("click", function () { + if ($(this).is(":checked")) { + $('[id^="structure-item-"]').show(); + } + else { + $('[id^="structure-item-"]').hide(); + structures = $lectureStructures.data("structures"); + for (const s of structures) { + $("#structure-item-" + s).show(); + } + } + }); + + $(document).on("change", "#lecture_course_id", function () { + $("#lecture_term_id").removeClass("is-invalid"); + $("#new-lecture-term-error").empty(); + const courseId = parseInt($(this).val()); + const termInfo = $(this).data("terminfo").filter(x => x[0] === courseId); + console.log(termInfo[0]); + if (termInfo[0]) { + if (termInfo[0][1]) { + $("#newLectureTerm").hide(); + $("#lecture_term_id").prop("disabled", true); + $("#newLectureSort").hide(); + } + else { + $("#newLectureTerm").show(); + $("#lecture_term_id").prop("disabled", false); + $("#newLectureSort").show(); + } + return; + } + }); + + $(document).on("change", "#medium_publish_media_0", function () { + $('[id^="medium_released_"]').attr("disabled", true); + $("#access-text").css("color", "grey"); + }); + + $(document).on("change", "#medium_publish_media_1", function () { + $('[id^="medium_released_"]').attr("disabled", false); + $("#access-text").css("color", ""); + }); + + $("#import_sections").on("change", function () { + if ($(this).prop("checked")) { + $("#import_tags").prop("disabled", false); + } + else { + $("#import_tags").prop("disabled", true).prop("checked", false); + } + }); +}); + +// clean up everything before turbolinks caches +$(document).on("turbolinks:before-cache", function () { + $(".lecture-tag").removeClass("bg-warning"); + $(".lecture-lesson").removeClass("bg-info").addClass("bg-secondary"); + $(document).off("change", "#lecture_course_id"); +}); diff --git a/app/assets/javascripts/lectures_admin.js b/app/assets/javascripts/lectures_admin.js new file mode 100644 index 000000000..6ec62b780 --- /dev/null +++ b/app/assets/javascripts/lectures_admin.js @@ -0,0 +1,45 @@ +/** + * Takes care of the URL hashes, such that each bootstrab tab gets assigned + * its own URL hash, e.g. "#assignments". + * + * This is necessary to be able to share the URL with a specific tab open. + * It also allows to stay on the same tab after a page reload + * (which is done when an edit action is saved/canceled, + * also see the lectures controller update action). + * + * Find out more details in this guide: + * https://webdesign.tutsplus.com/how-to-add-deep-linking-to-the-bootstrap-4-tabs-component--cms-31180t + */ +function configureUrlHashesForBootstrapTabs() { + $('#lecture-nav-pills button[role="tab"]').on("focus", function () { + const hash = $(this).attr("href"); + const urlWithoutHash = location.href.split("#")[0]; + const newUrl = `${urlWithoutHash}${hash}`; + history.pushState(null, "", newUrl); + }); +} + +function navigateToActiveNavTab() { + if (location.hash) { + const hrefXPathIdentifier = `button[href="${location.hash}"]`; + $(`#lecture-nav-pills ${hrefXPathIdentifier}`).tab("show"); + } + else { + $("#lecture-nav-content").focus(); + } +} + +$(document).on("ready turbolinks:load", function () { + initBootstrapPopovers(); + configureUrlHashesForBootstrapTabs(); + navigateToActiveNavTab(); + + // Reinitialize the masonry grid system when the lecture content is shown + $("#lecture-nav-content").on("shown.bs.tab", () => { + initMasonryGridSystem(); + }); +}); + +$(window).on("hashchange", function () { + navigateToActiveNavTab(); +}); diff --git a/app/assets/javascripts/masonry_grid.js b/app/assets/javascripts/masonry_grid.js new file mode 100644 index 000000000..9e0826029 --- /dev/null +++ b/app/assets/javascripts/masonry_grid.js @@ -0,0 +1,12 @@ +$(document).on("turbolinks:load", function () { + initMasonryGridSystem(); +}); + +/** + * Inits the masonry grid system for elements with the class "masonry-grid". + */ +function initMasonryGridSystem() { + $(".masonry-grid").masonry({ + percentPosition: true, + }); +} diff --git a/app/assets/stylesheets/lectures.scss b/app/assets/stylesheets/lectures.scss index 6e31dd03d..2ee04b58b 100644 --- a/app/assets/stylesheets/lectures.scss +++ b/app/assets/stylesheets/lectures.scss @@ -73,4 +73,53 @@ &:hover { color: white !important; } -} \ No newline at end of file +} + +#lecture-nav-pills { + background-color: white; + justify-content: center; + padding-top: 1em; + padding-bottom: 1em; + + box-shadow: 0px 2px 7px 0rem rgba(130,26,59,0.2); + border: #821A3B solid 1px; + border-radius: 0.4em; + margin-bottom: 1.5em; +} + +.lecture-nav-pill-button { + --bs-nav-link-color: #821A3B; + --bs-nav-link-hover-color: #8d1c40; + --bs-nav-pills-link-active-bg: #821A3B; + + &:focus-visible { + box-shadow: 0 0 0 0.25rem rgba(130, 26, 59, 0.25); + } +} + + +.lecture-pane { + background-color: white; + padding: 1.5em; + margin-bottom: 6em; + box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2); + border: gray 1px solid; + border-radius: 0.4em; +} + +.lecture-pane-separator { + border-top: 3px dotted #555555; + margin-top: 2em; + margin-bottom: 2em; +} + +h3.lecture-pane-header { + color: #838383; + font-size: 1.3em; +} + +#announcements-list { + max-height: 17em; + overflow-x: hidden; + overflow-y: auto; +} diff --git a/app/controllers/announcements_controller.rb b/app/controllers/announcements_controller.rb index 8425893a6..d225d27b2 100644 --- a/app/controllers/announcements_controller.rb +++ b/app/controllers/announcements_controller.rb @@ -37,7 +37,7 @@ def create redirect_to announcements_path return end - redirect_to edit_lecture_path(@announcement.lecture) + redirect_to "#{edit_lecture_path(@announcement.lecture)}#communication" return end @errors = @announcement.errors[:details].join(", ") diff --git a/app/controllers/lectures_controller.rb b/app/controllers/lectures_controller.rb index 10682539f..719f59540 100644 --- a/app/controllers/lectures_controller.rb +++ b/app/controllers/lectures_controller.rb @@ -120,7 +120,16 @@ def update end @lecture.touch @lecture.forum&.update(name: @lecture.forum_title) - redirect_to edit_lecture_path(@lecture) if @lecture.valid? + + # Redirect to the correct subpage + if @lecture.valid? + if params[:subpage].present? + redirect_to "#{edit_lecture_path(@lecture)}##{params[:subpage]}" + else + redirect_to edit_lecture_path(@lecture) + end + end + @errors = @lecture.errors end @@ -150,28 +159,28 @@ def add_forum forum.save @lecture.update(forum_id: forum.id) if forum.valid? end - redirect_to edit_lecture_path(@lecture) + redirect_to "#{edit_lecture_path(@lecture)}#communication" end # lock forum for this lecture def lock_forum @lecture.forum.update(locked: true) if @lecture.forum? @lecture.touch - redirect_to edit_lecture_path(@lecture) + redirect_to "#{edit_lecture_path(@lecture)}#communication" end # unlock forum for this lecture def unlock_forum @lecture.forum.update(locked: false) if @lecture.forum? @lecture.touch - redirect_to edit_lecture_path(@lecture) + redirect_to "#{edit_lecture_path(@lecture)}#communication" end # destroy forum for this lecture def destroy_forum @lecture.forum.destroy if @lecture.forum? @lecture.update(forum_id: nil) - redirect_to edit_lecture_path(@lecture) + redirect_to "#{edit_lecture_path(@lecture)}#communication" end # show all announcements for this lecture @@ -241,12 +250,12 @@ def close_comments @lecture.lessons.each do |lesson| lesson.media.update(annotations_status: -1) end - redirect_to edit_lecture_path(@lecture) + redirect_to "#{edit_lecture_path(@lecture)}#communication" end def open_comments @lecture.open_comments!(current_user) - redirect_to edit_lecture_path(@lecture) + redirect_to "#{edit_lecture_path(@lecture)}#communication" end def search diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 0586df987..889b776c1 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -43,11 +43,4 @@ document.addEventListener("turbolinks:load", function () { // DO not uncomment, evil // widget.reset(); } - - // Init Masonry grid system - // see https://getbootstrap.com/docs/5.0/examples/masonry/ - // and official documentation: https://masonry.desandro.com/ - $(".masonry-grid").masonry({ - percentPosition: true, - }); }); diff --git a/app/models/lecture.rb b/app/models/lecture.rb index 4ad4a39a5..7fd197471 100644 --- a/app/models/lecture.rb +++ b/app/models/lecture.rb @@ -841,7 +841,7 @@ def stale? end def valid_annotations_status? - [-1, 1].include?(annotations_status) + [0, 1].include?(annotations_status) end private diff --git a/app/views/assignments/_form.html.erb b/app/views/assignments/_form.html.erb index 0eec8828a..c79387994 100644 --- a/app/views/assignments/_form.html.erb +++ b/app/views/assignments/_form.html.erb @@ -69,13 +69,17 @@