fix: replace sidebar controls with resize handle

This commit is contained in:
2026-07-31 11:11:19 +02:00
parent 6b36184f9d
commit 5a2276aec9
12 changed files with 249 additions and 198 deletions
+3 -1
View File
@@ -1,11 +1,13 @@
from netbox.plugins import PluginConfig, get_plugin_config
__version__ = "0.5.1"
class NetBoxUtilitiesConfig(PluginConfig):
name = "netbox_utilities"
verbose_name = "NetBox Utilities"
description = "Navigation, tenant filtering, bulk module installation, and atomic rack reordering"
version = "0.5.0"
version = __version__
author = "LKE"
base_url = "utilities"
min_version = "4.6.5"
+4 -9
View File
@@ -2,7 +2,6 @@ from netbox.navigation.menu import get_menus
SIDEBAR_WIDTH_MIN = 216
SIDEBAR_WIDTH_MAX = 408
SIDEBAR_WIDTH_STEP = 24
SIDEBAR_WIDTH_DEFAULT = 288
@@ -12,19 +11,15 @@ def normalize_sidebar_width(value):
except (TypeError, ValueError):
return SIDEBAR_WIDTH_DEFAULT
width = min(SIDEBAR_WIDTH_MAX, max(SIDEBAR_WIDTH_MIN, width))
steps = (width - SIDEBAR_WIDTH_MIN + SIDEBAR_WIDTH_STEP // 2) // SIDEBAR_WIDTH_STEP
return SIDEBAR_WIDTH_MIN + steps * SIDEBAR_WIDTH_STEP
return min(SIDEBAR_WIDTH_MAX, max(SIDEBAR_WIDTH_MIN, width))
def update_sidebar_layout(collapsed, width, action):
def update_sidebar_layout(collapsed, width, action, requested_width=None):
width = normalize_sidebar_width(width)
if action == "toggle":
return not collapsed, width
if action == "smaller":
return collapsed, max(SIDEBAR_WIDTH_MIN, width - SIDEBAR_WIDTH_STEP)
if action == "larger":
return collapsed, min(SIDEBAR_WIDTH_MAX, width + SIDEBAR_WIDTH_STEP)
if action == "resize":
return False, normalize_sidebar_width(requested_width)
raise ValueError("Unbekannte Navigationsaktion.")
@@ -16,10 +16,9 @@
function normalizeWidth(value) {
const minimum = Number(preferences.width_min) || 216;
const maximum = Number(preferences.width_max) || 408;
const step = Number(preferences.width_step) || 24;
const requested = Number(value) || 288;
const snapped = minimum + Math.round((requested - minimum) / step) * step;
return Math.min(maximum, Math.max(minimum, snapped));
const requested = Number(value);
const width = Number.isFinite(requested) ? Math.round(requested) : 288;
return Math.min(maximum, Math.max(minimum, width));
}
function applyRootLayout() {
@@ -58,7 +57,8 @@
sidebar.querySelectorAll(':scope > li.nav-item.dropdown > button.nav-link').forEach((button) => {
if (preferences.collapsed) {
button.dataset.netboxUtilitiesTitle = 'true';
button.title = button.getAttribute('aria-label') || '';
button.title =
button.getAttribute('aria-label') || button.querySelector('.nav-link-title')?.textContent.trim() || '';
const menu = button.closest('.nav-item.dropdown')?.querySelector(':scope > .dropdown-menu.show');
if (menu) {
const top = Math.max(8, button.getBoundingClientRect().top);
@@ -94,111 +94,149 @@
});
}
function createControl(action, icon, label) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'btn btn-sm btn-ghost-secondary netbox-utilities-sidebar-control';
button.dataset.action = action;
button.title = label;
button.setAttribute('aria-label', label);
const iconElement = document.createElement('i');
iconElement.className = `mdi ${icon}`;
iconElement.setAttribute('aria-hidden', 'true');
button.appendChild(iconElement);
return button;
async function saveLayout(action, width) {
const body = new URLSearchParams({action});
if (width !== undefined) body.set('width', String(width));
const response = await fetch(preferences.layout_update_url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'X-CSRFToken': preferences.csrf_token,
},
body,
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
function updateControls(controls, sidebar, pending = false) {
const minimum = Number(preferences.width_min) || 216;
const maximum = Number(preferences.width_max) || 408;
const smaller = controls.querySelector('[data-action="smaller"]');
const larger = controls.querySelector('[data-action="larger"]');
const toggle = controls.querySelector('[data-action="toggle"]');
const toggleIcon = toggle.querySelector('i');
smaller.disabled = pending || preferences.collapsed || preferences.width <= minimum;
larger.disabled = pending || preferences.collapsed || preferences.width >= maximum;
toggle.disabled = pending;
toggleIcon.className = preferences.collapsed
? 'mdi mdi-arrow-expand-right'
: 'mdi mdi-arrow-collapse-left';
const toggleLabel = preferences.collapsed ? 'Navigation ausklappen' : 'Navigation auf Icons einklappen';
toggle.title = toggleLabel;
toggle.setAttribute('aria-label', toggleLabel);
updateMenuTitles(sidebar);
}
function applyAction(action) {
const step = Number(preferences.width_step) || 24;
if (action === 'toggle') preferences.collapsed = !preferences.collapsed;
if (action === 'smaller') preferences.width = normalizeWidth(preferences.width - step);
if (action === 'larger') preferences.width = normalizeWidth(preferences.width + step);
applyRootLayout();
}
function addLayoutControls(sidebarMenu, sidebar) {
function addResizeHandle(sidebarElement, sidebar) {
if (!preferences.layout_update_url || !preferences.csrf_token) return;
const controls = document.createElement('div');
controls.className = 'netbox-utilities-sidebar-controls';
controls.setAttribute('role', 'group');
controls.setAttribute('aria-label', 'Navigationsgröße');
controls.append(
createControl('smaller', 'mdi-minus', 'Navigation schmaler'),
createControl('toggle', 'mdi-arrow-collapse-left', 'Navigation auf Icons einklappen'),
createControl('larger', 'mdi-plus', 'Navigation breiter')
);
const handle = document.createElement('div');
handle.className = 'netbox-utilities-sidebar-resizer';
handle.tabIndex = 0;
handle.setAttribute('role', 'separator');
handle.setAttribute('aria-orientation', 'vertical');
handle.setAttribute('aria-label', 'Breite der Navigation ändern');
const releaseInfo = sidebarMenu.querySelector(':scope > .text-muted');
sidebarMenu.insertBefore(controls, releaseInfo || null);
updateControls(controls, sidebar);
const toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'netbox-utilities-sidebar-toggle';
const toggleIcon = document.createElement('i');
toggleIcon.className = 'mdi';
toggleIcon.setAttribute('aria-hidden', 'true');
toggle.appendChild(toggleIcon);
handle.appendChild(toggle);
sidebarElement.appendChild(handle);
controls.addEventListener('click', async (event) => {
const button = event.target.closest('button[data-action]');
if (!button || button.disabled) return;
let resizing = false;
let previousLayout = null;
const previous = {collapsed: preferences.collapsed, width: preferences.width};
applyAction(button.dataset.action);
updateControls(controls, sidebar, true);
function updateHandle() {
const minimum = Number(preferences.width_min) || 216;
const maximum = Number(preferences.width_max) || 408;
handle.setAttribute('aria-valuemin', String(minimum));
handle.setAttribute('aria-valuemax', String(maximum));
handle.setAttribute('aria-valuenow', String(preferences.collapsed ? minimum : preferences.width));
const label = preferences.collapsed ? 'Navigation ausklappen' : 'Navigation auf Icons einklappen';
toggle.title = label;
toggle.setAttribute('aria-label', label);
toggleIcon.className = preferences.collapsed ? 'mdi mdi-chevron-right' : 'mdi mdi-chevron-left';
updateMenuTitles(sidebar);
}
function applyWidth(clientX) {
preferences.collapsed = false;
preferences.width = normalizeWidth(clientX);
applyRootLayout();
updateHandle();
}
async function persist(action, width, fallback) {
try {
const response = await fetch(preferences.layout_update_url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'X-CSRFToken': preferences.csrf_token,
},
body: new URLSearchParams({action: button.dataset.action}),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const saved = await response.json();
const saved = await saveLayout(action, width);
preferences.collapsed = Boolean(saved.collapsed);
preferences.width = normalizeWidth(saved.width);
controls.classList.remove('text-danger');
controls.removeAttribute('title');
handle.classList.remove('netbox-utilities-sidebar-save-error');
handle.removeAttribute('data-save-error');
} catch (error) {
preferences.collapsed = previous.collapsed;
preferences.width = previous.width;
controls.classList.add('text-danger');
controls.title = 'Die Navigationseinstellung konnte nicht gespeichert werden.';
preferences.collapsed = fallback.collapsed;
preferences.width = fallback.width;
handle.classList.add('netbox-utilities-sidebar-save-error');
handle.dataset.saveError = 'Die Navigationseinstellung konnte nicht gespeichert werden.';
console.error('Could not save NetBox navigation layout', error);
}
applyRootLayout();
updateControls(controls, sidebar);
updateHandle();
}
toggle.addEventListener('click', async () => {
if (toggle.disabled) return;
const previous = {collapsed: preferences.collapsed, width: preferences.width};
preferences.collapsed = !preferences.collapsed;
applyRootLayout();
updateHandle();
toggle.disabled = true;
await persist('toggle', undefined, previous);
toggle.disabled = false;
});
handle.addEventListener('pointerdown', (event) => {
if (event.target.closest('button')) return;
event.preventDefault();
resizing = true;
previousLayout = {collapsed: preferences.collapsed, width: preferences.width};
handle.setPointerCapture(event.pointerId);
root.classList.add('netbox-utilities-navigation-resizing');
applyWidth(event.clientX);
});
handle.addEventListener('pointermove', (event) => {
if (resizing) applyWidth(event.clientX);
});
async function finishResize(event) {
if (!resizing) return;
resizing = false;
if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);
root.classList.remove('netbox-utilities-navigation-resizing');
const fallback = previousLayout;
previousLayout = null;
await persist('resize', preferences.width, fallback);
}
handle.addEventListener('pointerup', finishResize);
handle.addEventListener('pointercancel', finishResize);
handle.addEventListener('keydown', async (event) => {
const minimum = Number(preferences.width_min) || 216;
const maximum = Number(preferences.width_max) || 408;
const directions = {ArrowLeft: -16, ArrowRight: 16};
let width;
if (event.key in directions) width = preferences.width + directions[event.key];
if (event.key === 'Home') width = minimum;
if (event.key === 'End') width = maximum;
if (width === undefined) return;
event.preventDefault();
const previous = {collapsed: preferences.collapsed, width: preferences.width};
applyWidth(width);
await persist('resize', preferences.width, previous);
});
updateHandle();
}
function initializeNavigation() {
const sidebarMenu = document.getElementById('sidebar-menu');
const sidebar = document.querySelector('#sidebar-menu > ul.navbar-nav');
if (!sidebarMenu || !sidebar) return;
const sidebarElement = document.querySelector('.page > aside.navbar-vertical.navbar-expand-lg');
if (!sidebarMenu || !sidebar || !sidebarElement) return;
applyMenuPreferences(sidebar);
updateMenuTitles(sidebar);
configureCollapsedDropdowns(sidebar);
addLayoutControls(sidebarMenu, sidebar);
addResizeHandle(sidebarElement, sidebar);
}
applyRootLayout();
@@ -10,7 +10,7 @@
white-space: nowrap;
}
.netbox-utilities-sidebar-controls {
.netbox-utilities-sidebar-resizer {
display: none;
}
@@ -38,24 +38,77 @@
width: var(--netbox-utilities-sidebar-effective-width);
}
.netbox-utilities-sidebar-controls {
position: sticky;
bottom: 0.5rem;
z-index: 2;
display: flex;
justify-content: center;
gap: 0.25rem;
margin: 0.75rem;
padding: 0.35rem;
border: 1px solid var(--tblr-border-color-translucent);
border-radius: var(--tblr-border-radius);
background: var(--tblr-bg-surface);
box-shadow: var(--tblr-box-shadow-sm);
.netbox-utilities-sidebar-resizer {
position: fixed;
top: 0;
bottom: 0;
left: calc(var(--netbox-utilities-sidebar-effective-width) - 5px);
z-index: 1045;
display: block;
width: 10px;
cursor: col-resize;
touch-action: none;
outline: 0;
transition: left 160ms ease;
}
.netbox-utilities-sidebar-control {
flex: 0 1 3rem;
padding-inline: 0.5rem;
.netbox-utilities-sidebar-resizer::before {
position: absolute;
top: 0;
bottom: 0;
left: 4px;
width: 2px;
content: '';
background: transparent;
transition: background-color 120ms ease;
}
.netbox-utilities-sidebar-resizer:hover::before,
.netbox-utilities-sidebar-resizer:focus-visible::before,
html.netbox-utilities-navigation-resizing .netbox-utilities-sidebar-resizer::before {
background: var(--tblr-primary);
}
.netbox-utilities-sidebar-toggle {
position: absolute;
top: 50%;
left: 50%;
display: grid;
width: 24px;
height: 44px;
padding: 0;
color: var(--tblr-secondary-color);
cursor: pointer;
background: var(--tblr-bg-surface);
border: 1px solid var(--tblr-border-color);
border-radius: 0 var(--tblr-border-radius) var(--tblr-border-radius) 0;
box-shadow: var(--tblr-box-shadow-sm);
transform: translate(0, -50%);
place-items: center;
}
.netbox-utilities-sidebar-toggle:hover,
.netbox-utilities-sidebar-toggle:focus-visible {
color: var(--tblr-primary);
border-color: var(--tblr-primary);
outline: 0;
}
.netbox-utilities-sidebar-save-error::before {
background: var(--tblr-danger) !important;
}
html.netbox-utilities-navigation-resizing,
html.netbox-utilities-navigation-resizing body {
cursor: col-resize !important;
user-select: none;
}
html.netbox-utilities-navigation-resizing .page > aside.navbar-vertical.navbar-expand-lg,
html.netbox-utilities-navigation-resizing .page > aside.navbar-vertical.navbar-expand-lg ~ .navbar,
html.netbox-utilities-navigation-resizing .page > aside.navbar-vertical.navbar-expand-lg ~ .page-wrapper,
html.netbox-utilities-navigation-resizing .netbox-utilities-sidebar-resizer {
transition: none;
}
html.netbox-utilities-navigation-collapsed .navbar-vertical.navbar-expand-lg > .container-fluid {
@@ -105,19 +158,17 @@
box-shadow: var(--tblr-box-shadow-lg);
}
html.netbox-utilities-navigation-collapsed .netbox-utilities-sidebar-controls {
margin-inline: 0;
}
html.netbox-utilities-navigation-collapsed .netbox-utilities-sidebar-control:not([data-action="toggle"]) {
display: none;
html.netbox-utilities-navigation-collapsed .netbox-utilities-sidebar-resizer {
cursor: e-resize;
}
}
@media (prefers-reduced-motion: reduce) {
html.netbox-utilities-navigation-layout .page > aside.navbar-vertical.navbar-expand-lg,
html.netbox-utilities-navigation-layout .page > aside.navbar-vertical.navbar-expand-lg ~ .navbar,
html.netbox-utilities-navigation-layout .page > aside.navbar-vertical.navbar-expand-lg ~ .page-wrapper {
html.netbox-utilities-navigation-layout .page > aside.navbar-vertical.navbar-expand-lg ~ .page-wrapper,
.netbox-utilities-sidebar-resizer,
.netbox-utilities-sidebar-resizer::before {
transition: none;
}
}
+2 -2
View File
@@ -4,12 +4,12 @@ from django.urls import reverse
from netbox.plugins import PluginTemplateExtension
from tenancy.models import Tenant, TenantGroup
from . import __version__
from .models import NavigationPreference
from .navigation_helpers import (
SIDEBAR_WIDTH_DEFAULT,
SIDEBAR_WIDTH_MAX,
SIDEBAR_WIDTH_MIN,
SIDEBAR_WIDTH_STEP,
get_visible_menus,
normalize_preferences,
normalize_sidebar_width,
@@ -43,7 +43,6 @@ class UtilitiesGlobalContent(PluginTemplateExtension):
"width": normalize_sidebar_width(preference.sidebar_width if preference else SIDEBAR_WIDTH_DEFAULT),
"width_min": SIDEBAR_WIDTH_MIN,
"width_max": SIDEBAR_WIDTH_MAX,
"width_step": SIDEBAR_WIDTH_STEP,
"layout_update_url": reverse("plugins:netbox_utilities:navigation_layout"),
"csrf_token": get_token(request),
}
@@ -52,6 +51,7 @@ class UtilitiesGlobalContent(PluginTemplateExtension):
{
"navigation_enabled": request.user.is_authenticated and navigation_customization_enabled(),
"navigation_preferences": preference_data,
"asset_version": __version__,
},
)
@@ -1,6 +1,6 @@
{% load static %}
<link rel="stylesheet" href="{% static 'netbox_utilities/netbox_utilities.css' %}">
<link rel="stylesheet" href="{% static 'netbox_utilities/netbox_utilities.css' %}?v={{ asset_version }}">
{% if navigation_enabled %}
{{ navigation_preferences|json_script:"netbox-utilities-navigation-data" }}
<script src="{% static 'netbox_utilities/navigation.js' %}" defer></script>
<script src="{% static 'netbox_utilities/navigation.js' %}?v={{ asset_version }}" defer></script>
{% endif %}
@@ -22,46 +22,11 @@
</div>
{% else %}
<p class="text-secondary">
Passe Breite und Darstellung an, verschiebe Menüs mit den Pfeilen und blende nicht benötigte Bereiche aus.
Verschiebe Menüs mit den Pfeilen und blende nicht benötigte Bereiche aus.
Berechtigungen von NetBox werden dadurch nicht verändert.
</p>
<form method="post" id="navigation-preferences-form">
{% csrf_token %}
<fieldset class="border rounded p-3 mb-4">
<legend class="float-none w-auto px-2 fs-4 mb-2">Darstellung</legend>
<div class="form-check form-switch mb-3">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="sidebar-collapsed"
name="sidebar_collapsed"
{% if sidebar_collapsed %}checked{% endif %}
>
<label class="form-check-label fw-medium" for="sidebar-collapsed">
Eingeklappt nur Hauptmenü-Icons anzeigen
</label>
</div>
<label class="form-label fw-medium" for="sidebar-width">
Breite im ausgeklappten Zustand:
<output id="sidebar-width-output" for="sidebar-width">{{ sidebar_width }} px</output>
</label>
<input
class="form-range"
type="range"
id="sidebar-width"
name="sidebar_width"
min="{{ sidebar_width_min }}"
max="{{ sidebar_width_max }}"
step="{{ sidebar_width_step }}"
value="{{ sidebar_width }}"
>
<div class="d-flex justify-content-between text-secondary small">
<span>Kleiner</span>
<span>Standard: 288 px</span>
<span>Größer</span>
</div>
</fieldset>
<div class="list-group mb-3" id="navigation-menu-editor">
{% for menu in menus %}
<div class="list-group-item d-flex align-items-center gap-3 navigation-menu-row">
@@ -119,14 +84,6 @@
}
});
}
const width = document.getElementById('sidebar-width');
const output = document.getElementById('sidebar-width-output');
if (width && output) {
width.addEventListener('input', () => {
output.value = `${width.value} px`;
});
}
})();
</script>
{% endblock content %}
@@ -29,11 +29,11 @@ class NormalizePreferencesTest(SimpleTestCase):
class SidebarLayoutTest(SimpleTestCase):
def test_normalizes_width_to_supported_steps(self):
def test_clamps_width_without_discrete_steps(self):
self.assertEqual(normalize_sidebar_width(None), SIDEBAR_WIDTH_DEFAULT)
self.assertEqual(normalize_sidebar_width(100), SIDEBAR_WIDTH_MIN)
self.assertEqual(normalize_sidebar_width(500), SIDEBAR_WIDTH_MAX)
self.assertEqual(normalize_sidebar_width(300), 312)
self.assertEqual(normalize_sidebar_width(300), 300)
def test_toggles_icon_only_mode(self):
collapsed, width = update_sidebar_layout(False, SIDEBAR_WIDTH_DEFAULT, "toggle")
@@ -41,12 +41,15 @@ class SidebarLayoutTest(SimpleTestCase):
self.assertTrue(collapsed)
self.assertEqual(width, SIDEBAR_WIDTH_DEFAULT)
def test_resizes_within_limits(self):
self.assertEqual(update_sidebar_layout(False, SIDEBAR_WIDTH_MIN, "smaller")[1], SIDEBAR_WIDTH_MIN)
self.assertEqual(update_sidebar_layout(False, SIDEBAR_WIDTH_MAX, "larger")[1], SIDEBAR_WIDTH_MAX)
self.assertLess(
update_sidebar_layout(False, SIDEBAR_WIDTH_DEFAULT, "smaller")[1],
SIDEBAR_WIDTH_DEFAULT,
def test_resizes_continuously_within_limits_and_expands(self):
self.assertEqual(update_sidebar_layout(True, SIDEBAR_WIDTH_DEFAULT, "resize", 301), (False, 301))
self.assertEqual(
update_sidebar_layout(False, SIDEBAR_WIDTH_DEFAULT, "resize", 100),
(False, SIDEBAR_WIDTH_MIN),
)
self.assertEqual(
update_sidebar_layout(False, SIDEBAR_WIDTH_DEFAULT, "resize", 500),
(False, SIDEBAR_WIDTH_MAX),
)
def test_rejects_unknown_action(self):
@@ -25,6 +25,24 @@ class NavigationLayoutViewTest(SimpleTestCase):
self.assertTrue(preference.sidebar_collapsed)
preference.save.assert_called_once_with(update_fields=("sidebar_collapsed", "sidebar_width", "updated"))
@patch("netbox_utilities.views.navigation_customization_enabled", return_value=True)
@patch("netbox_utilities.views.NavigationPreference.objects.get_or_create")
def test_saves_continuously_resized_width(self, get_or_create, _feature_enabled):
preference = MagicMock(sidebar_collapsed=True, sidebar_width=288)
get_or_create.return_value = (preference, False)
request = self.factory.post(
"/plugins/utilities/navigation/layout/",
{"action": "resize", "width": "317"},
)
request.user = MagicMock()
response = NavigationLayoutView().post(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content), {"collapsed": False, "width": 317})
self.assertFalse(preference.sidebar_collapsed)
self.assertEqual(preference.sidebar_width, 317)
@patch("netbox_utilities.views.navigation_customization_enabled", return_value=True)
@patch("netbox_utilities.views.NavigationPreference.objects.get_or_create")
def test_rejects_unknown_layout_action(self, get_or_create, _feature_enabled):
+1 -13
View File
@@ -20,12 +20,8 @@ from .models import NavigationPreference, UtilitiesSettings
from .module_installation import BulkModuleInstallError, install_modules
from .navigation_helpers import (
SIDEBAR_WIDTH_DEFAULT,
SIDEBAR_WIDTH_MAX,
SIDEBAR_WIDTH_MIN,
SIDEBAR_WIDTH_STEP,
get_visible_menus,
normalize_preferences,
normalize_sidebar_width,
update_sidebar_layout,
)
from .runtime import (
@@ -137,8 +133,6 @@ class NavigationPreferencesView(LoginRequiredMixin, View):
)
preference.menu_order = order
preference.hidden_menus = hidden
preference.sidebar_collapsed = request.POST.get("sidebar_collapsed") == "on"
preference.sidebar_width = normalize_sidebar_width(request.POST.get("sidebar_width"))
preference.save()
messages.success(request, "Die persönliche Navigation wurde gespeichert.")
return redirect("plugins:netbox_utilities:navigation_preferences")
@@ -159,13 +153,6 @@ class NavigationPreferencesView(LoginRequiredMixin, View):
{
"menus": menus,
"feature_enabled": navigation_customization_enabled(),
"sidebar_collapsed": preference.sidebar_collapsed if preference else False,
"sidebar_width": normalize_sidebar_width(
preference.sidebar_width if preference else SIDEBAR_WIDTH_DEFAULT
),
"sidebar_width_min": SIDEBAR_WIDTH_MIN,
"sidebar_width_max": SIDEBAR_WIDTH_MAX,
"sidebar_width_step": SIDEBAR_WIDTH_STEP,
},
)
@@ -183,6 +170,7 @@ class NavigationLayoutView(LoginRequiredMixin, View):
preference.sidebar_collapsed,
preference.sidebar_width,
request.POST.get("action"),
request.POST.get("width"),
)
except ValueError as error:
return HttpResponseBadRequest(str(error))