Skip to content

Commit 5a6479a

Browse files
committed
Rework nav icons to be on the sections if there are sections
1 parent 428a64f commit 5a6479a

File tree

11 files changed

+141
-87
lines changed

11 files changed

+141
-87
lines changed

plain-admin/plain/admin/assets/admin/admin.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,35 @@ jQuery(($) => {
8888
updateActiveNav();
8989
});
9090

91-
// Simple navigation section toggle with slide animation
91+
// Navigation section toggle with accordion behavior
9292
$(document).on("click", "[data-nav-toggle]", function (e) {
9393
e.preventDefault();
9494
const sectionId = $(this).data("nav-toggle");
9595
const $section = $(`#${sectionId}`);
9696
const $svg = $(this).find("svg").last();
97+
const isCurrentlyOpen = $section.is(":visible");
9798

98-
$section.slideToggle(80);
99-
$svg.toggleClass("rotate-180");
99+
// Close all other sections in the same nav area
100+
const navArea = $(this).closest("div").find("[data-nav-toggle]");
101+
navArea.each(function () {
102+
const otherSectionId = $(this).data("nav-toggle");
103+
const $otherSection = $(`#${otherSectionId}`);
104+
const $otherSvg = $(this).find("svg").last();
105+
106+
if (otherSectionId !== sectionId) {
107+
$otherSection.slideUp(80);
108+
$otherSvg.removeClass("rotate-180");
109+
}
110+
});
111+
112+
// Toggle the clicked section
113+
if (isCurrentlyOpen) {
114+
$section.slideUp(80);
115+
$svg.removeClass("rotate-180");
116+
} else {
117+
$section.slideDown(80);
118+
$svg.addClass("rotate-180");
119+
}
100120
});
101121

