diff --git a/app/assets/stylesheets/_custom_bootstrap.scss b/app/assets/stylesheets/_custom_bootstrap.scss index 92af74a5..2173cbdc 100644 --- a/app/assets/stylesheets/_custom_bootstrap.scss +++ b/app/assets/stylesheets/_custom_bootstrap.scss @@ -1,3 +1,28 @@ +.flextensions-table, +.flextensions-table.dataTable { + // Common table headings for all tables + thead { + th, + th.dt-type-numeric, table.dataTable th.dt-type-date, table.dataTable td.dt-type-numeric, table.dataTable td.dt-type-date { + text-align: center; + font-weight: 600; + vertical-align: middle; + } + } + + tbody { + td { + vertical-align: middle; + } + } +} + +// For the requests table, we want to make the action buttons exactly the same size +// regardless of the icon used. +.action-btn i { + width: 12.5px; +} + .bd-placeholder-img { font-size: 1.125rem; text-anchor: middle; @@ -472,6 +497,33 @@ a.btn-outline-danger.disable:hover { // // BS-type table-responsive +// Column visibility dropdown - checkmarks for visible columns +.dt-button-collection { + .dt-button { + text-align: left !important; + padding-left: 2.2em !important; + position: relative; + + &.active, + &.dt-button-active { + background-color: transparent !important; + color: var(--bs-dropdown-link-color, inherit) !important; + + &:hover { + background-color: var(--bs-dropdown-link-hover-bg) !important; + color: var(--bs-dropdown-link-hover-color) !important; + } + + &::before { + content: '✓'; + position: absolute; + left: 0.75em; + font-weight: bold; + } + } + } +} + // Request Table desktop @media (min-width: 768px) { #requests-table #assignment { diff --git a/app/helpers/requests_helper.rb b/app/helpers/requests_helper.rb index d57b8fd5..79cb39d9 100644 --- a/app/helpers/requests_helper.rb +++ b/app/helpers/requests_helper.rb @@ -1,4 +1,8 @@ module RequestsHelper + def text_label_for(request) + "Request for #{request.user.try(:name) || 'N/A'} - #{request.assignment&.name || 'N/A'}" + end + def status_export_string(request) case request.status when 'pending' diff --git a/app/javascript/controllers/requests_controller.js b/app/javascript/controllers/requests_controller.js index 4b6d7278..21fd2d2d 100644 --- a/app/javascript/controllers/requests_controller.js +++ b/app/javascript/controllers/requests_controller.js @@ -17,8 +17,7 @@ export default class extends Controller { if (!DataTable.isDataTable('#requests-table')) { const searchQuery = this.element.dataset.searchQuery; - const readonlyToken = this.element.dataset.readonlyToken; - const courseId = this.element.dataset.courseId; + const controller = this; this.table = new DataTable('#requests-table', { paging: true, @@ -28,73 +27,43 @@ export default class extends Controller { responsive: true, columnDefs: [ { orderable: false, targets: 'no-sort' }, - { type: "date", targets: [5, 6, 7] } + { type: "date", targets: [5, 6, 7] }, + { visible: false, targets: [0] } ], order: [[5, "asc"]], layout: { topStart: { buttons: [ { - extend: 'collection', - text: 'Copy Google Sheets Import to Clipboard', - buttons: [ - { - text: 'All Requests', - className: 'dropdown-item', - action: function () { - const url = `${window.location.origin}/courses/${courseId}/requests/export.csv?readonly_api_token=${encodeURIComponent(readonlyToken)}`; - const formula = `=IMPORTDATA("${url}")`; - navigator.clipboard.writeText(formula); - } - }, - { - text: 'Pending Requests', - className: 'dropdown-item', - action: function () { - const url = `${window.location.origin}/courses/${courseId}/requests/export.csv?readonly_api_token=${encodeURIComponent(readonlyToken)}&status=pending`; - const formula = `=IMPORTDATA("${url}")`; - navigator.clipboard.writeText(formula); - } - } - ] + extend: 'colvis', + text: 'Columns', + columns: ':not(.no-sort)' }, { - extend: 'copy', - text: 'Copy Table to Clipboard', - title: null, - messageTop: null, - messageBottom: null, - info: false, - exportOptions: { - columns: ':visible:not(.no-sort)', - format: { - body: function (data, row, column, node) { - if (node && node.hasAttribute && node.hasAttribute('data-export')) { - return node.getAttribute('data-export'); - } - return data; - } + text: 'Batch Edit', + action: function (e, dt, node, config) { + const col = dt.column(0); + const willShow = !col.visible(); + col.visible(willShow); + + const batchActions = controller.element.querySelector('.batch-actions'); + if (batchActions) { + batchActions.classList.toggle('d-none', !willShow); } - } - }, - { - extend: 'csv', - text: 'Export as CSV', - filename: 'extension-requests', - exportOptions: { - columns: ':visible:not(.no-sort)', - format: { - body: function (data, row, column, node) { - if (node && node.hasAttribute && node.hasAttribute('data-export')) { - return node.getAttribute('data-export'); - } - return data; - } + + if (!willShow) { + controller._selectableCheckboxes().forEach((cb) => { cb.checked = false; }); + controller._syncSelectionControls(); } + + const btnEl = node.nodeType === 1 ? node : (node[0] || node); + btnEl.classList.toggle('active', willShow); + btnEl.innerHTML = willShow + ? 'Exit Batch Edit' + : 'Batch Edit'; } - }, - 'colvis' - ], + } + ] } } }); @@ -137,8 +106,8 @@ export default class extends Controller { async _submitSingleAction(button) { const url = button.dataset.url; - const btnGroup = button.closest('.btn-group'); - const allBtns = btnGroup ? btnGroup.querySelectorAll('button') : [button]; + const container = button.closest('.request-actions'); + const allBtns = container ? container.querySelectorAll('button') : [button]; allBtns.forEach((btn) => { btn.disabled = true; }); try { @@ -235,8 +204,8 @@ export default class extends Controller { statusTd.innerHTML = `${label}`; } - const btnGroup = tr.querySelector('.btn-group'); - if (btnGroup) btnGroup.remove(); + const requestActions = tr.querySelector('.request-actions'); + if (requestActions) requestActions.remove(); const checkbox = tr.querySelector('input[data-request-id]'); if (checkbox) { diff --git a/app/views/requests/instructor_index.html.erb b/app/views/requests/instructor_index.html.erb index 4274f8ef..92b8f4b8 100644 --- a/app/views/requests/instructor_index.html.erb +++ b/app/views/requests/instructor_index.html.erb @@ -4,7 +4,6 @@ data-controller="requests" data-search-query="<%= @search_query %>" data-show-all="<%= params[:show_all] == 'true' ? 'true' : 'false' %>" - data-readonly-token="<%= @course.readonly_api_token %>" data-course-id="<%= @course.id %>" data-mass-approve-url="<%= mass_approve_course_requests_path(@course, format: :json) %>" data-mass-reject-url="<%= mass_reject_course_requests_path(@course, format: :json) %>"> @@ -27,10 +26,10 @@ <% if @requests.any? %>
| - | Actions | -Name | -Assignment | -Student ID | -Requested At | -Original Due Date | -Requested Due Date | -# of Days | -Status | +Actions | +Name | +Assignment | +Student ID | +Requested At | +Original Due Date | +Requested Due Date | +# of Days | +Status |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| + |
<% if request.status == 'pending' %>
<% end %>
|
-
-
+
+ <% if request.status == 'pending' %>
+ |
+
+
+
+ <% else %>
<%= link_to course_request_path(@course, request), class: "btn btn-sm btn-primary", title: "View", aria: { label: "View request" } do %>
<% end %>
- <% if request.status == 'pending' %>
-
-
-
-
- <% end %>
-
+ <% end %>
<%= link_to request.user.try(:name) || 'N/A', course_request_path(@course, request) %>
@@ -143,7 +139,8 @@
| |