Skip to content
Draft
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
5 changes: 4 additions & 1 deletion dashboard/dashboard/static/project-detail.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
table.objectives tr.objective td.name {background-color: #205067; color: white; font-weight: bold;}
table.objectives tr.objective td.name {background-color: #205067; color: white; font-weight: bold; cursor: pointer; user-select: none; border: 0.5px solid #366177;}
table.objectives tr.objective td.name::before {content: "▶"; font-size: 0.7em; display: inline-block; transform: rotate(90deg); margin-right: 0.4em;}
table.objectives tbody.collapsed tr.objective td.name::before {content: "▶"; transform: rotate(0deg);}
Comment on lines +2 to +3
Copy link
Copy Markdown
Author

@a-velasco a-velasco Mar 25, 2026

Choose a reason for hiding this comment

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

At first I tried using the unicode characters ▼ and ▶, but the browser renders them with different sizes. Even when I explicitly used the same variation selector for both.

Only quick way I found to keep them identical is to use the same symbol and rotate it, but I am very open to a better alternative.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I suppose an alternative would be to use SVG? Your approach is neat, I like it. I suppose we could even have the icon rotate during the transform (but that's very not essential!)

table.objectives tbody.collapsed tr:not(.objective) {display: none;}
table.objectives tr.level {background-color: #f0f0f0; font-weight: bold;}

table.objectives td p, th p {margin: .2rem 0}
Expand Down
1 change: 1 addition & 0 deletions dashboard/dashboard/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@

<body {% block body_attributes %}{% endblock %}>
{% block content %}{% endblock %}
{% block eventlisteners %}{% endblock %}
</body>
</html>
40 changes: 40 additions & 0 deletions dashboard/projects/templates/projects/project.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,43 @@ <h1>{{ project }}</h1>
</div>

{% endblock content %}

{% block eventlisteners %}
<script>
const LOCAL_STORAGE_KEY = 'collapsed_objectives_{{ project.id }}';

function getCollapsed() {
return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '[]');
}

function saveCollapsed(ids) {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(ids));
}

// Restore collapsed state on page load.
const collapsed = getCollapsed();
document.querySelectorAll('tbody[id]').forEach(function (tbody) {
if (collapsed.includes(tbody.id)) {
tbody.classList.add('collapsed');
}
});

// Toggle on click.
document.addEventListener('click', function (e) {
const nameTd = e.target.closest('tr.objective > td.objective.name');
if (!nameTd) return;

const tbody = nameTd.closest('tbody');
tbody.classList.toggle('collapsed');

const ids = getCollapsed();
if (tbody.classList.contains('collapsed')) {
if (!ids.includes(tbody.id)) ids.push(tbody.id);
} else {
const i = ids.indexOf(tbody.id);
if (i > -1) ids.splice(i, 1);
}
saveCollapsed(ids);
});
</script>
{% endblock %}
5 changes: 4 additions & 1 deletion dashboard/staticfiles/project-detail.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
table.objectives tr.objective td.name {background-color: #205067; color: white; font-weight: bold;}
table.objectives tr.objective td.name {background-color: #205067; color: white; font-weight: bold; cursor: pointer; user-select: none; border: 0.5px solid #366177;}
table.objectives tr.objective td.name::before {content: "▶"; font-size: 0.7em; display: inline-block; transform: rotate(90deg); margin-right: 0.4em;}
table.objectives tbody.collapsed tr.objective td.name::before {content: "▶"; transform: rotate(0deg);}
table.objectives tbody.collapsed tr:not(.objective) {display: none;}
table.objectives tr.level {background-color: #f0f0f0; font-weight: bold;}

table.objectives td p, th p {margin: .2rem 0}
Expand Down
33 changes: 33 additions & 0 deletions dashboard/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,36 @@ def test_commitment_table(page):

def test_last_review(page):
expect(page.get_by_role("textbox", name="Last review:")).to_have_value("2024-12-16")


def test_collapsing_objectives(page):
"""Check that objective rows collapse/expand on click and the state persists in browser's local storage."""

storage_key = "collapsed_objectives_1"
tbody = page.locator("tbody#colourfulness")

# Clear any leftover state.
page.evaluate(f"localStorage.removeItem('{storage_key}')")

# Colourfulness objective is not collapsed by default.
assert not page.evaluate("document.querySelector('tbody#colourfulness').classList.contains('collapsed')")

# Click objective name, check that it collapses
tbody.locator("tr.objective td.objective.name").click()
assert page.evaluate("document.querySelector('tbody#colourfulness').classList.contains('collapsed')")

# Check that it is written to localStorage.
stored = page.evaluate(f"JSON.parse(localStorage.getItem('{storage_key}') || '[]')")
assert "colourfulness" in stored

# Reload the page, check that the objective remains collapsed.
page.reload()
assert page.evaluate("document.querySelector('tbody#colourfulness').classList.contains('collapsed')")

# Click objective to expand, check that it is no longer collapsed.
page.locator("tbody#colourfulness tr.objective td.objective.name").click()
assert not page.evaluate("document.querySelector('tbody#colourfulness').classList.contains('collapsed')")

# Check that it is removed from localStorage.
stored = page.evaluate(f"JSON.parse(localStorage.getItem('{storage_key}') || '[]')")
assert "colourfulness" not in stored
Loading