102122
function updateActiveNav() {

plain-admin/plain/admin/templates/admin/base.html

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -66,30 +66,44 @@
6666
<div class="mt-4 mb-2 px-2 text-xs text-stone-400 uppercase tracking-wider">App</div>
6767

6868
{% for section, views in app_sections.items() %}
69-
{% if section %}
69+
{% if section.name %}
7070
<button
7171
data-nav-toggle="app-section-{{ loop.index }}"
7272
class="flex items-center justify-between w-full px-2 py-1 mt-1 text-sm rounded hover:bg-white/5 cursor-pointer"
73-
title="Toggle {{ section }}">
74-
<span class="text-stone-300/90">{{ section }}</span>
73+
title="Toggle {{ section.name }}">
74+
<div class="flex items-center">
75+
<admin.Icon name={section.icon} class="w-3.5 h-3.5 mr-2 flex-shrink-0 text-stone-400" />
76+
<span class="text-stone-300/90">{{ section.name }}</span>
77+
</div>
7578
<admin.Icon name="chevron-down" class="w-3.5 h-3.5 transition-transform text-stone-500" data-nav-toggle-icon />
7679
</button>
7780
<div id="app-section-{{ loop.index }}" style="display: none;">
81+
{% for view in views %}
82+
{% set url = view.get_view_url() %}
83+
<a
84+
{% if url == request.path or view in parent_view_classes %}data-active{% endif %}
85+
class="data-[active]:text-blue-400 flex items-center px-2 py-0.5 mt-px text-sm rounded hover:text-stone-300 text-stone-300/90 hover:bg-white/5 pl-8"
86+
href="{{ url }}"
87+
hx-boost="true">
88+
<span class="truncate">{{ view.get_nav_title() }}</span>
89+
</a>
90+
{% endfor %}
91+
</div>
7892
{% else %}
7993
<div class="mb-4">
80-
{% endif %}
8194
{% for view in views %}
8295
{% set url = view.get_view_url() %}
8396
<a
8497
{% if url == request.path or view in parent_view_classes %}data-active{% endif %}
85-
class="data-[active]:bg-white/20 data-[active]:text-white flex items-center px-2 py-0.5 mt-px text-sm rounded hover:text-stone-300 text-stone-300/90 hover:bg-white/5"
98+
class="data-[active]:text-blue-400 flex items-center px-2 py-0.5 mt-px text-sm rounded hover:text-stone-300 text-stone-300/90 hover:bg-white/5"
8699
href="{{ url }}"
87100
hx-boost="true">
88-
<admin.Icon name={view.get_nav_icon()} class="w-3.5 h-3.5 mr-3 flex-shrink-0" />
101+
<admin.Icon name={view.nav_icon} class="w-3.5 h-3.5 mr-3 flex-shrink-0 text-stone-400" />
89102
<span class="truncate">{{ view.get_nav_title() }}</span>
90103
</a>
91104
{% endfor %}
92105
</div>
106+
{% endif %}
93107
{% endfor %}
94108
</div>
95109
{% endif %}
@@ -100,30 +114,44 @@
100114
<div class="mb-2 px-2 text-xs text-stone-400 uppercase tracking-wider">Plain</div>
101115

102116
{% for section, views in plain_sections.items() %}
103-
{% if section %}
117+
{% if section.name %}
104118
<button
105119
data-nav-toggle="plain-section-{{ loop.index }}"
106120
class="flex items-center justify-between w-full px-2 py-1 mt-1 text-sm rounded hover:bg-white/5 cursor-pointer"
107-
title="Toggle {{ section }}">
108-
<span class="text-stone-300/90">{{ section }}</span>
121+
title="Toggle {{ section.name }}">
122+
<div class="flex items-center">
123+
<admin.Icon name={section.icon} class="w-3.5 h-3.5 mr-2 flex-shrink-0 text-stone-400" />
124+
<span class="text-stone-300/90">{{ section.name }}</span>
125+
</div>
109126
<admin.Icon name="chevron-down" class="w-3.5 h-3.5 transition-transform text-stone-500" data-nav-toggle-icon />
110127
</button>
111128
<div id="plain-section-{{ loop.index }}" style="display: none;">
129+
{% for view in views %}
130+
{% set url = view.get_view_url() %}
131+
<a
132+
{% if url == request.path or view in parent_view_classes %}data-active{% endif %}
133+
class="data-[active]:text-blue-400 flex items-center px-2 py-0.5 mt-px text-sm rounded hover:text-stone-300 text-stone-300/90 hover:bg-white/5 pl-8"
134+
href="{{ url }}"
135+
hx-boost="true">
136+
<span class="truncate">{{ view.get_nav_title() }}</span>
137+
</a>
138+
{% endfor %}
139+
</div>
112140
{% else %}
113141
<div>
114-
{% endif %}
115142
{% for view in views %}
116143
{% set url = view.get_view_url() %}
117144
<a
118145
{% if url == request.path or view in parent_view_classes %}data-active{% endif %}
119-
class="data-[active]:bg-white/20 data-[active]:text-white flex items-center px-2 py-0.5 mt-px text-sm rounded hover:text-stone-300 text-stone-300/90 hover:bg-white/5"
146+
class="data-[active]:text-blue-400 flex items-center px-2 py-0.5 mt-px text-sm rounded hover:text-stone-300 text-stone-300/90 hover:bg-white/5"
120147
href="{{ url }}"
121148
hx-boost="true">
122-
<admin.Icon name={view.get_nav_icon()} class="w-3.5 h-3.5 mr-3 flex-shrink-0" />
149+
<admin.Icon name={view.nav_icon} class="w-3.5 h-3.5 mr-3 flex-shrink-0 text-stone-400" />
123150
<span class="truncate">{{ view.get_nav_title() }}</span>
124151
</a>
125152
{% endfor %}
126153
</div>
154+
{% endif %}
127155
{% endfor %}
128156
</div>
129157
{% endif %}
@@ -160,41 +188,27 @@
160188
<div class="flex-shrink-0 overflow-hidden">
161189
{% block header %}
162190
<div class="flex items-center">
163-
{% block image %}
164-
{% if image %}
165-
<img src="{{ image.src }}" alt="{{ image.alt }}" class="h-20 rounded mr-3 flex-shrink-0">
166-
{% endif %}
167-
{% endblock %}
168191
<div class="truncate">
169192
<nav class="flex items-center text-sm sm:text-base lg:text-xl text-white/90 space-x-1" aria-label="Breadcrumb">
170-
{% set current_nav_section = view_class.get_nav_section() %}
171-
{% if parent_view_classes or current_nav_section %}
172-
{% set ns = namespace(last_section='') %}
173-
{% for parent_view in parent_view_classes|reverse %}
174-
{% if parent_view.get_nav_section() and parent_view.get_nav_section() != ns.last_section %}
175-
{% if ns.last_section %}
176-
<admin.Icon name="chevron-right" class="w-3 h-3 text-white/50" />
177-
{% endif %}
178-
<span class="text-white/40 text-sm">{{ parent_view.get_nav_section() }}</span>
179-
<admin.Icon name="chevron-right" class="w-3 h-3 text-white/50" />
180-
{% set ns.last_section = parent_view.get_nav_section() %}
181-
{% elif loop.index > 1 %}
182-
<admin.Icon name="chevron-right" class="w-3 h-3 text-white/50" />
183-
{% endif %}
184-
<a href="{{ parent_view.get_view_url(object if object is defined else None) }}" class="inline-flex items-center hover:text-white/70 text-white/50 text-sm">
185-
{{ parent_view.get_nav_title() }}
186-
</a>
187-
{% endfor %}
188-
{% if current_nav_section and current_nav_section != ns.last_section %}
189-
{% if parent_view_classes %}
193+
{% for parent_view in parent_view_classes|reverse %}
194+
{% if parent_view.nav_section %}
195+
<span class="text-white/40 text-sm">{{ parent_view.nav_section }}</span>
190196
<admin.Icon name="chevron-right" class="w-3 h-3 text-white/50" />
191-
{% endif %}
192-
<span class="text-white/40 text-sm">{{ current_nav_section }}</span>
193197
{% endif %}
194-
{% if parent_view_classes or current_nav_section %}
198+
<a href="{{ parent_view.get_view_url(object if object is defined else None) }}" class="inline-flex items-center hover:text-white/70 text-white/50 text-sm">
199+
{{ parent_view.get_nav_title() }}
200+
</a>
201+
<admin.Icon name="chevron-right" class="w-3 h-3 text-white/50" />
202+
{% endfor %}
203+
{% if view_class.nav_section and not parent_view_classes %}
204+
<span class="text-white/40 text-sm">{{ view_class.nav_section }}</span>
195205
<admin.Icon name="chevron-right" class="w-3 h-3 text-white/50" />
196-
{% endif %}
197206
{% endif %}
207+
{% block image %}
208+
{% if image %}
209+
<img src="{{ image.src }}" alt="{{ image.alt }}" class="h-5 rounded mr-2 flex-shrink-0">
210+
{% endif %}
211+
{% endblock %}
198212
<h1 class="font-semibold text-sm">{% block title %}{{ title }}{% endblock %}</h1>
199213
</nav>
200214
</div>

plain-admin/plain/admin/views/base.py

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING
1+
from typing import TYPE_CHECKING, Optional
22

33
from plain.auth.views import AuthViewMixin
44
from plain.urls import reverse
@@ -29,13 +29,13 @@ class AdminView(AuthViewMixin, TemplateView):
2929
# An explicit disabling of showing this url/page in the nav
3030
# which importantly effects the (future) recent pages list
3131
# so you can also use this for pages that can never be bookmarked
32-
nav_section = ""
3332
nav_title = ""
33+
nav_section = ""
3434
nav_icon = "app"
3535

36-
links: dict[str] = {}
36+
links: dict[str, str] = {}
3737

38-
parent_view_class: "AdminView" = None
38+
parent_view_class: Optional["AdminView"] = None
3939

4040
template_name = "admin/page.html"
4141
cards: list["Card"] = []
@@ -90,17 +90,6 @@ def get_parent_view_classes(cls) -> list["AdminView"]:
9090
parent = parent.parent_view_class
9191
return parents
9292

93-
@classmethod
94-
def get_nav_section(cls) -> str | None:
95-
if not cls.nav_section:
96-
return cls.nav_section # Could be None or ""
97-
98-
if cls.parent_view_class:
99-
# Don't show child views by default
100-
return None
101-
102-
return cls.nav_section
103-
10493
@classmethod
10594
def get_nav_title(cls) -> str:
10695
if cls.nav_title:
@@ -113,10 +102,6 @@ def get_nav_title(cls) -> str:
113102
f"Please set a title or nav_title on the {cls} class or implement get_nav_title()."
114103
)
115104

