diff --git a/runbot/controllers/frontend.py b/runbot/controllers/frontend.py
index 181a2c184..a5d7c0dfd 100644
--- a/runbot/controllers/frontend.py
+++ b/runbot/controllers/frontend.py
@@ -908,6 +908,7 @@ def bundles_by_tag(self, bundle_tag_id=None, project=None, **kwargs):
if not project and projects:
project = projects[0]
bundles_by_team = defaultdict(list)
+ open_bundles_by_team = defaultdict(int)
nb_bundles = 0
nb_bundles_done = 0
for bundle in self.env['runbot.bundle'].search([('tag_ids', 'in', bundle_tag_id.id)]):
@@ -916,10 +917,13 @@ def bundles_by_tag(self, bundle_tag_id=None, project=None, **kwargs):
bundle_prs = bundle.branch_ids.filtered(lambda rec: rec.is_pr)
if any(bundle_prs) and not any(bundle_prs.mapped('alive')):
nb_bundles_done += 1
+ else:
+ open_bundles_by_team[bundle.team_id.name or 'No Team Defined'] += 1
qctx = {
'tag': bundle_tag_id,
'bundles_by_team': bundles_by_team,
+ 'open_bundles_by_team': open_bundles_by_team,
'nb_bundles': nb_bundles,
'nb_bundles_done': nb_bundles_done,
}
diff --git a/runbot/static/src/css/runbot.css b/runbot/static/src/css/runbot.css
index 39df923c8..8411a1cdf 100644
--- a/runbot/static/src/css/runbot.css
+++ b/runbot/static/src/css/runbot.css
@@ -471,3 +471,26 @@ code {
.log-details td {
padding-left: 20px;
}
+
+/*
+ * Bundle by tag
+ */
+.o_team_bundle_grid {
+ display: grid;
+ /* menu | name (2/3 of free space) | tags | age | filler (1/3) */
+ grid-template-columns: max-content 2fr max-content max-content 1fr;
+ column-gap: .5rem;
+ align-items: center;
+}
+
+.o_team_bundle_grid > li {
+ display: contents;
+}
+
+.o_team_bundle_grid > li > button {
+ grid-column: 1; /* each bundle starts a new grid row, even if the previous one has no age badge */
+}
+
+.o_hide_done .o_team_bundle_grid > li:has(a.bundle-done) {
+ display: none;
+}
diff --git a/runbot/static/src/js/runbot.js b/runbot/static/src/js/runbot.js
index 76b2a129b..15513d3bf 100644
--- a/runbot/static/src/js/runbot.js
+++ b/runbot/static/src/js/runbot.js
@@ -54,3 +54,32 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
});
+
+// Expand/collapse all team sections at once.
+document.addEventListener('DOMContentLoaded', function() {
+ const expandBtn = document.getElementById('toggleTeamsButton');
+ if (expandBtn) {
+ const bundleGrid = document.querySelector('.o_team_bundle_grid');
+ const container = bundleGrid.closest('.container-fluid');
+ expandBtn.addEventListener('click', function () {
+ const details = container.querySelectorAll('details');
+ const allOpen = Array.from(details).every(d => d.open);
+ details.forEach(d => d.open = !allOpen);
+ this.textContent = allOpen ? 'Expand all' : 'Collapse all';
+ });
+ }
+});
+
+// Show/hide done bundles, persisted across reloads.
+document.addEventListener('DOMContentLoaded', function() {
+ const toggleHideDoneBtn = document.getElementById('toggleHideDoneButton');
+ if (toggleHideDoneBtn) {
+ const bundleGrid = document.querySelector('.o_team_bundle_grid');
+ const container = bundleGrid.closest('.container-fluid');
+ toggleHideDoneBtn.addEventListener('click', function () {
+ const hidden = container.classList.toggle('o_hide_done');
+ this.textContent = hidden ? 'Show done' : 'Hide done';
+ localStorage.setItem('runbot_hide_done_bundles', hidden ? '1' : '');
+ });
+ }
+});
\ No newline at end of file
diff --git a/runbot/templates/bundles_by_tag.xml b/runbot/templates/bundles_by_tag.xml
index e7a7834d9..40ca25550 100644
--- a/runbot/templates/bundles_by_tag.xml
+++ b/runbot/templates/bundles_by_tag.xml
@@ -9,20 +9,22 @@