Skip to content
Closed
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
2 changes: 2 additions & 0 deletions gradebookng/bundle/src/main/bundle/gradebookng.properties
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ column.header.coursegrade = Course Grade

filter.students = Filter students
filter.studentsclear = Clear student filter
filter.items = Filter items
filter.itemsclear = Clear item filter
filter.groups = Filter by group/section

button.addgradeitem = Add Gradebook Item
Expand Down
2 changes: 2 additions & 0 deletions gradebookng/bundle/src/main/bundle/gradebookng_fr_FR.properties
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ column.header.coursegrade=Note pour le cours

filter.students=Filtrer les \u00e9tudiants
filter.studentsclear=Clear student filter
filter.items=Filtrer les \u00e9l\u00e9ments
filter.itemsclear=Effacer le filtre des \u00e9l\u00e9ments
filter.groups=Filtrer par section/groupe

button.addgradeitem=Ajouter \u00e9l\u00e9ment du bulletin de notes
Expand Down
9 changes: 9 additions & 0 deletions gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.html
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@
<li class="gb-student-summary"><!-- Populated by JavaScript --></li>
</ul>
<ul wicket:id="gbToolbarColumnTools" class="gb-toolbar-right my-2 d-flex flex-wrap list-unstyled align-items-center">
<li wicket:id="itemFilter" class="me-2 mb-1">
<div class="gb-student-filter gb-item-text-filter">
<label for="itemFilterInput" id="itemFilterLabel" class="sr-only"><wicket:message key="filter.items"/></label>
<input type="text" id="itemFilterInput" class="form-control" aria-controls="gradeTable" aria-labelledby="itemFilterLabel" wicket:message="placeholder:filter.items" />
<a class="gb-item-filter-clear-button" wicket:message="title:filter.itemsclear;aria-label:filter.itemsclear" href="javascript:void(0)">
<i class="fa fa-times-circle" aria-hidden="true"></i>
</a>
</div>
</li>
<li class="gb-grade-item-summary me-2 mb-1"><!-- Populated by JavaScript --></li>
<li class="me-2 mb-1">
<button wicket:id="toggleGradeItemsToolbarItem" id="toggleGradeItemsToolbarItem" class="button" aria-haspopup="true" aria-expanded="false" aria-controls="gradeItemsTogglePanel"><wicket:message key="label.toolbar.toggleitems"/></button>
Expand Down
26 changes: 20 additions & 6 deletions gradebookng/tool/src/java/org/sakaiproject/gradebookng/tool/pages/GradebookPage.java
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ public boolean isVisible() {

final WebMarkupContainer toolbarColumnTools = new WebMarkupContainer("gbToolbarColumnTools");
toolbarColumnTools.setVisible(this.hasGradebookItems);
final WebMarkupContainer itemFilter = new WebMarkupContainer("itemFilter");
itemFilter.setVisible(this.hasGradebookItems);
toolbarColumnTools.add(itemFilter);
toolbar.add(toolbarColumnTools);

final WebMarkupContainer toggleGradeItemsToolbarItem = new WebMarkupContainer("toggleGradeItemsToolbarItem");
Expand Down Expand Up @@ -677,15 +680,26 @@ public void renderHead(final IHeaderResponse response) {
response.render(JavaScriptHeaderItem
.forUrl(String.format("/gradebookng-tool/scripts/gradebook-connection-poll.js%s", version)));

final StringValue focusAssignmentId = getPageParameters().get(FOCUS_ASSIGNMENT_ID_PARAM);
final StringValue focusAssignmentIdParam = getPageParameters().get(FOCUS_ASSIGNMENT_ID_PARAM);
final StringValue showPopupForNewItem = getPageParameters().get(NEW_GBITEM_POPOVER_PARAM);
if(!showPopupForNewItem.isNull() && !focusAssignmentId.isNull() && this.hasStudents){
if (!focusAssignmentIdParam.isNull() && this.hasStudents) {
long focusAssignmentId = -1L;
try {
focusAssignmentId = focusAssignmentIdParam.toLong(-1L);
} catch (NumberFormatException e) {
focusAssignmentId = -1L;
}

getPageParameters().remove(FOCUS_ASSIGNMENT_ID_PARAM);
getPageParameters().remove(NEW_GBITEM_POPOVER_PARAM);
response.render(JavaScriptHeaderItem
.forScript(
String.format("GbGradeTable.focusColumnForAssignmentId(%s,%s)", focusAssignmentId.toString(),showPopupForNewItem),
null));

if (focusAssignmentId > 0L) {
final boolean showPopover = !showPopupForNewItem.isNull() && showPopupForNewItem.toBoolean(false);
response.render(JavaScriptHeaderItem
.forScript(
String.format("GbGradeTable.focusColumnForAssignmentId(%d,%s)", focusAssignmentId, showPopover),
null));
}
}
}

Expand Down
161 changes: 132 additions & 29 deletions gradebookng/tool/src/webapp/scripts/gradebook-gbgrade-table.js
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
GB_HIDDEN_ITEMS_KEY = portal.user.id + "#gradebook#hiddenitems";
GB_SCROLL_LEFT_KEY = portal.user.id + "#gradebook#scrollleft";

GbGradeTable = { _onReadyCallbacks: [] };
GbGradeTable.itemFilterText = "";

GbGradeTable.dropdownShownHandler = e => {

Expand Down Expand Up @@ -57,6 +59,13 @@ GbGradeTable.updateViewPreferences = function () {
});
};

GbGradeTable.persistScrollPosition = function() {
const tableContainer = document.querySelector(".tabulator-tableholder");
if (tableContainer) {
sessionStorage.setItem(GB_SCROLL_LEFT_KEY, tableContainer.scrollLeft);
}
};

var sakaiReminder = new SakaiReminder();

GbGradeTable.unpack = function (s, rowCount, columnCount) {
Expand Down Expand Up @@ -191,6 +200,35 @@ TrimPathFragmentCache.prototype.setHTML = function (target, values) {
GbGradeTable.replaceContents(target, this.getFragment(values));
}

GbGradeTable.buildItemFilterHaystacks = function() {
GbGradeTable.columns.forEach(column => {
const haystack = [
column?.title,
column?.abbrevTitle,
column?.name,
column?.categoryName,
].filter(Boolean).join(" ").toLowerCase();
column._itemFilterHaystack = haystack;
});
};
Comment on lines +203 to +213
Copy link
Member

Choose a reason for hiding this comment

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

@coderabbitai lets write columnSearchText or something else instead of haystack

Copy link
Contributor

Choose a reason for hiding this comment

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

@kunaljaykam, great suggestion! Using a more descriptive name like columnSearchText would definitely improve code readability. The term "haystack" is a metaphor (from "needle in a haystack"), but columnSearchText or searchableText makes the purpose clearer.

To keep the naming consistent, I'd recommend updating:

  1. Function name: buildItemFilterHaystacksbuildColumnSearchText (or buildItemFilterSearchText)
  2. Local variable: haystackcolumnSearchText
  3. Property name: column._itemFilterHaystackcolumn._searchText (or column._columnSearchText)
  4. Usage in matchesItemFilter: Update the reference from column?._itemFilterHaystack to column?._searchText

This would make the code more self-documenting and easier to understand for anyone reading it.


🧠 Learnings used
Learnt from: ottenhoff
Repo: sakaiproject/sakai PR: 0
File: :0-0
Timestamp: 2025-10-07T15:11:27.298Z
Learning: In samigo’s Total Scores view (samigo/samigo-app/src/webapp/jsf/evaluation/totalScores.jsp), mailto links were hidden after commit dee05746 (PR #12312, SAK-49674) added a render check requiring email.fromEmailAddress to be non-empty; PR #14154 (SAK-52058) restores visibility by checking only description.email.


GbGradeTable.matchesItemFilter = function(column) {
if (!GbGradeTable.itemFilterText) {
return true;
}

return (column?._itemFilterHaystack || "").indexOf(GbGradeTable.itemFilterText) !== -1;
};

GbGradeTable.isColumnHidden = function(column) {
return column.hidden || !GbGradeTable.matchesItemFilter(column);
};

GbGradeTable.setItemFilterText = function(text) {
GbGradeTable.itemFilterText = (text || "").toString().trim().toLowerCase();
GbGradeTable.redrawTable(true);
};

$(document).ready(function() {
// need TrimPath to load before parsing templates
GbGradeTable.templates = {
Expand Down Expand Up @@ -751,6 +789,7 @@ GbGradeTable.renderTable = function (elementId, tableData) {
GbGradeTable.columns = tableData.columns;
let hiddenItems = JSON.parse(sessionStorage.getItem(GB_HIDDEN_ITEMS_KEY)) || [];
GbGradeTable.columns.filter(c => hiddenItems.includes(c.assignmentId)).forEach(c => c.hidden = true);
GbGradeTable.buildItemFilterHaystacks();
GbGradeTable.settings = tableData.settings;
GbGradeTable.courseGradeId = tableData.courseGradeId;
GbGradeTable.gradebookId = tableData.gradebookId;
Expand Down Expand Up @@ -904,6 +943,16 @@ GbGradeTable.renderTable = function (elementId, tableData) {
}
});

// Restore horizontal scroll position if we stored it before a page refresh (eg, after saving an edited item)
const storedScrollLeft = sessionStorage.getItem(GB_SCROLL_LEFT_KEY);
if (storedScrollLeft !== null) {
const tableContainer = document.querySelector(".tabulator-tableholder");
if (tableContainer) {
tableContainer.scrollLeft = parseInt(storedScrollLeft, 10);
}
sessionStorage.removeItem(GB_SCROLL_LEFT_KEY);
}

GbGradeTable.instance.on("headerClick", (e, column) => {
if (e.target.classList.contains('gb-title')) {
const table = column.getTable();
Expand Down Expand Up @@ -1208,7 +1257,39 @@ GbGradeTable.renderTable = function (elementId, tableData) {
$input.val("").trigger("keyup");
}
});


let itemFilterTimeout;
let previousItemFilterText = "";
const $itemFilterInput = $("#itemFilterInput");
if ($itemFilterInput.length) {
$itemFilterInput
.on("keyup", function (event) {
clearTimeout(itemFilterTimeout);
itemFilterTimeout = setTimeout(function () {
const currentFilterText = event.target.value;
if (currentFilterText !== previousItemFilterText) {
previousItemFilterText = currentFilterText;
GbGradeTable.setItemFilterText(currentFilterText);
}
}, 300);
})
.on("keydown", function (event) {
if (event.key === "Enter") {
clearTimeout(itemFilterTimeout);
GbGradeTable.setItemFilterText(event.target.value);
return false;
}
});
}

$(document).on("click", ".gb-item-filter-clear-button", function (event) {
event.preventDefault();
const $input = $("#itemFilterInput");
if ($input.val().length > 0) {
$input.val("").trigger("keyup");
}
});

rubricGradingRow = 0;
rubricGradingCol = 0;

Expand Down Expand Up @@ -1240,6 +1321,9 @@ GbGradeTable.renderTable = function (elementId, tableData) {
.on("click", ".gb-dropdown-menu .edit-assignment-details", function () {
const cellElement = $(this).closest(".tabulator-cell, .tabulator-col");

// Persist current horizontal position so we can restore after the page reloads from the edit
GbGradeTable.persistScrollPosition();

GbGradeTable.ajax({
action: "editAssignment",
assignmentId: cellElement.data("assignment-id"),
Expand Down Expand Up @@ -1862,7 +1946,7 @@ GbGradeTable.getFilteredColumns = function() {

return GbGradeTable._fixedColumns.concat(
GbGradeTable.columns
.filter(col => !col.hidden)
.filter(col => !GbGradeTable.isColumnHidden(col))
.map((column, colIndex) => ({
field: (GbGradeTable._fixedColumns.length + colIndex).toString(),
formatter: GbGradeTable.cellFormatter,
Expand All @@ -1886,7 +1970,7 @@ GbGradeTable.getFilteredData = function() {
GbGradeTable.applyColumnFilter = function(data) {
for (var i=GbGradeTable.columns.length-1; i>=0; i--) {
var column = GbGradeTable.columns[i];
if (column.hidden) {
if (GbGradeTable.isColumnHidden(column)) {
for(var row=0; row<data.length; row++) {
data[row] = data[row]
.slice(0, i + GbGradeTable.FIXED_COLUMN_OFFSET)
Expand Down Expand Up @@ -2872,7 +2956,7 @@ GbGradeTable.refreshSummaryLabels = function() {
var visibleColumns = 0;
var totalColumns = GbGradeTable.columns.length;
$.each(GbGradeTable.columns, function(i, col) {
if (!col.hidden) {
if (!GbGradeTable.isColumnHidden(col)) {
visibleColumns = visibleColumns + 1;
}
});
Expand All @@ -2892,13 +2976,13 @@ GbGradeTable.refreshSummaryLabels = function() {
if (col.type === 'category') {
totalCategories += 1;

if (!col.hidden) {
if (!GbGradeTable.isColumnHidden(col)) {
visibleCategories += 1;
}
} else if (col.type === 'assignment') {
totalAssignments += 1;

if (!col.hidden) {
if (!GbGradeTable.isColumnHidden(col)) {
visibleAssignments += 1;
}
}
Expand Down Expand Up @@ -3138,30 +3222,49 @@ GbGradeTable.focusColumnForAssignmentId = function(assignmentId, showPopupForNew
if (assignmentId) {
GbGradeTable.addReadyCallback(function() {
const col = GbGradeTable.colForAssignment(assignmentId);
const cell = GbGradeTable.instance.getRows()[0].getCells()[col];

if (showPopupForNewItem === true) {
GbGradeTable.instance.scrollToColumn(col, "end", false);

const $selectedField = $(cell.getElement());
setTimeout(() => GbGradeTable.instance.addRange(cell, cell), 100);
$selectedField.attr('data-bs-toggle','popover');
$selectedField.attr('data-bs-placement','top');
$selectedField.attr('data-bs-container','body');
$selectedField.attr('data-bs-content',GbGradeTable.templates['newGradeItemPopoverMessage'].process());
$selectedField.attr('data-bs-title',GbGradeTable.templates['newGradeItemPopoverTitle'].process());
$selectedField.attr('data-bs-template','<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="mt-0 popover-header"></h3><div class="popover-body p-2"></div></div>');

$('body, button').on('click keyup touchend', function (e) {
if ($(e.target).data("bs-toggle") !== 'popover'
&& $(e.target).parents('.popover.in').length === 0) {
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(el => {
bootstrap.Popover.getInstance(el)?.hide();
});
}
})
new bootstrap.Popover($selectedField[0]).show();
const rows = GbGradeTable.instance.getRows();

if (col < 0) {
return;
}

GbGradeTable.instance.scrollToColumn(col, "end", false);

const firstRow = rows && rows.length ? rows[0] : null;
if (!firstRow) {
return;
}

setTimeout(() => {
const cell = firstRow.getCells()[col];
if (!cell) {
return;
}

GbGradeTable.instance.addRange(cell, cell);
cell.getElement().focus();

if (showPopupForNewItem === true) {

const $selectedField = $(cell.getElement());
$selectedField.attr('data-bs-toggle','popover');
$selectedField.attr('data-bs-placement','top');
$selectedField.attr('data-bs-container','body');
$selectedField.attr('data-bs-content',GbGradeTable.templates['newGradeItemPopoverMessage'].process());
$selectedField.attr('data-bs-title',GbGradeTable.templates['newGradeItemPopoverTitle'].process());
$selectedField.attr('data-bs-template','<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="mt-0 popover-header"></h3><div class="popover-body p-2"></div></div>');

$('body, button').on('click keyup touchend', function (e) {
if ($(e.target).data("bs-toggle") !== 'popover'
&& $(e.target).parents('.popover.in').length === 0) {
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(el => {
bootstrap.Popover.getInstance(el)?.hide();
});
}
})
new bootstrap.Popover($selectedField[0]).show();
}
}, 200);
});
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,36 @@
right: 8px;
color: var(--sakai-text-color-dimmed);
}
.gb-item-text-filter {
position: relative;
}
.gb-item-text-filter input {
padding-right: 32px;
}
.gb-item-text-filter .gb-item-filter-clear-button {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: none;
background: transparent;
color: var(--sakai-primary-color-1);
padding: 0;
}
@media (max-width: 768px) {
.gb-item-text-filter .gb-item-filter-clear-button {
width: 44px;
height: 44px;
}
.gb-item-text-filter input {
padding-right: 56px;
}
}
.gb-grade-item-summary .gb-item-summary-counts.warn-items-hidden,
.gb-student-summary .gb-student-summary-counts.warn-students-hidden {
color: var(--warnBanner-color);
Expand Down