116-
@classmethod
117-
def get_nav_icon(cls) -> str:
118-
return cls.nav_icon
119-
120105
@classmethod
121106
def get_view_url(cls, obj=None) -> str:
122107
# Check if this view's path expects an id parameter

plain-admin/plain/admin/views/registry.py

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
from plain.urls import path, reverse_lazy
22

33

4+
class NavSection:
5+
def __init__(self, name: str, icon: str = "folder"):
6+
self.name = name
7+
self.icon = icon
8+
9+
def __str__(self):
10+
return self.name
11+
12+
def __eq__(self, other):
13+
if isinstance(other, NavSection):
14+
return self.name == other.name
15+
return self.name == other
16+
17+
def __hash__(self):
18+
return hash(self.name)
19+
20+
421
class AdminViewRegistry:
522
def __init__(self):
623
# View classes that will be added to the admin automatically
@@ -32,57 +49,77 @@ def inner(viewset):
3249
def get_app_nav_sections(self):
3350
"""Returns nav sections for app/user packages only."""
3451
sections = {}
52+
section_icons = {} # Track icons per section
3553

3654
for view in self.registered_views:
3755
# Skip plain package views
3856
if view.__module__.startswith("plain."):
3957
continue
4058

41-
section = view.get_nav_section()
59+
section_name = view.nav_section
4260

