Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions app/assets/stylesheets/_custom_bootstrap.scss
Original file line number Diff line number Diff line change
@@ -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;
}

Comment on lines +20 to +25
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crap, this does nothing and should be deleted.

Suggested change
// 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;
Expand Down Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions app/helpers/requests_helper.rb
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
93 changes: 31 additions & 62 deletions app/javascript/controllers/requests_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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: '<i class="fas fa-columns me-1"></i>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: '<i class="fas fa-edit me-1"></i>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
? '<i class="fas fa-times me-1"></i>Exit Batch Edit'
: '<i class="fas fa-edit me-1"></i>Batch Edit';
}
},
'colvis'
],
}
]
}
}
});
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -235,8 +204,8 @@ export default class extends Controller {
statusTd.innerHTML = `<span class="badge ${badgeClass}">${label}</span>`;
}

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) {
Expand Down
118 changes: 80 additions & 38 deletions app/views/requests/instructor_index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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) %>">
Expand All @@ -27,32 +26,32 @@

<% if @requests.any? %>
<div class="table-responsive">
<table class="table table-bordered table-striped datatable"
<table class="table table-bordered table-striped datatable flextensions-table"
id="requests-table">
<thead>
<tr class="table-info">
<thead class="table-blue-medium text-white">
<tr>
<th class="text-center align-content-center no-sort" style="min-width: 60px;" data-priority="1">
<input type="checkbox"
id="select-all-requests"
data-requests-target="selectAllCheckbox"
data-action="change->requests#toggleSelectAll"
aria-label="Select all pending requests">
</th>
<th class="text-center align-content-center no-sort" style="min-width: 100px;" data-priority="1">Actions</th>
<th class="text-center align-content-center" id="name" data-priority="5">Name</th>
<th class="text-center align-content-center" id="assignment" style="min-width: 120px; max-width: 200px;" data-priority="2">Assignment</th>
<th class="text-center align-content-center" id="student-id" data-priority="9">Student ID</th>
<th class="text-center align-content-center" style="min-width: 198px;" data-priority="7">Requested At</th>
<th class="text-center align-content-center" style="min-width: 198px;" data-priority="8">Original Due Date</th>
<th class="text-center align-content-center" style="min-width: 198px;" data-priority="4">Requested Due Date</th>
<th class="text-center align-content-center" style="min-width: 90px;" data-priority="6"># of Days</th>
<th class="text-center align-content-center" style="min-width: 200px;" data-priority="3">Status</th>
<th style="width: 10ch;" data-priority="1" class="no-sort">Actions</th>
<th id="name" data-priority="5">Name</th>
<th id="assignment" style="min-width: 120px; max-width: 200px;" data-priority="2">Assignment</th>
<th style="width: 10ch;" data-priority="9" class="no-sort" >Student ID</th>
<th style="width: 32ch;" data-priority="7">Requested At</th>
<th style="width: 32ch;" data-priority="8">Original Due Date</th>
<th style="width: 32ch;" data-priority="4">Requested Due Date</th>
<th style="width: 14ch;" data-priority="6"># of Days</th>
<th style="min-width: 16ch;" data-priority="3">Status</th>
</tr>
</thead>
<tbody>
<% @requests.each do |request| %>
<tr>
<td class="text-center align-content-center">
<td>
<% if request.status == 'pending' %>
<div class="form-check form-switch mx-auto" style="width: 45px;">
<input class="form-check-input" type="checkbox"
Expand All @@ -66,32 +65,29 @@
</div>
<% end %>
</td>
<td class="text-center align-content-center">
<div class="d-flex flex-column align-items-center gap-1">
<td class="text-center align-content-center" style="width: 12ch">
<% if request.status == 'pending' %>
<div class="gap-1 request-actions">
<button type="button"
class="btn btn-sm btn-success"
data-action="click->requests#approve"
data-url="<%= approve_course_request_path(@course, request, format: :json) %>"
aria-label="Approve <%= text_label_for(request) %>">
<i class="fas fa-fw fa-check"></i>
</button>
<button type="button"
class="btn btn-sm btn-danger"
data-action="click->requests#reject"
data-url="<%= reject_course_request_path(@course, request, format: :json) %>"
aria-label="Deny <%= text_label_for(request) %>">
<i class="fas fa-fw fa-times"></i>
</button>
</div>
<% else %>
<%= link_to course_request_path(@course, request), class: "btn btn-sm btn-primary", title: "View", aria: { label: "View request" } do %>
<i class="fas fa-arrow-right"></i>
<% end %>
<% if request.status == 'pending' %>
<div class="btn-group flex-column">
<button type="button"
class="btn btn-sm btn-success"
data-action="click->requests#approve"
data-url="<%= approve_course_request_path(@course, request, format: :json) %>"
title="Approve"
aria-label="Approve request">
<i class="fas fa-check"></i>
</button>
<button type="button"
class="btn btn-sm btn-danger mt-1"
data-action="click->requests#reject"
data-url="<%= reject_course_request_path(@course, request, format: :json) %>"
title="Reject"
aria-label="Reject request">
<i class="fas fa-times"></i>
</button>
</div>
<% end %>
</div>
<% end %>
</td>
<td class="text-center align-content-center">
<%= link_to request.user.try(:name) || 'N/A', course_request_path(@course, request) %>
Expand Down Expand Up @@ -143,7 +139,8 @@
</tbody>
</table>
</div>
<div class="row mt-2">

<div class="row mt-2 batch-actions d-none">
<div class="col-12 text-start">
<button type="button"
class="btn btn-sm btn-success me-2"
Expand All @@ -161,6 +158,51 @@
</button>
</div>
</div>

<% export_url = export_course_requests_url(@course, format: :csv, readonly_api_token: @course.readonly_api_token) %>

<div class="d-flex gap-2 my-3">
<div class="dropdown">
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Copy to Google Sheets <i class="fa fa-table"></i>
</button>
<ul class="dropdown-menu">
<li>
<button class="dropdown-item" type="button"
data-sheets-url="<%= export_url %>">All Requests</button>
</li>
<li>
<button class="dropdown-item" type="button"
data-sheets-url="<%= export_url %>&status=pending">Pending Requests</button>
</li>
</ul>
</div>

<div class="dropdown">
<button class="btn btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Export CSV <i class="fa fa-file-csv"></i>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="<%= export_url %>">All Requests</a>
</li>
<li>
<a class="dropdown-item" href="<%= export_url %>&status=pending">Pending Requests</a>
</li>
</ul>
</div>
</div>

<script>
document.querySelectorAll('[data-sheets-url]').forEach(btn => {
btn.addEventListener('click', () => {
const formula = `=IMPORTDATA("${btn.dataset.sheetsUrl}")`;
navigator.clipboard.writeText(formula).then(() => {
alert("Copied a Google Sheets IMPORT function to the clipboard. Please paste this in your Google sheet and click 'Allow'.\n\nRemember not to share this with anyone outside your course staff.");
});
});
});
</script>
<% else %>
<div class="alert alert-warning">There are no current requests.</div>
<% end %>
Expand Down
Loading