Refactor templates and views for improved structure and maintainability
This commit is contained in:
@@ -21,7 +21,7 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">My cases / collections</h5>
|
<h5 class="card-title">My cases / collections</h5>
|
||||||
<p>View my <a href="{% url 'atlas:case_view' %}?author={{ request.user.id }}">cases</a>.</p>
|
<p>View my <a href="{% url 'atlas:case_view' %}?author={{ request.user.id }}">cases</a> or <a href="{% url 'atlas:collection_view' %}?author={{ request.user.id }}">my collections</a>.</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'atlas:user_uploads' %}" class="btn btn-outline-secondary">
|
<a href="{% url 'atlas:user_uploads' %}" class="btn btn-outline-secondary">
|
||||||
<i class="bi bi-cloud-arrow-up me-1"></i> Uploads awaiting import
|
<i class="bi bi-cloud-arrow-up me-1"></i> Uploads awaiting import
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
<ul class="list-unstyled mb-0">
|
<ul class="list-unstyled mb-0">
|
||||||
<li><a href="{% url 'atlas:user_collections' %}">Collections to view / take</a></li>
|
<li><a href="{% url 'atlas:user_collections' %}">Collections to view / take</a></li>
|
||||||
<li><a href="{% url 'atlas:case_view' %}?author={{ request.user.id }}">My cases</a></li>
|
<li><a href="{% url 'atlas:case_view' %}?author={{ request.user.id }}">My cases</a></li>
|
||||||
|
<li><a href="{% url 'atlas:collection_view' %}?author={{ request.user.id }}">My collections</a></li>
|
||||||
<li><a href="{% url 'atlas:user_uploads' %}">Uploads awaiting import</a></li>
|
<li><a href="{% url 'atlas:user_uploads' %}">Uploads awaiting import</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,95 +7,152 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
<!--<script type="text/javascript" src="/admin/jsi18n/"></script>-->
|
||||||
{{form.media}}
|
{{form.media}}
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- {{ form.media }} -->
|
<!-- {{ form.media }} -->
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Edit Exam Collection <small class="text-muted">{{ object.name }}</small></h2>
|
||||||
|
|
||||||
<h2>Edit Exam Collection {{object.name}}</h2>
|
<form action="" method="post" enctype="multipart/form-data" id="examcollection-form" class="needs-validation" novalidate>
|
||||||
<div class="card mb-4">
|
{% csrf_token %}
|
||||||
<div class="card-body">
|
{% if form.non_field_errors %}
|
||||||
<h2 class="card-title">Edit Exam Collection <small class="text-muted">{{ object.name }}</small></h2>
|
<div class="alert alert-danger">{{ form.non_field_errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form action="" method="post" enctype="multipart/form-data" id="examcollection-form" class="needs-validation" novalidate>
|
<style>
|
||||||
{% csrf_token %}
|
|
||||||
{% if form.non_field_errors %}
|
|
||||||
<div class="alert alert-danger">{{ form.non_field_errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Improve visual separation of form sections for this collection form */
|
/* Improve visual separation of form sections for this collection form */
|
||||||
#examcollection-form .mb-3 {
|
#examcollection-form .mb-3 {
|
||||||
padding: .75rem 1rem;
|
padding: .75rem 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
border-radius: .375rem;
|
border-radius: .375rem;
|
||||||
background: rgba(255,255,255,0.02);
|
background: rgba(255,255,255,0.02);
|
||||||
border-left: 3px solid rgba(255,255,255,0.03);
|
border-left: 3px solid rgba(255,255,255,0.03);
|
||||||
}
|
|
||||||
#examcollection-form .mb-3:nth-child(odd) {
|
|
||||||
background: rgba(255,255,255,0.015);
|
|
||||||
}
|
|
||||||
#examcollection-form .form-section-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.04);
|
|
||||||
margin: .5rem 0 1rem 0;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
#examcollection-form .form-section-title {
|
|
||||||
font-size: .95rem;
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--bs-light);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ form|crispy }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Insert subtle dividers between logical groups (.mb-3 blocks)
|
|
||||||
(function(){
|
|
||||||
var form = document.getElementById('examcollection-form');
|
|
||||||
if (!form) return;
|
|
||||||
// find direct .mb-3 children inside the form
|
|
||||||
var groups = Array.from(form.querySelectorAll(':scope > .mb-3'));
|
|
||||||
if (groups.length <= 1) return;
|
|
||||||
for (var i = 1; i < groups.length; i++) {
|
|
||||||
var divider = document.createElement('div');
|
|
||||||
divider.className = 'form-section-divider';
|
|
||||||
groups[i].parentNode.insertBefore(divider, groups[i]);
|
|
||||||
}
|
}
|
||||||
})();
|
#examcollection-form .mb-3:nth-child(odd) {
|
||||||
</script>
|
background: rgba(255,255,255,0.015);
|
||||||
|
}
|
||||||
|
#examcollection-form .form-section-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
margin: .5rem 0 1rem 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
#examcollection-form .form-section-title {
|
||||||
|
font-size: .95rem;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--bs-light);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="d-flex gap-2">
|
<div class="mb-3">
|
||||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1" aria-hidden="true"></i> Save</button>
|
<div class="row gy-3">
|
||||||
<a href="{% url 'generic:examcollection_list' %}" class="btn btn-outline-secondary"><i class="bi bi-arrow-left me-1" aria-hidden="true"></i> Back to collections</a>
|
<div class="col-md-6">
|
||||||
<button type="reset" class="btn btn-light">Reset</button>
|
<div class="mb-2">
|
||||||
</div>
|
<label for="id_name" class="form-label requiredField">Name <span class="text-danger">*</span></label>
|
||||||
</form>
|
{{ form.name }}
|
||||||
|
{% if form.name.errors %}
|
||||||
|
<div class="invalid-feedback d-block">{{ form.name.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="id_date" class="form-label">Date</label>
|
||||||
|
{{ form.date }}
|
||||||
|
{% if form.date.errors %}
|
||||||
|
<div class="invalid-feedback d-block">{{ form.date.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 d-flex align-items-center">
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
{{ form.archive }}
|
||||||
|
<label for="id_archive" class="form-check-label ms-2">Archive</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-3" />
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-2">Authors</h5>
|
||||||
|
<div>
|
||||||
|
{{ form.author }}
|
||||||
|
{% if form.author.errors %}
|
||||||
|
<div class="invalid-feedback d-block">{{ form.author.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 g-3">
|
||||||
|
{% for label, field_name, bound in exam_groups %}
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title small mb-2">{{ label }}</h5>
|
||||||
|
<div>
|
||||||
|
{{ bound }}
|
||||||
|
{% if bound.errors %}
|
||||||
|
<div class="invalid-feedback d-block">{{ bound.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Insert subtle dividers between logical groups (.mb-3 blocks)
|
||||||
|
(function(){
|
||||||
|
var form = document.getElementById('examcollection-form');
|
||||||
|
if (!form) return;
|
||||||
|
// find direct .mb-3 children inside the form
|
||||||
|
var groups = Array.from(form.querySelectorAll(':scope > .mb-3'));
|
||||||
|
if (groups.length <= 1) return;
|
||||||
|
for (var i = 1; i < groups.length; i++) {
|
||||||
|
var divider = document.createElement('div');
|
||||||
|
divider.className = 'form-section-divider';
|
||||||
|
groups[i].parentNode.insertBefore(divider, groups[i]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg me-1" aria-hidden="true"></i> Save</button>
|
||||||
|
<a href="{% url 'generic:examcollection_list' %}" class="btn btn-outline-secondary"><i class="bi bi-arrow-left me-1" aria-hidden="true"></i> Back to collections</a>
|
||||||
|
<button type="reset" class="btn btn-light">Reset</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<script>
|
||||||
<script>
|
|
||||||
// Simple client-side bootstrap validation
|
// Simple client-side bootstrap validation
|
||||||
(function () {
|
(function () {
|
||||||
'use strict'
|
'use strict'
|
||||||
var forms = document.querySelectorAll('.needs-validation')
|
var forms = document.querySelectorAll('.needs-validation')
|
||||||
Array.prototype.slice.call(forms).forEach(function (form) {
|
Array.prototype.slice.call(forms).forEach(function (form) {
|
||||||
form.addEventListener('submit', function (event) {
|
form.addEventListener('submit', function (event) {
|
||||||
if (!form.checkValidity()) {
|
if (!form.checkValidity()) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}
|
}
|
||||||
form.classList.add('was-validated')
|
form.classList.add('was-validated')
|
||||||
}, false)
|
}, false)
|
||||||
})
|
})
|
||||||
})()
|
})()
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{% if results %}
|
{% if results %}
|
||||||
<ul class="list-unstyled mb-0" id="user_search_results_list_{{ field }}">
|
<ul class="list-unstyled mb-0" id="user_search_results_list_{{ field }}">
|
||||||
{% for item in results %}
|
{% for item in results %}
|
||||||
<li class="py-1 d-flex justify-content-between align-items-center" data-user-id="{{ item.id }}" data-user-text="{{ item.text|escapejs }}" data-user-grade="{{ item.grade|default_if_none:''|escapejs }}">
|
<li class="py-1 d-flex justify-content-between align-items-center" data-user-id="{{ item.id }}" data-user-text="{{ item.text|escape }}" data-user-grade="{{ item.grade|default_if_none:''|escape }}">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
{{ item.text|escape }}
|
{{ item.text|escape }}
|
||||||
{% if item.grade %}
|
{% if item.grade %}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-sm btn-primary user-add-btn" data-user-id="{{ item.id }}" data-user-text="{{ item.text|escapejs }}" data-user-grade="{{ item.grade|default_if_none:''|escapejs }}">Add</button>
|
<button type="button" class="btn btn-sm btn-primary user-add-btn" data-user-id="{{ item.id }}" data-user-text="{{ item.text|escape }}" data-user-grade="{{ item.grade|default_if_none:''|escape }}">Add</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -5490,11 +5490,38 @@ class ExamCollectionEdit(UpdateView, AuthorRequiredMixin):
|
|||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
form_class = ExamCollectionForm
|
form_class = ExamCollectionForm
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
# Ensure we have a bound form to extract bound fields
|
||||||
|
form = kwargs.get('form') or self.get_form()
|
||||||
|
groups = []
|
||||||
|
for label, field_name, model in getattr(self.form_class, 'GROUP_TYPES', []):
|
||||||
|
try:
|
||||||
|
bound = form[field_name]
|
||||||
|
except Exception:
|
||||||
|
bound = None
|
||||||
|
groups.append((label, field_name, bound))
|
||||||
|
context['exam_groups'] = groups
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionCreate(CreateView, AuthorRequiredMixin):
|
class ExamCollectionCreate(CreateView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
form_class = ExamCollectionForm
|
form_class = ExamCollectionForm
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
form = kwargs.get('form') or self.get_form()
|
||||||
|
groups = []
|
||||||
|
for label, field_name, model in getattr(self.form_class, 'GROUP_TYPES', []):
|
||||||
|
try:
|
||||||
|
bound = form[field_name]
|
||||||
|
except Exception:
|
||||||
|
bound = None
|
||||||
|
groups.append((label, field_name, bound))
|
||||||
|
context['exam_groups'] = groups
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
|
|||||||
+76
-2
@@ -33,7 +33,7 @@ class UserSearchSelectMultipleWidget:
|
|||||||
options_html = ""
|
options_html = ""
|
||||||
selected_html = ""
|
selected_html = ""
|
||||||
for u in users:
|
for u in users:
|
||||||
options_html += f'<option value="{u.pk}" selected>{u.get_full_name() or u.username} - {u.email}</option>'
|
# determine grade_text first, then include data-user-grade attribute on option
|
||||||
grade_text = ""
|
grade_text = ""
|
||||||
try:
|
try:
|
||||||
up = getattr(u, "userprofile", None)
|
up = getattr(u, "userprofile", None)
|
||||||
@@ -42,6 +42,8 @@ class UserSearchSelectMultipleWidget:
|
|||||||
grade_text = getattr(g, "name", str(g)) if g is not None else ""
|
grade_text = getattr(g, "name", str(g)) if g is not None else ""
|
||||||
except Exception:
|
except Exception:
|
||||||
grade_text = ""
|
grade_text = ""
|
||||||
|
safe_grade_attr = (grade_text.replace('"', '"') if grade_text else '')
|
||||||
|
options_html += f'<option value="{u.pk}" selected data-user-grade="{safe_grade_attr}">{u.get_full_name() or u.username} - {u.email}</option>'
|
||||||
|
|
||||||
# Build the selected item HTML incrementally to avoid accidental
|
# Build the selected item HTML incrementally to avoid accidental
|
||||||
# mixing of boolean values into string concatenation (was causing
|
# mixing of boolean values into string concatenation (was causing
|
||||||
@@ -147,6 +149,16 @@ class UserSearchSelectMultipleWidget:
|
|||||||
timeout = setTimeout(() => fetchResults(search.value), 300);
|
timeout = setTimeout(() => fetchResults(search.value), 300);
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
// Prevent Enter from submitting the surrounding form; run the search instead
|
||||||
|
search.addEventListener('keydown', function(e) {{
|
||||||
|
if (e.key === 'Enter' || e.keyCode === 13) {{
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
clearTimeout(timeout);
|
||||||
|
fetchResults(search.value);
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
gradeSelect && gradeSelect.addEventListener('change', function(e) {{
|
gradeSelect && gradeSelect.addEventListener('change', function(e) {{
|
||||||
// re-run search with same query when grade changes
|
// re-run search with same query when grade changes
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
@@ -172,7 +184,16 @@ class UserSearchSelectMultipleWidget:
|
|||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
const id = btn.getAttribute('data-user-id');
|
const id = btn.getAttribute('data-user-id');
|
||||||
const text = btn.getAttribute('data-user-text') || btn.textContent.trim();
|
const text = btn.getAttribute('data-user-text') || btn.textContent.trim();
|
||||||
const grade = btn.getAttribute('data-user-grade') || '';
|
// Try button attribute, then list-item dataset, then visible badge text
|
||||||
|
let grade = btn.getAttribute('data-user-grade') || '';
|
||||||
|
if (!grade) {{
|
||||||
|
const li = btn.closest('li[data-user-id]');
|
||||||
|
if (li && li.dataset && li.dataset.userGrade) grade = li.dataset.userGrade;
|
||||||
|
else {{
|
||||||
|
const badge = btn.closest('li') ? btn.closest('li').querySelector('.badge') : null;
|
||||||
|
if (badge) grade = badge.textContent.trim();
|
||||||
|
}}
|
||||||
|
}}
|
||||||
addUserToField(widgetFieldName, id, text, grade);
|
addUserToField(widgetFieldName, id, text, grade);
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@@ -226,6 +247,49 @@ class UserSearchSelectMultipleWidget:
|
|||||||
addUserToField(fieldName, id, text, grade);
|
addUserToField(fieldName, id, text, grade);
|
||||||
}});
|
}});
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// Initialize visible selected-list from any pre-existing <option selected> entries
|
||||||
|
(function initSelectedListFromOptions() {{
|
||||||
|
try {{
|
||||||
|
const sel = document.getElementById('{field_id}');
|
||||||
|
const listEl = document.getElementById('{selected_list_id}');
|
||||||
|
if (!sel || !listEl) return;
|
||||||
|
for (let i=0;i<sel.options.length;i++) {{
|
||||||
|
const opt = sel.options[i];
|
||||||
|
const id = opt.value;
|
||||||
|
if (!id) continue;
|
||||||
|
const liId = 'selected_user_' + '{self.name}' + '_' + id;
|
||||||
|
const existingLi = document.getElementById(liId);
|
||||||
|
const grade = opt.getAttribute('data-user-grade') || '';
|
||||||
|
|
||||||
|
if (existingLi) {{
|
||||||
|
// If server-rendered li exists but has no badge, add one from the option attribute
|
||||||
|
const hasBadge = existingLi.querySelector('.badge');
|
||||||
|
if (!hasBadge && grade) {{
|
||||||
|
const span = existingLi.querySelector('span');
|
||||||
|
if (span) {{
|
||||||
|
const badge = document.createElement('span');
|
||||||
|
badge.className = 'badge bg-secondary ms-2';
|
||||||
|
badge.textContent = grade;
|
||||||
|
span.appendChild(badge);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
continue;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// build li from option when not rendered server-side
|
||||||
|
const text = opt.textContent || opt.innerText || opt.text;
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'list-group-item d-flex justify-content-between align-items-center';
|
||||||
|
li.id = liId;
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.innerHTML = text + (grade ? ' <span class="badge bg-secondary ms-2">' + grade + '</span>' : '');
|
||||||
|
const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-sm btn-outline-danger ms-2'; btn.innerText = 'Remove';
|
||||||
|
btn.addEventListener('click', function() {{ removeUserFromField('{self.name}', id); }});
|
||||||
|
li.appendChild(span); li.appendChild(btn); listEl.appendChild(li);
|
||||||
|
}}
|
||||||
|
}} catch (e) {{ /* ignore init errors */ }}
|
||||||
|
}})();
|
||||||
}})();
|
}})();
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
@@ -481,6 +545,16 @@ class ExamSearchSelectMultipleWidget:
|
|||||||
timeout = setTimeout(() => fetchResults(search.value), 300);
|
timeout = setTimeout(() => fetchResults(search.value), 300);
|
||||||
}});
|
}});
|
||||||
|
|
||||||
|
// Prevent Enter from submitting the surrounding form; run the search instead
|
||||||
|
search.addEventListener('keydown', function(e) {{
|
||||||
|
if (e.key === 'Enter' || e.keyCode === 13) {{
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
clearTimeout(timeout);
|
||||||
|
fetchResults(search.value);
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
// Toggle help panel visibility
|
// Toggle help panel visibility
|
||||||
if (helpBtn && helpPanel) {{
|
if (helpBtn && helpPanel) {{
|
||||||
helpBtn.addEventListener('click', function(e) {{
|
helpBtn.addEventListener('click', function(e) {{
|
||||||
|
|||||||
Reference in New Issue
Block a user