4361
# Skip views with nav_section = None (don't show in nav)
44-
if section is None:
62+
# But allow empty string "" for ungrouped items
63+
if section_name is None:
4564
continue
4665

47-
if section not in sections:
48-
sections[section] = []
49-
sections[section].append(view)
66+
# Set section icon if this view defines one and we don't have one yet
67+
if view.nav_icon and section_name not in section_icons:
68+
section_icons[section_name] = view.nav_icon
69+
70+
# Create or get the NavSection
71+
section_icon = section_icons.get(section_name, "folder")
72+
nav_section = NavSection(section_name, section_icon)
73+
74+
if nav_section not in sections:
75+
sections[nav_section] = []
76+
sections[nav_section].append(view)
5077

5178
# Sort each section by nav_title
5279
for section in sections.values():
5380
section.sort(key=lambda v: v.get_nav_title())
5481

5582
# Sort sections alphabetically, but put empty string first
5683
def section_sort_key(item):
57-
section_name = item[0]
84+
section_name = item[0].name
5885
return ("z" if section_name else "", section_name)
5986

6087
return dict(sorted(sections.items(), key=section_sort_key))
6188

6289
def get_plain_nav_sections(self):
6390
"""Returns nav sections for plain packages only."""
6491
sections = {}
92+
section_icons = {} # Track icons per section
6593

6694
for view in self.registered_views:
6795
# Only include plain package views
6896
if not view.__module__.startswith("plain."):
6997
continue
7098

71-
section = view.get_nav_section()
99+
section_name = view.nav_section
72100
# Skip views with nav_section = None (don't show in nav)
73-
if section is None:
101+
# But allow empty string "" for ungrouped items
102+
if section_name is None:
74103
continue
75104

76-
if section not in sections:
77-
sections[section] = []
78-
sections[section].append(view)
105+
# Set section icon if this view defines one and we don't have one yet
106+
if view.nav_icon and section_name not in section_icons:
107+
section_icons[section_name] = view.nav_icon
108+
109+
# Create or get the NavSection
110+
section_icon = section_icons.get(section_name, "folder")
111+
nav_section = NavSection(section_name, section_icon)
112+
113+
if nav_section not in sections:
114+
sections[nav_section] = []
115+
sections[nav_section].append(view)
79116

80117
# Sort each section by nav_title
81118
for section in sections.values():
82119
section.sort(key=lambda v: v.get_nav_title())
83120

84121
# Sort sections alphabetically
85-
return dict(sorted(sections.items()))
122+
return dict(sorted(sections.items(), key=lambda item: item[0].name))
86123

87124
def get_urls(self):
88125
urls = []

plain-cache/plain/cache/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
class CachedItemViewset(AdminViewset):
1212
class ListView(AdminModelListView):
1313
nav_section = "Cache"
14+
nav_icon = "archive"
1415
model = CachedItem
1516
title = "Cached items"
16-
nav_icon = "archive"
1717
fields = [
1818
"key",
1919
"created_at",

plain-oauth/plain/oauth/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ def get_chart_data(self) -> dict:
3737
class OAuthConnectionViewset(AdminViewset):
3838
class ListView(AdminModelListView):
3939
nav_section = "OAuth"
40+
nav_icon = "link-45deg"
4041
model = OAuthConnection
4142
title = "Connections"
42-
nav_icon = "link-45deg"
4343
fields = ["id", "user", "provider_key", "provider_user_id"]
4444
cards = [ProvidersChartCard]
4545

0 commit comments

Comments
 (0)