JS: pill-popover interactive module + Choices.js CDN in report.html
Main interactive layer for the inline-filters feature. Appends two
blocks to report.html (inside {% block content %}, before the final
{% endblock %}):
1. Choices.js CDN <link> + <script> (admin-only gated, SRI-hashed) —
moved here because Task 5 will delete _report_config_modal.html,
which previously loaded the CDN. Keeping this on the report page
directly means the pills stay functional after modal retirement.
2. A scoped IIFE that wires up the three filter pills into an
interactive, state-managed UI:
- Click pill -> open popover (lazy-inits Choices.js on first open)
- Click outside / Esc / other pill -> close
- OK commits popover's local edits into pending state; dirty pills
get the orange outline + pulsing dot; Apply button slides in
- Cross-filter: picking projects auto-removes now-invalid teams
with toast notice ("Removed Team X — no logs on selected
projects"), and vice versa. Scope = entire history.
- Apply -> rebuilds querystring from pending state + navigates
(full page reload, same URL scheme as the retired modal)
- Reset -> reverts all pills to URL-current values
XSS-safe throughout: textContent and createElement; no innerHTML with
user data. Matches the pattern in base.html's work-log-payroll modal
from the Work-Log Payroll feature.
Graceful fallback: if Choices.js CDN fails to load, the module bails
early with a console warning; native <select multiple> still works
inside the popovers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b52ae47257
commit
6d2c72f6d1
@ -525,4 +525,442 @@
|
||||
"New Report" here opens the familiar config screen without
|
||||
navigating away. -->
|
||||
{% include 'core/_report_config_modal.html' %}
|
||||
|
||||
{# === CHOICES.JS CDN — admin-only === #}
|
||||
{# Loaded here because the pill popovers enhance their <select multiple> #}
|
||||
{# elements with Choices.js. Task 5 will delete _report_config_modal.html #}
|
||||
{# which currently also loads this CDN; having it here keeps the pills #}
|
||||
{# functional after retirement. Falls back to native multi-select if CDN #}
|
||||
{# fails. SRI hashes match the ones used in the retired modal. #}
|
||||
{% if user.is_staff or user.is_superuser %}
|
||||
<link rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/choices.js@10.2.0/public/assets/styles/choices.min.css"
|
||||
integrity="sha384-9oHz8X4XgvL+WkhPjPTMHviP0FM/eWUHWFmAVXKJ3PnbIK8Vi2ranPMgb0LZhaeQ"
|
||||
crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/choices.js@10.2.0/public/assets/scripts/choices.min.js"
|
||||
integrity="sha384-9r5e85TmdjVjyjYzZAV3TG5A6tcrmD7JjNBGfT2r1wp9txUPttent/DMiMuOwRNG"
|
||||
crossorigin="anonymous"
|
||||
defer></script>
|
||||
{% endif %}
|
||||
|
||||
{# === INLINE FILTERS — PILL POPOVER MODULE === #}
|
||||
{# Scoped IIFE; runs once on DOMContentLoaded. Manages popover open/close, #}
|
||||
{# dirty state, cross-filter, Apply submission. XSS-safe: createElement + #}
|
||||
{# textContent only (no innerHTML with user data). Matches CLAUDE.md pattern. #}
|
||||
{% if user.is_staff or user.is_superuser %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// --- Bail if Choices.js isn't loaded (graceful fallback) ---
|
||||
if (typeof Choices === 'undefined') {
|
||||
console.warn('[inline-filters] Choices.js not loaded; pills will not enhance');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Context from json_script tags (safe-decoded by browser JSON parser) ---
|
||||
var pairsEl = document.getElementById('projectTeamPairs');
|
||||
var urlProjectsEl = document.getElementById('urlSelectedProjectIds');
|
||||
var urlTeamsEl = document.getElementById('urlSelectedTeamIds');
|
||||
var pairs = pairsEl ? JSON.parse(pairsEl.textContent) : [];
|
||||
// URL state (strings from template) — cast to ints for comparison
|
||||
function toIntArr(s) { return (JSON.parse(s || '[]') || []).map(function(v) { return parseInt(v, 10); }).filter(function(n) { return !isNaN(n); }); }
|
||||
var urlProjects = toIntArr(urlProjectsEl && urlProjectsEl.textContent);
|
||||
var urlTeams = toIntArr(urlTeamsEl && urlTeamsEl.textContent);
|
||||
|
||||
// --- Cross-filter lookup indices ---
|
||||
var projectToTeams = {}, teamToProjects = {};
|
||||
pairs.forEach(function(pair) {
|
||||
var pid = pair.project_id, tid = pair.team_id;
|
||||
if (!projectToTeams[pid]) projectToTeams[pid] = new Set();
|
||||
if (!teamToProjects[tid]) teamToProjects[tid] = new Set();
|
||||
projectToTeams[pid].add(tid);
|
||||
teamToProjects[tid].add(pid);
|
||||
});
|
||||
|
||||
// --- Pending state (starts = URL state; becomes dirty when user edits) ---
|
||||
var pending = {
|
||||
dateMode: null, fromMonth: null, toMonth: null, startDate: null, endDate: null,
|
||||
projects: urlProjects.slice(),
|
||||
teams: urlTeams.slice(),
|
||||
};
|
||||
var fromMonthInput = document.getElementById('popoverFromMonth');
|
||||
var toMonthInput = document.getElementById('popoverToMonth');
|
||||
var startDateInput = document.getElementById('popoverStartDate');
|
||||
var endDateInput = document.getElementById('popoverEndDate');
|
||||
|
||||
// Initialise input values from URL query params
|
||||
var qs = new URLSearchParams(window.location.search);
|
||||
fromMonthInput.value = qs.get('from_month') || '';
|
||||
toMonthInput.value = qs.get('to_month') || '';
|
||||
startDateInput.value = qs.get('start_date') || '';
|
||||
endDateInput.value = qs.get('end_date') || '';
|
||||
var initialDateMode = (qs.get('start_date') || qs.get('end_date')) ? 'custom' : 'month';
|
||||
document.getElementById('popDateModeMonth').checked = (initialDateMode === 'month');
|
||||
document.getElementById('popDateModeCustom').checked = (initialDateMode === 'custom');
|
||||
document.getElementById('popoverMonthFields').classList.toggle('d-none', initialDateMode !== 'month');
|
||||
document.getElementById('popoverCustomFields').classList.toggle('d-none', initialDateMode !== 'custom');
|
||||
pending.dateMode = initialDateMode;
|
||||
pending.fromMonth = fromMonthInput.value;
|
||||
pending.toMonth = toMonthInput.value;
|
||||
pending.startDate = startDateInput.value;
|
||||
pending.endDate = endDateInput.value;
|
||||
|
||||
var urlDate = {
|
||||
mode: pending.dateMode,
|
||||
fromMonth: pending.fromMonth, toMonth: pending.toMonth,
|
||||
startDate: pending.startDate, endDate: pending.endDate,
|
||||
};
|
||||
|
||||
// --- Choices.js instances (lazy init on first open) ---
|
||||
var projectsChoices = null, teamsChoices = null;
|
||||
|
||||
// --- DOM refs ---
|
||||
var pills = {
|
||||
date: document.getElementById('filter-pill-date'),
|
||||
projects: document.getElementById('filter-pill-projects'),
|
||||
teams: document.getElementById('filter-pill-teams'),
|
||||
};
|
||||
var popovers = {
|
||||
date: document.getElementById('popover-date'),
|
||||
projects: document.getElementById('popover-projects'),
|
||||
teams: document.getElementById('popover-teams'),
|
||||
};
|
||||
var labels = {
|
||||
date: pills.date.querySelector('.filter-pill__label'),
|
||||
projects: pills.projects.querySelector('.filter-pill__label'),
|
||||
teams: pills.teams.querySelector('.filter-pill__label'),
|
||||
};
|
||||
var applyGroup = document.getElementById('apply-filters-group');
|
||||
var applyBtn = document.getElementById('apply-filters-btn');
|
||||
var resetBtn = document.getElementById('cancel-filters-btn');
|
||||
var toastContainer = document.getElementById('filter-toast-container');
|
||||
|
||||
// === POPOVER OPEN / CLOSE ===
|
||||
function closeAllPopovers(except) {
|
||||
Object.keys(popovers).forEach(function(key) {
|
||||
if (key !== except) {
|
||||
popovers[key].hidden = true;
|
||||
pills[key].setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
function openPopover(key) {
|
||||
closeAllPopovers(key);
|
||||
var pop = popovers[key];
|
||||
pop.hidden = false;
|
||||
pills[key].setAttribute('aria-expanded', 'true');
|
||||
|
||||
// Lazy-init Choices.js
|
||||
if (key === 'projects' && !projectsChoices) {
|
||||
projectsChoices = new Choices(document.getElementById('popoverProjects'), {
|
||||
removeItemButton: true, shouldSort: false, placeholder: true,
|
||||
placeholderValue: 'All projects (leave empty for all)',
|
||||
});
|
||||
}
|
||||
if (key === 'teams' && !teamsChoices) {
|
||||
teamsChoices = new Choices(document.getElementById('popoverTeams'), {
|
||||
removeItemButton: true, shouldSort: false, placeholder: true,
|
||||
placeholderValue: 'All teams (leave empty for all)',
|
||||
});
|
||||
}
|
||||
// Apply cross-filter to dropdown options (disable invalid ones)
|
||||
if (key === 'projects') applyCrossFilter('projects');
|
||||
if (key === 'teams') applyCrossFilter('teams');
|
||||
}
|
||||
|
||||
// --- Pill click handlers ---
|
||||
Object.keys(pills).forEach(function(key) {
|
||||
pills[key].addEventListener('click', function(ev) {
|
||||
ev.stopPropagation();
|
||||
var isOpen = !popovers[key].hidden;
|
||||
if (isOpen) {
|
||||
popovers[key].hidden = true;
|
||||
pills[key].setAttribute('aria-expanded', 'false');
|
||||
} else {
|
||||
openPopover(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- Click outside closes all ---
|
||||
document.addEventListener('click', function(ev) {
|
||||
if (!ev.target.closest('.filter-pill-wrap')) closeAllPopovers();
|
||||
});
|
||||
// --- Esc closes all ---
|
||||
document.addEventListener('keydown', function(ev) {
|
||||
if (ev.key === 'Escape') closeAllPopovers();
|
||||
});
|
||||
|
||||
// === POPOVER OK / CANCEL HANDLERS ===
|
||||
document.querySelectorAll('.filter-popover').forEach(function(pop) {
|
||||
var okBtn = pop.querySelector('.popover-ok');
|
||||
var cancelBtn = pop.querySelector('.popover-cancel');
|
||||
okBtn.addEventListener('click', function() {
|
||||
if (pop.id === 'popover-date') commitDateFromInputs();
|
||||
if (pop.id === 'popover-projects') commitProjectsFromChoices();
|
||||
if (pop.id === 'popover-teams') commitTeamsFromChoices();
|
||||
updateAllPillsDirty();
|
||||
closeAllPopovers();
|
||||
});
|
||||
cancelBtn.addEventListener('click', function() {
|
||||
if (pop.id === 'popover-date') revertDateInputs();
|
||||
if (pop.id === 'popover-projects') revertProjectsChoices();
|
||||
if (pop.id === 'popover-teams') revertTeamsChoices();
|
||||
closeAllPopovers();
|
||||
});
|
||||
});
|
||||
|
||||
// === DATE MODE TOGGLE inside the date popover ===
|
||||
document.getElementById('popDateModeMonth').addEventListener('change', function() {
|
||||
document.getElementById('popoverMonthFields').classList.remove('d-none');
|
||||
document.getElementById('popoverCustomFields').classList.add('d-none');
|
||||
});
|
||||
document.getElementById('popDateModeCustom').addEventListener('change', function() {
|
||||
document.getElementById('popoverMonthFields').classList.add('d-none');
|
||||
document.getElementById('popoverCustomFields').classList.remove('d-none');
|
||||
});
|
||||
|
||||
// === COMMIT / REVERT HELPERS ===
|
||||
function commitDateFromInputs() {
|
||||
pending.dateMode = document.getElementById('popDateModeCustom').checked ? 'custom' : 'month';
|
||||
pending.fromMonth = fromMonthInput.value;
|
||||
pending.toMonth = toMonthInput.value;
|
||||
pending.startDate = startDateInput.value;
|
||||
pending.endDate = endDateInput.value;
|
||||
if (pending.dateMode === 'month' && pending.fromMonth && pending.toMonth) {
|
||||
labels.date.textContent = humanMonth(pending.fromMonth) + ' – ' + humanMonth(pending.toMonth);
|
||||
} else if (pending.dateMode === 'custom' && pending.startDate && pending.endDate) {
|
||||
labels.date.textContent = humanDate(pending.startDate) + ' – ' + humanDate(pending.endDate);
|
||||
}
|
||||
}
|
||||
function revertDateInputs() {
|
||||
document.getElementById('popDateModeMonth').checked = (pending.dateMode === 'month');
|
||||
document.getElementById('popDateModeCustom').checked = (pending.dateMode === 'custom');
|
||||
document.getElementById('popoverMonthFields').classList.toggle('d-none', pending.dateMode !== 'month');
|
||||
document.getElementById('popoverCustomFields').classList.toggle('d-none', pending.dateMode !== 'custom');
|
||||
fromMonthInput.value = pending.fromMonth || '';
|
||||
toMonthInput.value = pending.toMonth || '';
|
||||
startDateInput.value = pending.startDate || '';
|
||||
endDateInput.value = pending.endDate || '';
|
||||
}
|
||||
function commitProjectsFromChoices() {
|
||||
if (!projectsChoices) return;
|
||||
pending.projects = projectsChoices.getValue(true).map(function(v) { return parseInt(v, 10); });
|
||||
updatePillLabel('projects', 'All Projects', document.getElementById('popoverProjects'));
|
||||
// Cross-filter: any team that's no longer valid gets auto-removed
|
||||
var invalidTeams = pending.teams.filter(function(tid) {
|
||||
return pending.projects.length > 0 && !pending.projects.some(function(pid) {
|
||||
return projectToTeams[pid] && projectToTeams[pid].has(tid);
|
||||
});
|
||||
});
|
||||
if (invalidTeams.length > 0) {
|
||||
pending.teams = pending.teams.filter(function(tid) { return invalidTeams.indexOf(tid) < 0; });
|
||||
if (teamsChoices) rebuildChoicesSelection(teamsChoices, pending.teams);
|
||||
invalidTeams.forEach(function(tid) {
|
||||
var name = teamNameById(tid);
|
||||
showToast('Removed ' + name + ' — no logs on selected projects');
|
||||
});
|
||||
updatePillLabel('teams', 'All Teams', document.getElementById('popoverTeams'));
|
||||
}
|
||||
}
|
||||
function revertProjectsChoices() {
|
||||
if (!projectsChoices) return;
|
||||
rebuildChoicesSelection(projectsChoices, pending.projects);
|
||||
}
|
||||
function commitTeamsFromChoices() {
|
||||
if (!teamsChoices) return;
|
||||
pending.teams = teamsChoices.getValue(true).map(function(v) { return parseInt(v, 10); });
|
||||
updatePillLabel('teams', 'All Teams', document.getElementById('popoverTeams'));
|
||||
var invalidProjects = pending.projects.filter(function(pid) {
|
||||
return pending.teams.length > 0 && !pending.teams.some(function(tid) {
|
||||
return teamToProjects[tid] && teamToProjects[tid].has(pid);
|
||||
});
|
||||
});
|
||||
if (invalidProjects.length > 0) {
|
||||
pending.projects = pending.projects.filter(function(pid) { return invalidProjects.indexOf(pid) < 0; });
|
||||
if (projectsChoices) rebuildChoicesSelection(projectsChoices, pending.projects);
|
||||
invalidProjects.forEach(function(pid) {
|
||||
var name = projectNameById(pid);
|
||||
showToast('Removed ' + name + ' — no logs for selected teams');
|
||||
});
|
||||
updatePillLabel('projects', 'All Projects', document.getElementById('popoverProjects'));
|
||||
}
|
||||
}
|
||||
function revertTeamsChoices() {
|
||||
if (!teamsChoices) return;
|
||||
rebuildChoicesSelection(teamsChoices, pending.teams);
|
||||
}
|
||||
|
||||
// --- Rebuild a Choices.js widget's selection from a list of IDs ---
|
||||
function rebuildChoicesSelection(instance, ids) {
|
||||
instance.removeActiveItems();
|
||||
var idStrs = ids.map(String);
|
||||
if (idStrs.length > 0) {
|
||||
instance.setChoiceByValue(idStrs);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Update a pill's label text based on selected options ---
|
||||
function updatePillLabel(key, allText, selectEl) {
|
||||
var ids = (key === 'projects') ? pending.projects : pending.teams;
|
||||
if (ids.length === 0) {
|
||||
labels[key].textContent = allText;
|
||||
return;
|
||||
}
|
||||
var names = ids.map(function(id) {
|
||||
var opt = selectEl.querySelector('option[value="' + id + '"]');
|
||||
return opt ? opt.textContent : String(id);
|
||||
});
|
||||
if (names.length === 1) labels[key].textContent = names[0];
|
||||
else if (names.length === 2) labels[key].textContent = names.join(', ');
|
||||
else labels[key].textContent = names[0] + ' + ' + (names.length - 1) + ' more';
|
||||
}
|
||||
|
||||
// --- Apply cross-filter: disable invalid options in the just-opened popover ---
|
||||
function applyCrossFilter(justOpened) {
|
||||
if (justOpened === 'projects' && projectsChoices) {
|
||||
if (pending.teams.length === 0) return; // no constraint
|
||||
var validPids = new Set();
|
||||
pending.teams.forEach(function(tid) {
|
||||
if (teamToProjects[tid]) {
|
||||
teamToProjects[tid].forEach(function(pid) { validPids.add(pid); });
|
||||
}
|
||||
});
|
||||
var sel = document.getElementById('popoverProjects');
|
||||
Array.from(sel.options).forEach(function(opt) {
|
||||
var pid = parseInt(opt.value, 10);
|
||||
opt.disabled = !validPids.has(pid) && !pending.projects.includes(pid);
|
||||
});
|
||||
projectsChoices.destroy();
|
||||
projectsChoices = new Choices(sel, {
|
||||
removeItemButton: true, shouldSort: false, placeholder: true,
|
||||
placeholderValue: 'All projects (leave empty for all)',
|
||||
});
|
||||
}
|
||||
if (justOpened === 'teams' && teamsChoices) {
|
||||
if (pending.projects.length === 0) return;
|
||||
var validTids = new Set();
|
||||
pending.projects.forEach(function(pid) {
|
||||
if (projectToTeams[pid]) {
|
||||
projectToTeams[pid].forEach(function(tid) { validTids.add(tid); });
|
||||
}
|
||||
});
|
||||
var selT = document.getElementById('popoverTeams');
|
||||
Array.from(selT.options).forEach(function(opt) {
|
||||
var tid = parseInt(opt.value, 10);
|
||||
opt.disabled = !validTids.has(tid) && !pending.teams.includes(tid);
|
||||
});
|
||||
teamsChoices.destroy();
|
||||
teamsChoices = new Choices(selT, {
|
||||
removeItemButton: true, shouldSort: false, placeholder: true,
|
||||
placeholderValue: 'All teams (leave empty for all)',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// === DIRTY STATE ===
|
||||
function isDateDirty() {
|
||||
return pending.dateMode !== urlDate.mode ||
|
||||
pending.fromMonth !== urlDate.fromMonth ||
|
||||
pending.toMonth !== urlDate.toMonth ||
|
||||
pending.startDate !== urlDate.startDate ||
|
||||
pending.endDate !== urlDate.endDate;
|
||||
}
|
||||
function isProjectsDirty() {
|
||||
return !sameIntSet(pending.projects, urlProjects);
|
||||
}
|
||||
function isTeamsDirty() {
|
||||
return !sameIntSet(pending.teams, urlTeams);
|
||||
}
|
||||
function sameIntSet(a, b) {
|
||||
if (a.length !== b.length) return false;
|
||||
var sA = new Set(a), sB = new Set(b);
|
||||
for (var v of sA) { if (!sB.has(v)) return false; }
|
||||
return true;
|
||||
}
|
||||
function updateAllPillsDirty() {
|
||||
pills.date.classList.toggle('filter-pill--dirty', isDateDirty());
|
||||
pills.projects.classList.toggle('filter-pill--dirty', isProjectsDirty());
|
||||
pills.teams.classList.toggle('filter-pill--dirty', isTeamsDirty());
|
||||
var anyDirty = isDateDirty() || isProjectsDirty() || isTeamsDirty();
|
||||
applyGroup.hidden = !anyDirty;
|
||||
}
|
||||
|
||||
// === APPLY: rebuild querystring + navigate ===
|
||||
applyBtn.addEventListener('click', function() {
|
||||
var params = new URLSearchParams();
|
||||
if (pending.dateMode === 'month') {
|
||||
if (pending.fromMonth) params.append('from_month', pending.fromMonth);
|
||||
if (pending.toMonth) params.append('to_month', pending.toMonth);
|
||||
} else {
|
||||
if (pending.startDate) params.append('start_date', pending.startDate);
|
||||
if (pending.endDate) params.append('end_date', pending.endDate);
|
||||
}
|
||||
pending.projects.forEach(function(pid) { params.append('project', pid); });
|
||||
pending.teams.forEach(function(tid) { params.append('team', tid); });
|
||||
window.location = window.location.pathname + '?' + params.toString();
|
||||
});
|
||||
|
||||
// === RESET: revert to URL state ===
|
||||
resetBtn.addEventListener('click', function() {
|
||||
pending.dateMode = urlDate.mode;
|
||||
pending.fromMonth = urlDate.fromMonth;
|
||||
pending.toMonth = urlDate.toMonth;
|
||||
pending.startDate = urlDate.startDate;
|
||||
pending.endDate = urlDate.endDate;
|
||||
pending.projects = urlProjects.slice();
|
||||
pending.teams = urlTeams.slice();
|
||||
revertDateInputs();
|
||||
if (projectsChoices) rebuildChoicesSelection(projectsChoices, pending.projects);
|
||||
if (teamsChoices) rebuildChoicesSelection(teamsChoices, pending.teams);
|
||||
if (pending.dateMode === 'month' && pending.fromMonth && pending.toMonth) {
|
||||
labels.date.textContent = humanMonth(pending.fromMonth) + ' – ' + humanMonth(pending.toMonth);
|
||||
} else if (pending.dateMode === 'custom' && pending.startDate && pending.endDate) {
|
||||
labels.date.textContent = humanDate(pending.startDate) + ' – ' + humanDate(pending.endDate);
|
||||
}
|
||||
updatePillLabel('projects', 'All Projects', document.getElementById('popoverProjects'));
|
||||
updatePillLabel('teams', 'All Teams', document.getElementById('popoverTeams'));
|
||||
updateAllPillsDirty();
|
||||
});
|
||||
|
||||
// === TOAST ===
|
||||
function showToast(message) {
|
||||
var toast = document.createElement('div');
|
||||
toast.className = 'filter-toast';
|
||||
toast.textContent = message; // textContent: XSS-safe
|
||||
toastContainer.appendChild(toast);
|
||||
setTimeout(function() {
|
||||
toast.classList.add('filter-toast--exiting');
|
||||
setTimeout(function() {
|
||||
if (toast.parentNode) toast.parentNode.removeChild(toast);
|
||||
}, 220);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
// === LABEL HELPERS ===
|
||||
function humanMonth(ym) {
|
||||
if (!ym || ym.length < 7) return ym || '';
|
||||
var parts = ym.split('-');
|
||||
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
return months[parseInt(parts[1], 10) - 1] + ' ' + parts[0];
|
||||
}
|
||||
function humanDate(ymd) {
|
||||
if (!ymd || ymd.length < 10) return ymd || '';
|
||||
var parts = ymd.split('-');
|
||||
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
return parseInt(parts[2], 10) + ' ' + months[parseInt(parts[1], 10) - 1] + ' ' + parts[0];
|
||||
}
|
||||
function projectNameById(pid) {
|
||||
var opt = document.getElementById('popoverProjects').querySelector('option[value="' + pid + '"]');
|
||||
return opt ? opt.textContent : 'Project ' + pid;
|
||||
}
|
||||
function teamNameById(tid) {
|
||||
var opt = document.getElementById('popoverTeams').querySelector('option[value="' + tid + '"]');
|
||||
return opt ? opt.textContent : 'Team ' + tid;
|
||||
}
|
||||
|
||||
// --- Initial render ---
|
||||
updateAllPillsDirty();
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user