Skip to content

Commit 1b563c0

Browse files
Merge pull request #2561 from IFRCGo/feature/admin-page-to-query-users
Add filter and SU flag to the User query page
2 parents 77d5488 + fc32cee commit 1b563c0

File tree

1 file changed

+55
-1
lines changed

1 file changed

+55
-1
lines changed

api/templates/admin/users_per_permission.html

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
<form method="get" class="form-inline" style="margin-bottom: 1rem;">
88
<div>
99
<label for="id_groups">{% trans "Groups" %}</label><br/>
10+
<input type="text" id="id_groups_filter" placeholder="{% trans 'Filter' %}" style="min-width: 360px; margin-bottom: .5rem;" />
11+
<br/>
1012
<select id="id_groups" name="groups" multiple size="12" style="min-width: 360px;">
1113
{% for g in groups %}
1214
<option value="{{ g.id }}" {% if g.id in selected_group_ids %}selected{% endif %}>{{ g.name }}</option>
@@ -45,7 +47,8 @@
4547
<th>{% trans "Name" %}</th>
4648
<th>{% trans "Organization" %}</th>
4749
<th>{% trans "Active" %}</th>
48-
<th>{% trans "Staff status" %}</th>
50+
<th>{% trans "Staff" %}</th>
51+
<th>{% trans "Superuser" %}</th>
4952
</tr>
5053
</thead>
5154
<tbody>
@@ -59,6 +62,7 @@
5962
<!-- yesno:"+,," -> '+' for True, '' for False/None -->
6063
<td class="bool-col">{{ u.is_active|yesno:"+,," }}</td>
6164
<td class="bool-col">{{ u.is_staff|yesno:"+,," }}</td>
65+
<td class="bool-col">{{ u.is_superuser|yesno:"+,," }}</td>
6266
</tr>
6367
{% empty %}
6468
<tr><td colspan="7">{% trans "No users match the current filters." %}</td></tr>
@@ -70,4 +74,54 @@
7074
<p>{% trans "Select one or more Groups to view users. Optionally include superusers." %}</p>
7175
{% endif %}
7276
</div>
77+
78+
<script>
79+
(function() {
80+
const filterInput = document.getElementById('id_groups_filter');
81+
const selectEl = document.getElementById('id_groups');
82+
if (!filterInput || !selectEl) return;
83+
84+
// Snapshot original options once
85+
const allOptions = Array.from(selectEl.options).map(opt => ({
86+
value: opt.value,
87+
label: opt.text,
88+
textLower: opt.text.toLowerCase(),
89+
selected: opt.selected,
90+
}));
91+
92+
function syncSelectedState() {
93+
const selectedValues = new Set(Array.from(selectEl.selectedOptions).map(o => o.value));
94+
for (const o of allOptions) {
95+
o.selected = selectedValues.has(o.value);
96+
}
97+
}
98+
99+
function renderOptions(query) {
100+
const q = (query || '').toLowerCase();
101+
const filtered = allOptions.filter(o => o.selected || o.textLower.includes(q));
102+
// Rebuild options while preserving selection
103+
selectEl.innerHTML = '';
104+
for (const o of filtered) {
105+
const opt = document.createElement('option');
106+
opt.value = o.value;
107+
opt.text = o.label;
108+
if (o.selected) opt.selected = true;
109+
selectEl.appendChild(opt);
110+
}
111+
}
112+
113+
selectEl.addEventListener('change', () => {
114+
syncSelectedState();
115+
renderOptions(filterInput.value);
116+
});
117+
118+
filterInput.addEventListener('input', () => {
119+
syncSelectedState();
120+
renderOptions(filterInput.value);
121+
});
122+
123+
// Initial render (no filter)
124+
renderOptions('');
125+
})();
126+
</script>
73127
{% endblock %}

0 commit comments

Comments
 (0)