|
|
@@ -612,12 +612,63 @@
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ // --- Multi-select owner filter (persisted in localStorage) -------------
|
|
|
+
|
|
|
+ const ownerFilterKey = 'sp:' + sprintId + ':ownerFilter';
|
|
|
+ /** @type {Set<string>} */
|
|
|
+ const ownerFilterSet = (function () {
|
|
|
+ try {
|
|
|
+ const raw = window.localStorage.getItem(ownerFilterKey);
|
|
|
+ if (raw) {
|
|
|
+ const arr = JSON.parse(raw);
|
|
|
+ if (Array.isArray(arr)) { return new Set(arr.map(String)); }
|
|
|
+ }
|
|
|
+ } catch (_) { /* ignore */ }
|
|
|
+ return new Set();
|
|
|
+ })();
|
|
|
+
|
|
|
+ function persistOwnerFilter() {
|
|
|
+ try {
|
|
|
+ window.localStorage.setItem(ownerFilterKey, JSON.stringify(Array.from(ownerFilterSet)));
|
|
|
+ } catch (_) { /* ignore quota / private mode */ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateOwnerFilterUi() {
|
|
|
+ // Reflect state back into the checkboxes.
|
|
|
+ $root.find('[data-owner-filter-opt]').each(function () {
|
|
|
+ $(this).prop('checked', ownerFilterSet.has(String($(this).val())));
|
|
|
+ });
|
|
|
+ // Count label on the trigger.
|
|
|
+ const n = ownerFilterSet.size;
|
|
|
+ $root.find('[data-owner-filter-count]').text(n === 0 ? '' : '(' + n + ')');
|
|
|
+ }
|
|
|
+
|
|
|
+ $root.on('change', '[data-owner-filter-opt]', function () {
|
|
|
+ const v = String($(this).val());
|
|
|
+ if ($(this).is(':checked')) { ownerFilterSet.add(v); } else { ownerFilterSet.delete(v); }
|
|
|
+ persistOwnerFilter();
|
|
|
+ updateOwnerFilterUi();
|
|
|
+ applyFilters();
|
|
|
+ });
|
|
|
+
|
|
|
+ $root.on('click', '[data-owner-filter-clear]', function () {
|
|
|
+ ownerFilterSet.clear();
|
|
|
+ persistOwnerFilter();
|
|
|
+ updateOwnerFilterUi();
|
|
|
+ applyFilters();
|
|
|
+ });
|
|
|
+
|
|
|
+ $root.on('click', '[data-owner-filter-trigger]', function (ev) {
|
|
|
+ ev.stopPropagation();
|
|
|
+ $root.find('[data-columns-dropdown]').addClass('hidden');
|
|
|
+ $root.find('[data-owner-filter-dropdown]').toggleClass('hidden');
|
|
|
+ });
|
|
|
+
|
|
|
// --- Filters (search / prio / owner) ----------------------------------
|
|
|
|
|
|
function applyFilters() {
|
|
|
- const q = String($root.find('[data-task-search]').val() || '').trim().toLowerCase();
|
|
|
- const prio = String($root.find('[data-prio-filter]').val() || '');
|
|
|
- const owner = String($root.find('[data-owner-filter]').val() || '');
|
|
|
+ const q = String($root.find('[data-task-search]').val() || '').trim().toLowerCase();
|
|
|
+ const prio = String($root.find('[data-prio-filter]').val() || '');
|
|
|
let visibleCount = 0;
|
|
|
|
|
|
$taskTbody.children('tr[data-task-row]').each(function () {
|
|
|
@@ -625,15 +676,13 @@
|
|
|
const title = String($row.find('[data-title]').val() || $row.find('[data-title]').text() || '').toLowerCase();
|
|
|
const rowPrio = String($row.attr('data-prio'));
|
|
|
const rowOwner = String($row.attr('data-owner') || '');
|
|
|
+ // The filter set treats "" (no owner) as the special "__none__" key.
|
|
|
+ const ownerKey = rowOwner === '' ? '__none__' : rowOwner;
|
|
|
|
|
|
let ok = true;
|
|
|
if (q !== '' && !title.includes(q)) { ok = false; }
|
|
|
if (prio !== '' && rowPrio !== prio) { ok = false; }
|
|
|
- if (owner === '__none__') {
|
|
|
- if (rowOwner !== '') { ok = false; }
|
|
|
- } else if (owner !== '' && rowOwner !== owner) {
|
|
|
- ok = false;
|
|
|
- }
|
|
|
+ if (ownerFilterSet.size > 0 && !ownerFilterSet.has(ownerKey)) { ok = false; }
|
|
|
|
|
|
$row.toggle(ok);
|
|
|
if (ok) { visibleCount++; }
|
|
|
@@ -648,7 +697,63 @@
|
|
|
clearTimeout(searchDebounce);
|
|
|
searchDebounce = setTimeout(applyFilters, 120);
|
|
|
});
|
|
|
- $root.on('change', '[data-prio-filter], [data-owner-filter]', applyFilters);
|
|
|
+ $root.on('change', '[data-prio-filter]', applyFilters);
|
|
|
+
|
|
|
+ // --- Column visibility toggle (persisted in localStorage) --------------
|
|
|
+
|
|
|
+ const columnsKey = 'sp:' + sprintId + ':hiddenCols';
|
|
|
+ /** @type {Set<string>} */
|
|
|
+ const hiddenCols = (function () {
|
|
|
+ try {
|
|
|
+ const raw = window.localStorage.getItem(columnsKey);
|
|
|
+ if (raw) {
|
|
|
+ const arr = JSON.parse(raw);
|
|
|
+ if (Array.isArray(arr)) { return new Set(arr.map(String)); }
|
|
|
+ }
|
|
|
+ } catch (_) { /* ignore */ }
|
|
|
+ return new Set();
|
|
|
+ })();
|
|
|
+
|
|
|
+ function persistHiddenCols() {
|
|
|
+ try {
|
|
|
+ window.localStorage.setItem(columnsKey, JSON.stringify(Array.from(hiddenCols)));
|
|
|
+ } catch (_) { /* ignore */ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function applyColumnVisibility() {
|
|
|
+ // Tailwind's .hidden is display:none; works on table cells.
|
|
|
+ $root.find('[data-col]').each(function () {
|
|
|
+ const col = String($(this).attr('data-col'));
|
|
|
+ $(this).toggleClass('hidden', hiddenCols.has(col));
|
|
|
+ });
|
|
|
+ // Reflect state into the Columns dropdown checkboxes.
|
|
|
+ $root.find('[data-column-opt]').each(function () {
|
|
|
+ $(this).prop('checked', !hiddenCols.has(String($(this).val())));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ $root.on('change', '[data-column-opt]', function () {
|
|
|
+ const v = String($(this).val());
|
|
|
+ if ($(this).is(':checked')) { hiddenCols.delete(v); } else { hiddenCols.add(v); }
|
|
|
+ persistHiddenCols();
|
|
|
+ applyColumnVisibility();
|
|
|
+ });
|
|
|
+
|
|
|
+ $root.on('click', '[data-columns-trigger]', function (ev) {
|
|
|
+ ev.stopPropagation();
|
|
|
+ $root.find('[data-owner-filter-dropdown]').addClass('hidden');
|
|
|
+ $root.find('[data-columns-dropdown]').toggleClass('hidden');
|
|
|
+ });
|
|
|
+
|
|
|
+ // Close either dropdown when clicking outside.
|
|
|
+ $(document).on('click', function (ev) {
|
|
|
+ if ($(ev.target).closest('[data-owner-filter-root]').length === 0) {
|
|
|
+ $root.find('[data-owner-filter-dropdown]').addClass('hidden');
|
|
|
+ }
|
|
|
+ if ($(ev.target).closest('[data-columns-root]').length === 0) {
|
|
|
+ $root.find('[data-columns-dropdown]').addClass('hidden');
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
// --- Column sort (client-side) ----------------------------------------
|
|
|
|
|
|
@@ -726,6 +831,11 @@
|
|
|
recomputeRow(parseInt($(this).data('sw-id'), 10));
|
|
|
});
|
|
|
recomputeSumMax();
|
|
|
+
|
|
|
+ // Restore persisted task-list UI state BEFORE applyFilters so hidden
|
|
|
+ // columns don't briefly flash in before being toggled off.
|
|
|
+ updateOwnerFilterUi();
|
|
|
+ applyColumnVisibility();
|
|
|
applyFilters();
|
|
|
|
|
|
})(jQuery);
|