FossilRepo

Add pagination, search, and filtering to all 18 list views Created reusable pagination partials (_pagination.html for Django Paginator, _pagination_manual.html for FossilReader lists). Added _manual_paginate() helper for non-queryset pagination. All list views now have: search input with HTMX live search (300ms debounce), pagination (25 items/page), and context-aware empty states. Covers: wiki, forum, releases, technotes, branches, tags, unversioned files, webhooks, API tokens, branch protection, ticket fields, ticket reports, projects, groups, members, teams, pages, audit log.

lmata 2026-04-07 15:59 trunk
Commit 4e1dde0f6daf98a49ebe7324af56749aaaaf05247adae068fd5b4e4d724cc2ec
+175 -12
--- fossil/views.py
+++ fossil/views.py
@@ -1,11 +1,13 @@
11
import contextlib
2
+import math
23
import re
34
from datetime import datetime
45
56
import markdown as md
67
from django.contrib.auth.decorators import login_required
8
+from django.core.paginator import Paginator
79
from django.http import Http404, HttpResponse, JsonResponse
810
from django.shortcuts import get_object_or_404, redirect, render
911
from django.utils.safestring import mark_safe
1012
from django.views.decorators.csrf import csrf_exempt
1113
@@ -13,10 +15,37 @@
1315
from projects.models import Project
1416
1517
from .models import FossilRepository
1618
from .reader import FossilReader
1719
20
+
21
+def _manual_paginate(items, request, per_page=25):
22
+ """Paginate a plain list (FossilReader results) and return (sliced_items, pagination_dict).
23
+
24
+ The pagination dict has keys compatible with the _pagination_manual.html partial:
25
+ has_previous, has_next, previous_page_number, next_page_number, number, num_pages, count.
26
+ """
27
+ total = len(items)
28
+ num_pages = max(1, math.ceil(total / per_page))
29
+ try:
30
+ page = int(request.GET.get("page", 1))
31
+ except (ValueError, TypeError):
32
+ page = 1
33
+ page = max(1, min(page, num_pages))
34
+ offset = (page - 1) * per_page
35
+ sliced = items[offset : offset + per_page]
36
+ pagination = {
37
+ "has_previous": page > 1,
38
+ "has_next": offset + per_page < total,
39
+ "previous_page_number": page - 1,
40
+ "next_page_number": page + 1,
41
+ "number": page,
42
+ "num_pages": num_pages,
43
+ "count": total,
44
+ }
45
+ return sliced, pagination
46
+
1847
1948
def _render_fossil_content(content: str, project_slug: str = "", base_path: str = "") -> str:
2049
"""Render content that may be Fossil wiki markup, HTML, or Markdown.
2150
2251
Fossil wiki pages can contain:
@@ -682,12 +711,10 @@
682711
683712
if search:
684713
tickets = [t for t in tickets if search.lower() in t.title.lower()]
685714
686715
total = len(tickets)
687
- import math
688
-
689716
total_pages = max(1, math.ceil(total / per_page))
690717
page = min(page, total_pages)
691718
tickets = tickets[(page - 1) * per_page : page * per_page]
692719
has_next = page < total_pages
693720
has_prev = page > 1
@@ -759,13 +786,35 @@
759786
760787
with reader:
761788
pages = reader.get_wiki_pages()
762789
home_page = reader.get_wiki_page("Home")
763790
791
+ search = request.GET.get("search", "").strip()
792
+ if search:
793
+ pages = [p for p in pages if search.lower() in p.name.lower()]
794
+
795
+ pages, pagination = _manual_paginate(pages, request)
796
+
764797
home_content_html = ""
765798
if home_page:
766799
home_content_html = mark_safe(sanitize_html(_render_fossil_content(home_page.content, project_slug=slug)))
800
+
801
+ if request.headers.get("HX-Request"):
802
+ return render(
803
+ request,
804
+ "fossil/wiki_list.html",
805
+ {
806
+ "project": project,
807
+ "fossil_repo": fossil_repo,
808
+ "pages": pages,
809
+ "home_page": home_page,
810
+ "home_content_html": home_content_html,
811
+ "search": search,
812
+ "pagination": pagination,
813
+ "active_tab": "wiki",
814
+ },
815
+ )
767816
768817
return render(
769818
request,
770819
"fossil/wiki_list.html",
771820
{
@@ -772,10 +821,12 @@
772821
"project": project,
773822
"fossil_repo": fossil_repo,
774823
"pages": pages,
775824
"home_page": home_page,
776825
"home_content_html": home_content_html,
826
+ "search": search,
827
+ "pagination": pagination,
777828
"active_tab": "wiki",
778829
},
779830
)
780831
781832
@@ -844,10 +895,17 @@
844895
)
845896
846897
# Sort merged list by timestamp descending
847898
merged.sort(key=lambda x: x["timestamp"], reverse=True)
848899
900
+ search = request.GET.get("search", "").strip()
901
+ if search:
902
+ search_lower = search.lower()
903
+ merged = [p for p in merged if search_lower in (p.get("title") or "").lower() or search_lower in (p.get("body") or "").lower()]
904
+
905
+ merged, pagination = _manual_paginate(merged, request)
906
+
849907
has_write = can_write_project(request.user, project)
850908
851909
return render(
852910
request,
853911
"fossil/forum_list.html",
@@ -854,10 +912,12 @@
854912
{
855913
"project": project,
856914
"fossil_repo": fossil_repo,
857915
"posts": merged,
858916
"has_write": has_write,
917
+ "search": search,
918
+ "pagination": pagination,
859919
"active_tab": "forum",
860920
},
861921
)
862922
863923
@@ -1026,17 +1086,26 @@
10261086
10271087
from fossil.webhooks import Webhook
10281088
10291089
webhooks = Webhook.objects.filter(repository=fossil_repo)
10301090
1091
+ search = request.GET.get("search", "").strip()
1092
+ if search:
1093
+ webhooks = webhooks.filter(url__icontains=search)
1094
+
1095
+ paginator = Paginator(webhooks, 25)
1096
+ page_obj = paginator.get_page(request.GET.get("page", 1))
1097
+
10311098
return render(
10321099
request,
10331100
"fossil/webhook_list.html",
10341101
{
10351102
"project": project,
10361103
"fossil_repo": fossil_repo,
1037
- "webhooks": webhooks,
1104
+ "webhooks": page_obj,
1105
+ "page_obj": page_obj,
1106
+ "search": search,
10381107
"active_tab": "settings",
10391108
},
10401109
)
10411110
10421111
@@ -1888,16 +1957,30 @@
18881957
project, fossil_repo, reader = _get_repo_and_reader(slug, request)
18891958
18901959
with reader:
18911960
notes = reader.get_technotes()
18921961
1962
+ search = request.GET.get("search", "").strip()
1963
+ if search:
1964
+ search_lower = search.lower()
1965
+ notes = [n for n in notes if search_lower in (n.comment or "").lower()]
1966
+
1967
+ notes, pagination = _manual_paginate(notes, request)
1968
+
18931969
has_write = can_write_project(request.user, project)
18941970
18951971
return render(
18961972
request,
18971973
"fossil/technote_list.html",
1898
- {"project": project, "notes": notes, "has_write": has_write, "active_tab": "wiki"},
1974
+ {
1975
+ "project": project,
1976
+ "notes": notes,
1977
+ "has_write": has_write,
1978
+ "search": search,
1979
+ "pagination": pagination,
1980
+ "active_tab": "wiki",
1981
+ },
18991982
)
19001983
19011984
19021985
@login_required
19031986
def technote_create(request, slug):
@@ -2010,19 +2093,28 @@
20102093
project, fossil_repo, reader = _get_repo_and_reader(slug, request)
20112094
20122095
with reader:
20132096
files = reader.get_unversioned_files()
20142097
2098
+ search = request.GET.get("search", "").strip()
2099
+ if search:
2100
+ search_lower = search.lower()
2101
+ files = [f for f in files if search_lower in f.name.lower()]
2102
+
2103
+ files, pagination = _manual_paginate(files, request)
2104
+
20152105
has_admin = can_admin_project(request.user, project)
20162106
20172107
return render(
20182108
request,
20192109
"fossil/unversioned_list.html",
20202110
{
20212111
"project": project,
20222112
"files": files,
20232113
"has_admin": has_admin,
2114
+ "search": search,
2115
+ "pagination": pagination,
20242116
"active_tab": "files",
20252117
},
20262118
)
20272119
20282120
@@ -2331,17 +2423,26 @@
23312423
project, fossil_repo, reader = _get_repo_and_reader(slug, request)
23322424
23332425
with reader:
23342426
branches = reader.get_branches()
23352427
2428
+ search = request.GET.get("search", "").strip()
2429
+ if search:
2430
+ search_lower = search.lower()
2431
+ branches = [b for b in branches if search_lower in b.name.lower()]
2432
+
2433
+ branches, pagination = _manual_paginate(branches, request)
2434
+
23362435
return render(
23372436
request,
23382437
"fossil/branch_list.html",
23392438
{
23402439
"project": project,
23412440
"fossil_repo": fossil_repo,
23422441
"branches": branches,
2442
+ "search": search,
2443
+ "pagination": pagination,
23432444
"active_tab": "code",
23442445
},
23452446
)
23462447
23472448
@@ -2352,14 +2453,27 @@
23522453
project, fossil_repo, reader = _get_repo_and_reader(slug, request)
23532454
23542455
with reader:
23552456
tags = reader.get_tags()
23562457
2458
+ search = request.GET.get("search", "").strip()
2459
+ if search:
2460
+ search_lower = search.lower()
2461
+ tags = [t for t in tags if search_lower in t.name.lower()]
2462
+
2463
+ tags, pagination = _manual_paginate(tags, request)
2464
+
23572465
return render(
23582466
request,
23592467
"fossil/tag_list.html",
2360
- {"project": project, "tags": tags, "active_tab": "code"},
2468
+ {
2469
+ "project": project,
2470
+ "tags": tags,
2471
+ "search": search,
2472
+ "pagination": pagination,
2473
+ "active_tab": "code",
2474
+ },
23612475
)
23622476
23632477
23642478
# --- Raw File Download ---
23652479
@@ -2793,18 +2907,28 @@
27932907
27942908
has_write = can_write_project(request.user, project)
27952909
if not has_write:
27962910
releases = releases.filter(is_draft=False)
27972911
2912
+ search = request.GET.get("search", "").strip()
2913
+ if search:
2914
+ releases = releases.filter(tag_name__icontains=search) | releases.filter(name__icontains=search)
2915
+ releases = releases.distinct()
2916
+
2917
+ paginator = Paginator(releases, 25)
2918
+ page_obj = paginator.get_page(request.GET.get("page", 1))
2919
+
27982920
return render(
27992921
request,
28002922
"fossil/release_list.html",
28012923
{
28022924
"project": project,
28032925
"fossil_repo": fossil_repo,
2804
- "releases": releases,
2926
+ "releases": page_obj,
2927
+ "page_obj": page_obj,
28052928
"has_write": has_write,
2929
+ "search": search,
28062930
"active_tab": "releases",
28072931
},
28082932
)
28092933
28102934
@@ -3197,17 +3321,26 @@
31973321
31983322
from fossil.api_tokens import APIToken
31993323
32003324
tokens = APIToken.objects.filter(repository=fossil_repo)
32013325
3326
+ search = request.GET.get("search", "").strip()
3327
+ if search:
3328
+ tokens = tokens.filter(name__icontains=search)
3329
+
3330
+ paginator = Paginator(tokens, 25)
3331
+ page_obj = paginator.get_page(request.GET.get("page", 1))
3332
+
32023333
return render(
32033334
request,
32043335
"fossil/api_token_list.html",
32053336
{
32063337
"project": project,
32073338
"fossil_repo": fossil_repo,
3208
- "tokens": tokens,
3339
+ "tokens": page_obj,
3340
+ "page_obj": page_obj,
3341
+ "search": search,
32093342
"active_tab": "settings",
32103343
},
32113344
)
32123345
32133346
@@ -3284,17 +3417,26 @@
32843417
32853418
from fossil.branch_protection import BranchProtection
32863419
32873420
rules = BranchProtection.objects.filter(repository=fossil_repo)
32883421
3422
+ search = request.GET.get("search", "").strip()
3423
+ if search:
3424
+ rules = rules.filter(branch_pattern__icontains=search)
3425
+
3426
+ paginator = Paginator(rules, 25)
3427
+ page_obj = paginator.get_page(request.GET.get("page", 1))
3428
+
32893429
return render(
32903430
request,
32913431
"fossil/branch_protection_list.html",
32923432
{
32933433
"project": project,
32943434
"fossil_repo": fossil_repo,
3295
- "rules": rules,
3435
+ "rules": page_obj,
3436
+ "page_obj": page_obj,
3437
+ "search": search,
32963438
"active_tab": "settings",
32973439
},
32983440
)
32993441
33003442
@@ -3421,17 +3563,27 @@
34213563
34223564
from fossil.ticket_fields import TicketFieldDefinition
34233565
34243566
fields = TicketFieldDefinition.objects.filter(repository=fossil_repo)
34253567
3568
+ search = request.GET.get("search", "").strip()
3569
+ if search:
3570
+ fields = fields.filter(label__icontains=search) | fields.filter(name__icontains=search)
3571
+ fields = fields.distinct()
3572
+
3573
+ paginator = Paginator(fields, 25)
3574
+ page_obj = paginator.get_page(request.GET.get("page", 1))
3575
+
34263576
return render(
34273577
request,
34283578
"fossil/ticket_fields_list.html",
34293579
{
34303580
"project": project,
34313581
"fossil_repo": fossil_repo,
3432
- "fields": fields,
3582
+ "fields": page_obj,
3583
+ "page_obj": page_obj,
3584
+ "search": search,
34333585
"active_tab": "settings",
34343586
},
34353587
)
34363588
34373589
@@ -3563,21 +3715,32 @@
35633715
project, fossil_repo = _get_project_and_repo(slug, request, "read")
35643716
35653717
from fossil.ticket_reports import TicketReport
35663718
35673719
reports = TicketReport.objects.filter(repository=fossil_repo)
3568
- if not can_admin_project(request.user, project):
3720
+ is_admin = can_admin_project(request.user, project)
3721
+ if not is_admin:
35693722
reports = reports.filter(is_public=True)
35703723
3724
+ search = request.GET.get("search", "").strip()
3725
+ if search:
3726
+ reports = reports.filter(title__icontains=search) | reports.filter(description__icontains=search)
3727
+ reports = reports.distinct()
3728
+
3729
+ paginator = Paginator(reports, 25)
3730
+ page_obj = paginator.get_page(request.GET.get("page", 1))
3731
+
35713732
return render(
35723733
request,
35733734
"fossil/ticket_reports_list.html",
35743735
{
35753736
"project": project,
35763737
"fossil_repo": fossil_repo,
3577
- "reports": reports,
3578
- "can_admin": can_admin_project(request.user, project),
3738
+ "reports": page_obj,
3739
+ "page_obj": page_obj,
3740
+ "can_admin": is_admin,
3741
+ "search": search,
35793742
"active_tab": "tickets",
35803743
},
35813744
)
35823745
35833746
35843747
--- fossil/views.py
+++ fossil/views.py
@@ -1,11 +1,13 @@
1 import contextlib
 
2 import re
3 from datetime import datetime
4
5 import markdown as md
6 from django.contrib.auth.decorators import login_required
 
7 from django.http import Http404, HttpResponse, JsonResponse
8 from django.shortcuts import get_object_or_404, redirect, render
9 from django.utils.safestring import mark_safe
10 from django.views.decorators.csrf import csrf_exempt
11
@@ -13,10 +15,37 @@
13 from projects.models import Project
14
15 from .models import FossilRepository
16 from .reader import FossilReader
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
19 def _render_fossil_content(content: str, project_slug: str = "", base_path: str = "") -> str:
20 """Render content that may be Fossil wiki markup, HTML, or Markdown.
21
22 Fossil wiki pages can contain:
@@ -682,12 +711,10 @@
682
683 if search:
684 tickets = [t for t in tickets if search.lower() in t.title.lower()]
685
686 total = len(tickets)
687 import math
688
689 total_pages = max(1, math.ceil(total / per_page))
690 page = min(page, total_pages)
691 tickets = tickets[(page - 1) * per_page : page * per_page]
692 has_next = page < total_pages
693 has_prev = page > 1
@@ -759,13 +786,35 @@
759
760 with reader:
761 pages = reader.get_wiki_pages()
762 home_page = reader.get_wiki_page("Home")
763
 
 
 
 
 
 
764 home_content_html = ""
765 if home_page:
766 home_content_html = mark_safe(sanitize_html(_render_fossil_content(home_page.content, project_slug=slug)))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
767
768 return render(
769 request,
770 "fossil/wiki_list.html",
771 {
@@ -772,10 +821,12 @@
772 "project": project,
773 "fossil_repo": fossil_repo,
774 "pages": pages,
775 "home_page": home_page,
776 "home_content_html": home_content_html,
 
 
777 "active_tab": "wiki",
778 },
779 )
780
781
@@ -844,10 +895,17 @@
844 )
845
846 # Sort merged list by timestamp descending
847 merged.sort(key=lambda x: x["timestamp"], reverse=True)
848
 
 
 
 
 
 
 
849 has_write = can_write_project(request.user, project)
850
851 return render(
852 request,
853 "fossil/forum_list.html",
@@ -854,10 +912,12 @@
854 {
855 "project": project,
856 "fossil_repo": fossil_repo,
857 "posts": merged,
858 "has_write": has_write,
 
 
859 "active_tab": "forum",
860 },
861 )
862
863
@@ -1026,17 +1086,26 @@
1026
1027 from fossil.webhooks import Webhook
1028
1029 webhooks = Webhook.objects.filter(repository=fossil_repo)
1030
 
 
 
 
 
 
 
1031 return render(
1032 request,
1033 "fossil/webhook_list.html",
1034 {
1035 "project": project,
1036 "fossil_repo": fossil_repo,
1037 "webhooks": webhooks,
 
 
1038 "active_tab": "settings",
1039 },
1040 )
1041
1042
@@ -1888,16 +1957,30 @@
1888 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
1889
1890 with reader:
1891 notes = reader.get_technotes()
1892
 
 
 
 
 
 
 
1893 has_write = can_write_project(request.user, project)
1894
1895 return render(
1896 request,
1897 "fossil/technote_list.html",
1898 {"project": project, "notes": notes, "has_write": has_write, "active_tab": "wiki"},
 
 
 
 
 
 
 
1899 )
1900
1901
1902 @login_required
1903 def technote_create(request, slug):
@@ -2010,19 +2093,28 @@
2010 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
2011
2012 with reader:
2013 files = reader.get_unversioned_files()
2014
 
 
 
 
 
 
 
2015 has_admin = can_admin_project(request.user, project)
2016
2017 return render(
2018 request,
2019 "fossil/unversioned_list.html",
2020 {
2021 "project": project,
2022 "files": files,
2023 "has_admin": has_admin,
 
 
2024 "active_tab": "files",
2025 },
2026 )
2027
2028
@@ -2331,17 +2423,26 @@
2331 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
2332
2333 with reader:
2334 branches = reader.get_branches()
2335
 
 
 
 
 
 
 
2336 return render(
2337 request,
2338 "fossil/branch_list.html",
2339 {
2340 "project": project,
2341 "fossil_repo": fossil_repo,
2342 "branches": branches,
 
 
2343 "active_tab": "code",
2344 },
2345 )
2346
2347
@@ -2352,14 +2453,27 @@
2352 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
2353
2354 with reader:
2355 tags = reader.get_tags()
2356
 
 
 
 
 
 
 
2357 return render(
2358 request,
2359 "fossil/tag_list.html",
2360 {"project": project, "tags": tags, "active_tab": "code"},
 
 
 
 
 
 
2361 )
2362
2363
2364 # --- Raw File Download ---
2365
@@ -2793,18 +2907,28 @@
2793
2794 has_write = can_write_project(request.user, project)
2795 if not has_write:
2796 releases = releases.filter(is_draft=False)
2797
 
 
 
 
 
 
 
 
2798 return render(
2799 request,
2800 "fossil/release_list.html",
2801 {
2802 "project": project,
2803 "fossil_repo": fossil_repo,
2804 "releases": releases,
 
2805 "has_write": has_write,
 
2806 "active_tab": "releases",
2807 },
2808 )
2809
2810
@@ -3197,17 +3321,26 @@
3197
3198 from fossil.api_tokens import APIToken
3199
3200 tokens = APIToken.objects.filter(repository=fossil_repo)
3201
 
 
 
 
 
 
 
3202 return render(
3203 request,
3204 "fossil/api_token_list.html",
3205 {
3206 "project": project,
3207 "fossil_repo": fossil_repo,
3208 "tokens": tokens,
 
 
3209 "active_tab": "settings",
3210 },
3211 )
3212
3213
@@ -3284,17 +3417,26 @@
3284
3285 from fossil.branch_protection import BranchProtection
3286
3287 rules = BranchProtection.objects.filter(repository=fossil_repo)
3288
 
 
 
 
 
 
 
3289 return render(
3290 request,
3291 "fossil/branch_protection_list.html",
3292 {
3293 "project": project,
3294 "fossil_repo": fossil_repo,
3295 "rules": rules,
 
 
3296 "active_tab": "settings",
3297 },
3298 )
3299
3300
@@ -3421,17 +3563,27 @@
3421
3422 from fossil.ticket_fields import TicketFieldDefinition
3423
3424 fields = TicketFieldDefinition.objects.filter(repository=fossil_repo)
3425
 
 
 
 
 
 
 
 
3426 return render(
3427 request,
3428 "fossil/ticket_fields_list.html",
3429 {
3430 "project": project,
3431 "fossil_repo": fossil_repo,
3432 "fields": fields,
 
 
3433 "active_tab": "settings",
3434 },
3435 )
3436
3437
@@ -3563,21 +3715,32 @@
3563 project, fossil_repo = _get_project_and_repo(slug, request, "read")
3564
3565 from fossil.ticket_reports import TicketReport
3566
3567 reports = TicketReport.objects.filter(repository=fossil_repo)
3568 if not can_admin_project(request.user, project):
 
3569 reports = reports.filter(is_public=True)
3570
 
 
 
 
 
 
 
 
3571 return render(
3572 request,
3573 "fossil/ticket_reports_list.html",
3574 {
3575 "project": project,
3576 "fossil_repo": fossil_repo,
3577 "reports": reports,
3578 "can_admin": can_admin_project(request.user, project),
 
 
3579 "active_tab": "tickets",
3580 },
3581 )
3582
3583
3584
--- fossil/views.py
+++ fossil/views.py
@@ -1,11 +1,13 @@
1 import contextlib
2 import math
3 import re
4 from datetime import datetime
5
6 import markdown as md
7 from django.contrib.auth.decorators import login_required
8 from django.core.paginator import Paginator
9 from django.http import Http404, HttpResponse, JsonResponse
10 from django.shortcuts import get_object_or_404, redirect, render
11 from django.utils.safestring import mark_safe
12 from django.views.decorators.csrf import csrf_exempt
13
@@ -13,10 +15,37 @@
15 from projects.models import Project
16
17 from .models import FossilRepository
18 from .reader import FossilReader
19
20
21 def _manual_paginate(items, request, per_page=25):
22 """Paginate a plain list (FossilReader results) and return (sliced_items, pagination_dict).
23
24 The pagination dict has keys compatible with the _pagination_manual.html partial:
25 has_previous, has_next, previous_page_number, next_page_number, number, num_pages, count.
26 """
27 total = len(items)
28 num_pages = max(1, math.ceil(total / per_page))
29 try:
30 page = int(request.GET.get("page", 1))
31 except (ValueError, TypeError):
32 page = 1
33 page = max(1, min(page, num_pages))
34 offset = (page - 1) * per_page
35 sliced = items[offset : offset + per_page]
36 pagination = {
37 "has_previous": page > 1,
38 "has_next": offset + per_page < total,
39 "previous_page_number": page - 1,
40 "next_page_number": page + 1,
41 "number": page,
42 "num_pages": num_pages,
43 "count": total,
44 }
45 return sliced, pagination
46
47
48 def _render_fossil_content(content: str, project_slug: str = "", base_path: str = "") -> str:
49 """Render content that may be Fossil wiki markup, HTML, or Markdown.
50
51 Fossil wiki pages can contain:
@@ -682,12 +711,10 @@
711
712 if search:
713 tickets = [t for t in tickets if search.lower() in t.title.lower()]
714
715 total = len(tickets)
 
 
716 total_pages = max(1, math.ceil(total / per_page))
717 page = min(page, total_pages)
718 tickets = tickets[(page - 1) * per_page : page * per_page]
719 has_next = page < total_pages
720 has_prev = page > 1
@@ -759,13 +786,35 @@
786
787 with reader:
788 pages = reader.get_wiki_pages()
789 home_page = reader.get_wiki_page("Home")
790
791 search = request.GET.get("search", "").strip()
792 if search:
793 pages = [p for p in pages if search.lower() in p.name.lower()]
794
795 pages, pagination = _manual_paginate(pages, request)
796
797 home_content_html = ""
798 if home_page:
799 home_content_html = mark_safe(sanitize_html(_render_fossil_content(home_page.content, project_slug=slug)))
800
801 if request.headers.get("HX-Request"):
802 return render(
803 request,
804 "fossil/wiki_list.html",
805 {
806 "project": project,
807 "fossil_repo": fossil_repo,
808 "pages": pages,
809 "home_page": home_page,
810 "home_content_html": home_content_html,
811 "search": search,
812 "pagination": pagination,
813 "active_tab": "wiki",
814 },
815 )
816
817 return render(
818 request,
819 "fossil/wiki_list.html",
820 {
@@ -772,10 +821,12 @@
821 "project": project,
822 "fossil_repo": fossil_repo,
823 "pages": pages,
824 "home_page": home_page,
825 "home_content_html": home_content_html,
826 "search": search,
827 "pagination": pagination,
828 "active_tab": "wiki",
829 },
830 )
831
832
@@ -844,10 +895,17 @@
895 )
896
897 # Sort merged list by timestamp descending
898 merged.sort(key=lambda x: x["timestamp"], reverse=True)
899
900 search = request.GET.get("search", "").strip()
901 if search:
902 search_lower = search.lower()
903 merged = [p for p in merged if search_lower in (p.get("title") or "").lower() or search_lower in (p.get("body") or "").lower()]
904
905 merged, pagination = _manual_paginate(merged, request)
906
907 has_write = can_write_project(request.user, project)
908
909 return render(
910 request,
911 "fossil/forum_list.html",
@@ -854,10 +912,12 @@
912 {
913 "project": project,
914 "fossil_repo": fossil_repo,
915 "posts": merged,
916 "has_write": has_write,
917 "search": search,
918 "pagination": pagination,
919 "active_tab": "forum",
920 },
921 )
922
923
@@ -1026,17 +1086,26 @@
1086
1087 from fossil.webhooks import Webhook
1088
1089 webhooks = Webhook.objects.filter(repository=fossil_repo)
1090
1091 search = request.GET.get("search", "").strip()
1092 if search:
1093 webhooks = webhooks.filter(url__icontains=search)
1094
1095 paginator = Paginator(webhooks, 25)
1096 page_obj = paginator.get_page(request.GET.get("page", 1))
1097
1098 return render(
1099 request,
1100 "fossil/webhook_list.html",
1101 {
1102 "project": project,
1103 "fossil_repo": fossil_repo,
1104 "webhooks": page_obj,
1105 "page_obj": page_obj,
1106 "search": search,
1107 "active_tab": "settings",
1108 },
1109 )
1110
1111
@@ -1888,16 +1957,30 @@
1957 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
1958
1959 with reader:
1960 notes = reader.get_technotes()
1961
1962 search = request.GET.get("search", "").strip()
1963 if search:
1964 search_lower = search.lower()
1965 notes = [n for n in notes if search_lower in (n.comment or "").lower()]
1966
1967 notes, pagination = _manual_paginate(notes, request)
1968
1969 has_write = can_write_project(request.user, project)
1970
1971 return render(
1972 request,
1973 "fossil/technote_list.html",
1974 {
1975 "project": project,
1976 "notes": notes,
1977 "has_write": has_write,
1978 "search": search,
1979 "pagination": pagination,
1980 "active_tab": "wiki",
1981 },
1982 )
1983
1984
1985 @login_required
1986 def technote_create(request, slug):
@@ -2010,19 +2093,28 @@
2093 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
2094
2095 with reader:
2096 files = reader.get_unversioned_files()
2097
2098 search = request.GET.get("search", "").strip()
2099 if search:
2100 search_lower = search.lower()
2101 files = [f for f in files if search_lower in f.name.lower()]
2102
2103 files, pagination = _manual_paginate(files, request)
2104
2105 has_admin = can_admin_project(request.user, project)
2106
2107 return render(
2108 request,
2109 "fossil/unversioned_list.html",
2110 {
2111 "project": project,
2112 "files": files,
2113 "has_admin": has_admin,
2114 "search": search,
2115 "pagination": pagination,
2116 "active_tab": "files",
2117 },
2118 )
2119
2120
@@ -2331,17 +2423,26 @@
2423 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
2424
2425 with reader:
2426 branches = reader.get_branches()
2427
2428 search = request.GET.get("search", "").strip()
2429 if search:
2430 search_lower = search.lower()
2431 branches = [b for b in branches if search_lower in b.name.lower()]
2432
2433 branches, pagination = _manual_paginate(branches, request)
2434
2435 return render(
2436 request,
2437 "fossil/branch_list.html",
2438 {
2439 "project": project,
2440 "fossil_repo": fossil_repo,
2441 "branches": branches,
2442 "search": search,
2443 "pagination": pagination,
2444 "active_tab": "code",
2445 },
2446 )
2447
2448
@@ -2352,14 +2453,27 @@
2453 project, fossil_repo, reader = _get_repo_and_reader(slug, request)
2454
2455 with reader:
2456 tags = reader.get_tags()
2457
2458 search = request.GET.get("search", "").strip()
2459 if search:
2460 search_lower = search.lower()
2461 tags = [t for t in tags if search_lower in t.name.lower()]
2462
2463 tags, pagination = _manual_paginate(tags, request)
2464
2465 return render(
2466 request,
2467 "fossil/tag_list.html",
2468 {
2469 "project": project,
2470 "tags": tags,
2471 "search": search,
2472 "pagination": pagination,
2473 "active_tab": "code",
2474 },
2475 )
2476
2477
2478 # --- Raw File Download ---
2479
@@ -2793,18 +2907,28 @@
2907
2908 has_write = can_write_project(request.user, project)
2909 if not has_write:
2910 releases = releases.filter(is_draft=False)
2911
2912 search = request.GET.get("search", "").strip()
2913 if search:
2914 releases = releases.filter(tag_name__icontains=search) | releases.filter(name__icontains=search)
2915 releases = releases.distinct()
2916
2917 paginator = Paginator(releases, 25)
2918 page_obj = paginator.get_page(request.GET.get("page", 1))
2919
2920 return render(
2921 request,
2922 "fossil/release_list.html",
2923 {
2924 "project": project,
2925 "fossil_repo": fossil_repo,
2926 "releases": page_obj,
2927 "page_obj": page_obj,
2928 "has_write": has_write,
2929 "search": search,
2930 "active_tab": "releases",
2931 },
2932 )
2933
2934
@@ -3197,17 +3321,26 @@
3321
3322 from fossil.api_tokens import APIToken
3323
3324 tokens = APIToken.objects.filter(repository=fossil_repo)
3325
3326 search = request.GET.get("search", "").strip()
3327 if search:
3328 tokens = tokens.filter(name__icontains=search)
3329
3330 paginator = Paginator(tokens, 25)
3331 page_obj = paginator.get_page(request.GET.get("page", 1))
3332
3333 return render(
3334 request,
3335 "fossil/api_token_list.html",
3336 {
3337 "project": project,
3338 "fossil_repo": fossil_repo,
3339 "tokens": page_obj,
3340 "page_obj": page_obj,
3341 "search": search,
3342 "active_tab": "settings",
3343 },
3344 )
3345
3346
@@ -3284,17 +3417,26 @@
3417
3418 from fossil.branch_protection import BranchProtection
3419
3420 rules = BranchProtection.objects.filter(repository=fossil_repo)
3421
3422 search = request.GET.get("search", "").strip()
3423 if search:
3424 rules = rules.filter(branch_pattern__icontains=search)
3425
3426 paginator = Paginator(rules, 25)
3427 page_obj = paginator.get_page(request.GET.get("page", 1))
3428
3429 return render(
3430 request,
3431 "fossil/branch_protection_list.html",
3432 {
3433 "project": project,
3434 "fossil_repo": fossil_repo,
3435 "rules": page_obj,
3436 "page_obj": page_obj,
3437 "search": search,
3438 "active_tab": "settings",
3439 },
3440 )
3441
3442
@@ -3421,17 +3563,27 @@
3563
3564 from fossil.ticket_fields import TicketFieldDefinition
3565
3566 fields = TicketFieldDefinition.objects.filter(repository=fossil_repo)
3567
3568 search = request.GET.get("search", "").strip()
3569 if search:
3570 fields = fields.filter(label__icontains=search) | fields.filter(name__icontains=search)
3571 fields = fields.distinct()
3572
3573 paginator = Paginator(fields, 25)
3574 page_obj = paginator.get_page(request.GET.get("page", 1))
3575
3576 return render(
3577 request,
3578 "fossil/ticket_fields_list.html",
3579 {
3580 "project": project,
3581 "fossil_repo": fossil_repo,
3582 "fields": page_obj,
3583 "page_obj": page_obj,
3584 "search": search,
3585 "active_tab": "settings",
3586 },
3587 )
3588
3589
@@ -3563,21 +3715,32 @@
3715 project, fossil_repo = _get_project_and_repo(slug, request, "read")
3716
3717 from fossil.ticket_reports import TicketReport
3718
3719 reports = TicketReport.objects.filter(repository=fossil_repo)
3720 is_admin = can_admin_project(request.user, project)
3721 if not is_admin:
3722 reports = reports.filter(is_public=True)
3723
3724 search = request.GET.get("search", "").strip()
3725 if search:
3726 reports = reports.filter(title__icontains=search) | reports.filter(description__icontains=search)
3727 reports = reports.distinct()
3728
3729 paginator = Paginator(reports, 25)
3730 page_obj = paginator.get_page(request.GET.get("page", 1))
3731
3732 return render(
3733 request,
3734 "fossil/ticket_reports_list.html",
3735 {
3736 "project": project,
3737 "fossil_repo": fossil_repo,
3738 "reports": page_obj,
3739 "page_obj": page_obj,
3740 "can_admin": is_admin,
3741 "search": search,
3742 "active_tab": "tickets",
3743 },
3744 )
3745
3746
3747
--- organization/views.py
+++ organization/views.py
@@ -1,8 +1,9 @@
11
from django.contrib import messages
22
from django.contrib.auth.decorators import login_required
33
from django.contrib.auth.models import User
4
+from django.core.paginator import Paginator
45
from django.db import models
56
from django.http import HttpResponse
67
from django.shortcuts import get_object_or_404, redirect, render
78
89
from core.permissions import P
@@ -63,14 +64,19 @@
6364
6465
search = request.GET.get("search", "").strip()
6566
if search:
6667
members = members.filter(member__username__icontains=search)
6768
69
+ paginator = Paginator(members, 25)
70
+ page_obj = paginator.get_page(request.GET.get("page", 1))
71
+
6872
if request.headers.get("HX-Request"):
69
- return render(request, "organization/partials/member_table.html", {"members": members, "org": org})
73
+ return render(
74
+ request, "organization/partials/member_table.html", {"members": page_obj, "page_obj": page_obj, "org": org, "search": search}
75
+ )
7076
71
- return render(request, "organization/member_list.html", {"members": members, "org": org, "search": search})
77
+ return render(request, "organization/member_list.html", {"members": page_obj, "page_obj": page_obj, "org": org, "search": search})
7278
7379
7480
@login_required
7581
def member_add(request):
7682
P.ORGANIZATION_MEMBER_ADD.check(request.user)
@@ -118,14 +124,17 @@
118124
119125
search = request.GET.get("search", "").strip()
120126
if search:
121127
teams = teams.filter(name__icontains=search)
122128
129
+ paginator = Paginator(teams, 25)
130
+ page_obj = paginator.get_page(request.GET.get("page", 1))
131
+
123132
if request.headers.get("HX-Request"):
124
- return render(request, "organization/partials/team_table.html", {"teams": teams})
133
+ return render(request, "organization/partials/team_table.html", {"teams": page_obj, "page_obj": page_obj, "search": search})
125134
126
- return render(request, "organization/team_list.html", {"teams": teams, "search": search})
135
+ return render(request, "organization/team_list.html", {"teams": page_obj, "page_obj": page_obj, "search": search})
127136
128137
129138
@login_required
130139
def team_create(request):
131140
P.TEAM_ADD.check(request.user)
@@ -389,10 +398,12 @@
389398
390399
391400
@login_required
392401
def audit_log(request):
393402
"""Unified audit log across all tracked models. Requires superuser or org admin."""
403
+ import math
404
+
394405
if not request.user.is_superuser:
395406
P.ORGANIZATION_CHANGE.check(request.user)
396407
397408
from fossil.models import FossilRepository
398409
from projects.models import Project
@@ -409,11 +420,11 @@
409420
410421
for label, model in trackable_models:
411422
if model_filter and label.lower() != model_filter.lower():
412423
continue
413424
history_model = model.history.model
414
- qs = history_model.objects.all().select_related("history_user").order_by("-history_date")[:100]
425
+ qs = history_model.objects.all().select_related("history_user").order_by("-history_date")[:500]
415426
for h in qs:
416427
entries.append(
417428
{
418429
"date": h.history_date,
419430
"user": h.history_user,
@@ -423,11 +434,31 @@
423434
"object_id": h.pk,
424435
}
425436
)
426437
427438
entries.sort(key=lambda x: x["date"], reverse=True)
428
- entries = entries[:200]
439
+
440
+ # Manual pagination over the merged, sorted list
441
+ per_page = 25
442
+ total = len(entries)
443
+ num_pages = max(1, math.ceil(total / per_page))
444
+ try:
445
+ page = int(request.GET.get("page", 1))
446
+ except (ValueError, TypeError):
447
+ page = 1
448
+ page = max(1, min(page, num_pages))
449
+ offset = (page - 1) * per_page
450
+ entries = entries[offset : offset + per_page]
451
+ pagination = {
452
+ "has_previous": page > 1,
453
+ "has_next": offset + per_page < total,
454
+ "previous_page_number": page - 1,
455
+ "next_page_number": page + 1,
456
+ "number": page,
457
+ "num_pages": num_pages,
458
+ "count": total,
459
+ }
429460
430461
available_models = [label for label, _ in trackable_models]
431462
432463
return render(
433464
request,
@@ -434,10 +465,11 @@
434465
"organization/audit_log.html",
435466
{
436467
"entries": entries,
437468
"model_filter": model_filter,
438469
"available_models": available_models,
470
+ "pagination": pagination,
439471
},
440472
)
441473
442474
443475
@login_required
444476
--- organization/views.py
+++ organization/views.py
@@ -1,8 +1,9 @@
1 from django.contrib import messages
2 from django.contrib.auth.decorators import login_required
3 from django.contrib.auth.models import User
 
4 from django.db import models
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, redirect, render
7
8 from core.permissions import P
@@ -63,14 +64,19 @@
63
64 search = request.GET.get("search", "").strip()
65 if search:
66 members = members.filter(member__username__icontains=search)
67
 
 
 
68 if request.headers.get("HX-Request"):
69 return render(request, "organization/partials/member_table.html", {"members": members, "org": org})
 
 
70
71 return render(request, "organization/member_list.html", {"members": members, "org": org, "search": search})
72
73
74 @login_required
75 def member_add(request):
76 P.ORGANIZATION_MEMBER_ADD.check(request.user)
@@ -118,14 +124,17 @@
118
119 search = request.GET.get("search", "").strip()
120 if search:
121 teams = teams.filter(name__icontains=search)
122
 
 
 
123 if request.headers.get("HX-Request"):
124 return render(request, "organization/partials/team_table.html", {"teams": teams})
125
126 return render(request, "organization/team_list.html", {"teams": teams, "search": search})
127
128
129 @login_required
130 def team_create(request):
131 P.TEAM_ADD.check(request.user)
@@ -389,10 +398,12 @@
389
390
391 @login_required
392 def audit_log(request):
393 """Unified audit log across all tracked models. Requires superuser or org admin."""
 
 
394 if not request.user.is_superuser:
395 P.ORGANIZATION_CHANGE.check(request.user)
396
397 from fossil.models import FossilRepository
398 from projects.models import Project
@@ -409,11 +420,11 @@
409
410 for label, model in trackable_models:
411 if model_filter and label.lower() != model_filter.lower():
412 continue
413 history_model = model.history.model
414 qs = history_model.objects.all().select_related("history_user").order_by("-history_date")[:100]
415 for h in qs:
416 entries.append(
417 {
418 "date": h.history_date,
419 "user": h.history_user,
@@ -423,11 +434,31 @@
423 "object_id": h.pk,
424 }
425 )
426
427 entries.sort(key=lambda x: x["date"], reverse=True)
428 entries = entries[:200]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
430 available_models = [label for label, _ in trackable_models]
431
432 return render(
433 request,
@@ -434,10 +465,11 @@
434 "organization/audit_log.html",
435 {
436 "entries": entries,
437 "model_filter": model_filter,
438 "available_models": available_models,
 
439 },
440 )
441
442
443 @login_required
444
--- organization/views.py
+++ organization/views.py
@@ -1,8 +1,9 @@
1 from django.contrib import messages
2 from django.contrib.auth.decorators import login_required
3 from django.contrib.auth.models import User
4 from django.core.paginator import Paginator
5 from django.db import models
6 from django.http import HttpResponse
7 from django.shortcuts import get_object_or_404, redirect, render
8
9 from core.permissions import P
@@ -63,14 +64,19 @@
64
65 search = request.GET.get("search", "").strip()
66 if search:
67 members = members.filter(member__username__icontains=search)
68
69 paginator = Paginator(members, 25)
70 page_obj = paginator.get_page(request.GET.get("page", 1))
71
72 if request.headers.get("HX-Request"):
73 return render(
74 request, "organization/partials/member_table.html", {"members": page_obj, "page_obj": page_obj, "org": org, "search": search}
75 )
76
77 return render(request, "organization/member_list.html", {"members": page_obj, "page_obj": page_obj, "org": org, "search": search})
78
79
80 @login_required
81 def member_add(request):
82 P.ORGANIZATION_MEMBER_ADD.check(request.user)
@@ -118,14 +124,17 @@
124
125 search = request.GET.get("search", "").strip()
126 if search:
127 teams = teams.filter(name__icontains=search)
128
129 paginator = Paginator(teams, 25)
130 page_obj = paginator.get_page(request.GET.get("page", 1))
131
132 if request.headers.get("HX-Request"):
133 return render(request, "organization/partials/team_table.html", {"teams": page_obj, "page_obj": page_obj, "search": search})
134
135 return render(request, "organization/team_list.html", {"teams": page_obj, "page_obj": page_obj, "search": search})
136
137
138 @login_required
139 def team_create(request):
140 P.TEAM_ADD.check(request.user)
@@ -389,10 +398,12 @@
398
399
400 @login_required
401 def audit_log(request):
402 """Unified audit log across all tracked models. Requires superuser or org admin."""
403 import math
404
405 if not request.user.is_superuser:
406 P.ORGANIZATION_CHANGE.check(request.user)
407
408 from fossil.models import FossilRepository
409 from projects.models import Project
@@ -409,11 +420,11 @@
420
421 for label, model in trackable_models:
422 if model_filter and label.lower() != model_filter.lower():
423 continue
424 history_model = model.history.model
425 qs = history_model.objects.all().select_related("history_user").order_by("-history_date")[:500]
426 for h in qs:
427 entries.append(
428 {
429 "date": h.history_date,
430 "user": h.history_user,
@@ -423,11 +434,31 @@
434 "object_id": h.pk,
435 }
436 )
437
438 entries.sort(key=lambda x: x["date"], reverse=True)
439
440 # Manual pagination over the merged, sorted list
441 per_page = 25
442 total = len(entries)
443 num_pages = max(1, math.ceil(total / per_page))
444 try:
445 page = int(request.GET.get("page", 1))
446 except (ValueError, TypeError):
447 page = 1
448 page = max(1, min(page, num_pages))
449 offset = (page - 1) * per_page
450 entries = entries[offset : offset + per_page]
451 pagination = {
452 "has_previous": page > 1,
453 "has_next": offset + per_page < total,
454 "previous_page_number": page - 1,
455 "next_page_number": page + 1,
456 "number": page,
457 "num_pages": num_pages,
458 "count": total,
459 }
460
461 available_models = [label for label, _ in trackable_models]
462
463 return render(
464 request,
@@ -434,10 +465,11 @@
465 "organization/audit_log.html",
466 {
467 "entries": entries,
468 "model_filter": model_filter,
469 "available_models": available_models,
470 "pagination": pagination,
471 },
472 )
473
474
475 @login_required
476
+6 -2
--- pages/views.py
+++ pages/views.py
@@ -1,8 +1,9 @@
11
import markdown
22
from django.contrib import messages
33
from django.contrib.auth.decorators import login_required
4
+from django.core.paginator import Paginator
45
from django.http import HttpResponse
56
from django.shortcuts import get_object_or_404, redirect, render
67
from django.utils.safestring import mark_safe
78
89
from core.permissions import P
@@ -23,14 +24,17 @@
2324
2425
search = request.GET.get("search", "").strip()
2526
if search:
2627
pages = pages.filter(name__icontains=search)
2728
29
+ paginator = Paginator(pages, 25)
30
+ page_obj = paginator.get_page(request.GET.get("page", 1))
31
+
2832
if request.headers.get("HX-Request"):
29
- return render(request, "pages/partials/page_table.html", {"pages": pages})
33
+ return render(request, "pages/partials/page_table.html", {"pages": page_obj, "page_obj": page_obj, "search": search})
3034
31
- return render(request, "pages/page_list.html", {"pages": pages, "search": search})
35
+ return render(request, "pages/page_list.html", {"pages": page_obj, "page_obj": page_obj, "search": search})
3236
3337
3438
@login_required
3539
def page_create(request):
3640
P.PAGE_ADD.check(request.user)
3741
--- pages/views.py
+++ pages/views.py
@@ -1,8 +1,9 @@
1 import markdown
2 from django.contrib import messages
3 from django.contrib.auth.decorators import login_required
 
4 from django.http import HttpResponse
5 from django.shortcuts import get_object_or_404, redirect, render
6 from django.utils.safestring import mark_safe
7
8 from core.permissions import P
@@ -23,14 +24,17 @@
23
24 search = request.GET.get("search", "").strip()
25 if search:
26 pages = pages.filter(name__icontains=search)
27
 
 
 
28 if request.headers.get("HX-Request"):
29 return render(request, "pages/partials/page_table.html", {"pages": pages})
30
31 return render(request, "pages/page_list.html", {"pages": pages, "search": search})
32
33
34 @login_required
35 def page_create(request):
36 P.PAGE_ADD.check(request.user)
37
--- pages/views.py
+++ pages/views.py
@@ -1,8 +1,9 @@
1 import markdown
2 from django.contrib import messages
3 from django.contrib.auth.decorators import login_required
4 from django.core.paginator import Paginator
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, redirect, render
7 from django.utils.safestring import mark_safe
8
9 from core.permissions import P
@@ -23,14 +24,17 @@
24
25 search = request.GET.get("search", "").strip()
26 if search:
27 pages = pages.filter(name__icontains=search)
28
29 paginator = Paginator(pages, 25)
30 page_obj = paginator.get_page(request.GET.get("page", 1))
31
32 if request.headers.get("HX-Request"):
33 return render(request, "pages/partials/page_table.html", {"pages": page_obj, "page_obj": page_obj, "search": search})
34
35 return render(request, "pages/page_list.html", {"pages": page_obj, "page_obj": page_obj, "search": search})
36
37
38 @login_required
39 def page_create(request):
40 P.PAGE_ADD.check(request.user)
41
--- projects/views.py
+++ projects/views.py
@@ -1,7 +1,8 @@
11
from django.contrib import messages
22
from django.contrib.auth.decorators import login_required
3
+from django.core.paginator import Paginator
34
from django.db.models import Count
45
from django.http import HttpResponse
56
from django.shortcuts import get_object_or_404, redirect, render
67
78
from core.permissions import P
@@ -19,14 +20,17 @@
1920
2021
search = request.GET.get("search", "").strip()
2122
if search:
2223
projects = projects.filter(name__icontains=search)
2324
25
+ paginator = Paginator(projects, 25)
26
+ page_obj = paginator.get_page(request.GET.get("page", 1))
27
+
2428
if request.headers.get("HX-Request"):
25
- return render(request, "projects/partials/project_table.html", {"projects": projects})
29
+ return render(request, "projects/partials/project_table.html", {"projects": page_obj, "page_obj": page_obj, "search": search})
2630
27
- return render(request, "projects/project_list.html", {"projects": projects, "search": search})
31
+ return render(request, "projects/project_list.html", {"projects": page_obj, "page_obj": page_obj, "search": search})
2832
2933
3034
@login_required
3135
def project_create(request):
3236
P.PROJECT_ADD.check(request.user)
@@ -250,14 +254,21 @@
250254
@login_required
251255
def group_list(request):
252256
P.PROJECT_GROUP_VIEW.check(request.user)
253257
groups = ProjectGroup.objects.all().prefetch_related("projects")
254258
259
+ search = request.GET.get("search", "").strip()
260
+ if search:
261
+ groups = groups.filter(name__icontains=search)
262
+
263
+ paginator = Paginator(groups, 25)
264
+ page_obj = paginator.get_page(request.GET.get("page", 1))
265
+
255266
if request.headers.get("HX-Request"):
256
- return render(request, "projects/partials/group_table.html", {"groups": groups})
267
+ return render(request, "projects/partials/group_table.html", {"groups": page_obj, "page_obj": page_obj, "search": search})
257268
258
- return render(request, "projects/group_list.html", {"groups": groups})
269
+ return render(request, "projects/group_list.html", {"groups": page_obj, "page_obj": page_obj, "search": search})
259270
260271
261272
@login_required
262273
def group_create(request):
263274
P.PROJECT_GROUP_ADD.check(request.user)
264275
--- projects/views.py
+++ projects/views.py
@@ -1,7 +1,8 @@
1 from django.contrib import messages
2 from django.contrib.auth.decorators import login_required
 
3 from django.db.models import Count
4 from django.http import HttpResponse
5 from django.shortcuts import get_object_or_404, redirect, render
6
7 from core.permissions import P
@@ -19,14 +20,17 @@
19
20 search = request.GET.get("search", "").strip()
21 if search:
22 projects = projects.filter(name__icontains=search)
23
 
 
 
24 if request.headers.get("HX-Request"):
25 return render(request, "projects/partials/project_table.html", {"projects": projects})
26
27 return render(request, "projects/project_list.html", {"projects": projects, "search": search})
28
29
30 @login_required
31 def project_create(request):
32 P.PROJECT_ADD.check(request.user)
@@ -250,14 +254,21 @@
250 @login_required
251 def group_list(request):
252 P.PROJECT_GROUP_VIEW.check(request.user)
253 groups = ProjectGroup.objects.all().prefetch_related("projects")
254
 
 
 
 
 
 
 
255 if request.headers.get("HX-Request"):
256 return render(request, "projects/partials/group_table.html", {"groups": groups})
257
258 return render(request, "projects/group_list.html", {"groups": groups})
259
260
261 @login_required
262 def group_create(request):
263 P.PROJECT_GROUP_ADD.check(request.user)
264
--- projects/views.py
+++ projects/views.py
@@ -1,7 +1,8 @@
1 from django.contrib import messages
2 from django.contrib.auth.decorators import login_required
3 from django.core.paginator import Paginator
4 from django.db.models import Count
5 from django.http import HttpResponse
6 from django.shortcuts import get_object_or_404, redirect, render
7
8 from core.permissions import P
@@ -19,14 +20,17 @@
20
21 search = request.GET.get("search", "").strip()
22 if search:
23 projects = projects.filter(name__icontains=search)
24
25 paginator = Paginator(projects, 25)
26 page_obj = paginator.get_page(request.GET.get("page", 1))
27
28 if request.headers.get("HX-Request"):
29 return render(request, "projects/partials/project_table.html", {"projects": page_obj, "page_obj": page_obj, "search": search})
30
31 return render(request, "projects/project_list.html", {"projects": page_obj, "page_obj": page_obj, "search": search})
32
33
34 @login_required
35 def project_create(request):
36 P.PROJECT_ADD.check(request.user)
@@ -250,14 +254,21 @@
254 @login_required
255 def group_list(request):
256 P.PROJECT_GROUP_VIEW.check(request.user)
257 groups = ProjectGroup.objects.all().prefetch_related("projects")
258
259 search = request.GET.get("search", "").strip()
260 if search:
261 groups = groups.filter(name__icontains=search)
262
263 paginator = Paginator(groups, 25)
264 page_obj = paginator.get_page(request.GET.get("page", 1))
265
266 if request.headers.get("HX-Request"):
267 return render(request, "projects/partials/group_table.html", {"groups": page_obj, "page_obj": page_obj, "search": search})
268
269 return render(request, "projects/group_list.html", {"groups": page_obj, "page_obj": page_obj, "search": search})
270
271
272 @login_required
273 def group_create(request):
274 P.PROJECT_GROUP_ADD.check(request.user)
275
--- templates/fossil/api_token_list.html
+++ templates/fossil/api_token_list.html
@@ -8,16 +8,29 @@
88
<div class="flex items-center justify-between mb-6">
99
<div>
1010
<h2 class="text-lg font-semibold text-gray-200">API Tokens</h2>
1111
<p class="text-sm text-gray-500 mt-1">Tokens for CI/CD systems and automation. Used with Bearer authentication.</p>
1212
</div>
13
- <a href="{% url 'fossil:api_token_create' slug=project.slug %}"
14
- class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
15
- Generate Token
16
- </a>
13
+ <div class="flex items-center gap-3">
14
+ <input type="search"
15
+ name="search"
16
+ value="{{ search }}"
17
+ placeholder="Search tokens..."
18
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
19
+ hx-get="{% url 'fossil:api_tokens' slug=project.slug %}"
20
+ hx-trigger="input changed delay:300ms, search"
21
+ hx-target="#token-content"
22
+ hx-swap="innerHTML"
23
+ hx-push-url="true" />
24
+ <a href="{% url 'fossil:api_token_create' slug=project.slug %}"
25
+ class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
26
+ Generate Token
27
+ </a>
28
+ </div>
1729
</div>
1830
31
+<div id="token-content">
1932
{% if tokens %}
2033
<div class="space-y-3">
2134
{% for token in tokens %}
2235
<div class="rounded-lg bg-gray-800 border border-gray-700">
2336
<div class="px-5 py-4">
@@ -54,17 +67,21 @@
5467
</div>
5568
{% endfor %}
5669
</div>
5770
{% else %}
5871
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
59
- <p class="text-sm text-gray-500">No API tokens generated yet.</p>
72
+ <p class="text-sm text-gray-500">{% if search %}No tokens matching "{{ search }}".{% else %}No API tokens generated yet.{% endif %}</p>
73
+ {% if not search %}
6074
<a href="{% url 'fossil:api_token_create' slug=project.slug %}"
6175
class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
6276
Generate the first token
6377
</a>
78
+ {% endif %}
6479
</div>
6580
{% endif %}
81
+{% include "includes/_pagination.html" %}
82
+</div>
6683
6784
<div class="mt-6 rounded-lg bg-gray-800/50 border border-gray-700 p-4">
6885
<h3 class="text-sm font-semibold text-gray-300 mb-2">Usage</h3>
6986
<p class="text-xs text-gray-500 mb-2">Use the token with the CI Status API:</p>
7087
<pre class="text-xs font-mono text-gray-400 bg-gray-900 rounded p-3 overflow-x-auto">curl -X POST {{ request.scheme }}://{{ request.get_host }}/projects/{{ project.slug }}/fossil/api/status \
7188
--- templates/fossil/api_token_list.html
+++ templates/fossil/api_token_list.html
@@ -8,16 +8,29 @@
8 <div class="flex items-center justify-between mb-6">
9 <div>
10 <h2 class="text-lg font-semibold text-gray-200">API Tokens</h2>
11 <p class="text-sm text-gray-500 mt-1">Tokens for CI/CD systems and automation. Used with Bearer authentication.</p>
12 </div>
13 <a href="{% url 'fossil:api_token_create' slug=project.slug %}"
14 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
15 Generate Token
16 </a>
 
 
 
 
 
 
 
 
 
 
 
 
17 </div>
18
 
19 {% if tokens %}
20 <div class="space-y-3">
21 {% for token in tokens %}
22 <div class="rounded-lg bg-gray-800 border border-gray-700">
23 <div class="px-5 py-4">
@@ -54,17 +67,21 @@
54 </div>
55 {% endfor %}
56 </div>
57 {% else %}
58 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
59 <p class="text-sm text-gray-500">No API tokens generated yet.</p>
 
60 <a href="{% url 'fossil:api_token_create' slug=project.slug %}"
61 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
62 Generate the first token
63 </a>
 
64 </div>
65 {% endif %}
 
 
66
67 <div class="mt-6 rounded-lg bg-gray-800/50 border border-gray-700 p-4">
68 <h3 class="text-sm font-semibold text-gray-300 mb-2">Usage</h3>
69 <p class="text-xs text-gray-500 mb-2">Use the token with the CI Status API:</p>
70 <pre class="text-xs font-mono text-gray-400 bg-gray-900 rounded p-3 overflow-x-auto">curl -X POST {{ request.scheme }}://{{ request.get_host }}/projects/{{ project.slug }}/fossil/api/status \
71
--- templates/fossil/api_token_list.html
+++ templates/fossil/api_token_list.html
@@ -8,16 +8,29 @@
8 <div class="flex items-center justify-between mb-6">
9 <div>
10 <h2 class="text-lg font-semibold text-gray-200">API Tokens</h2>
11 <p class="text-sm text-gray-500 mt-1">Tokens for CI/CD systems and automation. Used with Bearer authentication.</p>
12 </div>
13 <div class="flex items-center gap-3">
14 <input type="search"
15 name="search"
16 value="{{ search }}"
17 placeholder="Search tokens..."
18 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
19 hx-get="{% url 'fossil:api_tokens' slug=project.slug %}"
20 hx-trigger="input changed delay:300ms, search"
21 hx-target="#token-content"
22 hx-swap="innerHTML"
23 hx-push-url="true" />
24 <a href="{% url 'fossil:api_token_create' slug=project.slug %}"
25 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
26 Generate Token
27 </a>
28 </div>
29 </div>
30
31 <div id="token-content">
32 {% if tokens %}
33 <div class="space-y-3">
34 {% for token in tokens %}
35 <div class="rounded-lg bg-gray-800 border border-gray-700">
36 <div class="px-5 py-4">
@@ -54,17 +67,21 @@
67 </div>
68 {% endfor %}
69 </div>
70 {% else %}
71 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
72 <p class="text-sm text-gray-500">{% if search %}No tokens matching "{{ search }}".{% else %}No API tokens generated yet.{% endif %}</p>
73 {% if not search %}
74 <a href="{% url 'fossil:api_token_create' slug=project.slug %}"
75 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
76 Generate the first token
77 </a>
78 {% endif %}
79 </div>
80 {% endif %}
81 {% include "includes/_pagination.html" %}
82 </div>
83
84 <div class="mt-6 rounded-lg bg-gray-800/50 border border-gray-700 p-4">
85 <h3 class="text-sm font-semibold text-gray-300 mb-2">Usage</h3>
86 <p class="text-xs text-gray-500 mb-2">Use the token with the CI Status API:</p>
87 <pre class="text-xs font-mono text-gray-400 bg-gray-900 rounded p-3 overflow-x-auto">curl -X POST {{ request.scheme }}://{{ request.get_host }}/projects/{{ project.slug }}/fossil/api/status \
88
--- templates/fossil/branch_list.html
+++ templates/fossil/branch_list.html
@@ -3,10 +3,26 @@
33
44
{% block content %}
55
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
66
{% include "fossil/_project_nav.html" %}
77
8
+<div class="flex items-center justify-between mb-4">
9
+ <div>
10
+ <input type="search"
11
+ name="search"
12
+ value="{{ search }}"
13
+ placeholder="Search branches..."
14
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
15
+ hx-get="{% url 'fossil:branches' slug=project.slug %}"
16
+ hx-trigger="input changed delay:300ms, search"
17
+ hx-target="#branch-content"
18
+ hx-swap="innerHTML"
19
+ hx-push-url="true" />
20
+ </div>
21
+</div>
22
+
23
+<div id="branch-content">
824
<div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
925
<table class="min-w-full divide-y divide-gray-700">
1026
<thead class="bg-gray-900">
1127
<tr>
1228
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Branch</th>
@@ -35,12 +51,14 @@
3551
<td class="px-4 py-3 text-sm text-gray-400 text-right">{{ branch.checkin_count }}</td>
3652
<td class="px-4 py-3 text-sm text-gray-500 text-right">{{ branch.last_checkin|timesince }} ago</td>
3753
</tr>
3854
{% empty %}
3955
<tr>
40
- <td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">No branches.</td>
56
+ <td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">{% if search %}No branches matching "{{ search }}".{% else %}No branches.{% endif %}</td>
4157
</tr>
4258
{% endfor %}
4359
</tbody>
4460
</table>
61
+</div>
62
+{% include "includes/_pagination_manual.html" %}
4563
</div>
4664
{% endblock %}
4765
--- templates/fossil/branch_list.html
+++ templates/fossil/branch_list.html
@@ -3,10 +3,26 @@
3
4 {% block content %}
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8 <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
9 <table class="min-w-full divide-y divide-gray-700">
10 <thead class="bg-gray-900">
11 <tr>
12 <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Branch</th>
@@ -35,12 +51,14 @@
35 <td class="px-4 py-3 text-sm text-gray-400 text-right">{{ branch.checkin_count }}</td>
36 <td class="px-4 py-3 text-sm text-gray-500 text-right">{{ branch.last_checkin|timesince }} ago</td>
37 </tr>
38 {% empty %}
39 <tr>
40 <td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">No branches.</td>
41 </tr>
42 {% endfor %}
43 </tbody>
44 </table>
 
 
45 </div>
46 {% endblock %}
47
--- templates/fossil/branch_list.html
+++ templates/fossil/branch_list.html
@@ -3,10 +3,26 @@
3
4 {% block content %}
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-4">
9 <div>
10 <input type="search"
11 name="search"
12 value="{{ search }}"
13 placeholder="Search branches..."
14 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
15 hx-get="{% url 'fossil:branches' slug=project.slug %}"
16 hx-trigger="input changed delay:300ms, search"
17 hx-target="#branch-content"
18 hx-swap="innerHTML"
19 hx-push-url="true" />
20 </div>
21 </div>
22
23 <div id="branch-content">
24 <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
25 <table class="min-w-full divide-y divide-gray-700">
26 <thead class="bg-gray-900">
27 <tr>
28 <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Branch</th>
@@ -35,12 +51,14 @@
51 <td class="px-4 py-3 text-sm text-gray-400 text-right">{{ branch.checkin_count }}</td>
52 <td class="px-4 py-3 text-sm text-gray-500 text-right">{{ branch.last_checkin|timesince }} ago</td>
53 </tr>
54 {% empty %}
55 <tr>
56 <td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">{% if search %}No branches matching "{{ search }}".{% else %}No branches.{% endif %}</td>
57 </tr>
58 {% endfor %}
59 </tbody>
60 </table>
61 </div>
62 {% include "includes/_pagination_manual.html" %}
63 </div>
64 {% endblock %}
65
--- templates/fossil/branch_protection_list.html
+++ templates/fossil/branch_protection_list.html
@@ -8,16 +8,29 @@
88
<div class="flex items-center justify-between mb-6">
99
<div>
1010
<h2 class="text-lg font-semibold text-gray-200">Branch Protection Rules</h2>
1111
<p class="text-sm text-gray-500 mt-1">Protect branches from unreviewed changes and require CI checks to pass.</p>
1212
</div>
13
- <a href="{% url 'fossil:branch_protection_create' slug=project.slug %}"
14
- class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
15
- Add Rule
16
- </a>
13
+ <div class="flex items-center gap-3">
14
+ <input type="search"
15
+ name="search"
16
+ value="{{ search }}"
17
+ placeholder="Search rules..."
18
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
19
+ hx-get="{% url 'fossil:branch_protections' slug=project.slug %}"
20
+ hx-trigger="input changed delay:300ms, search"
21
+ hx-target="#protection-content"
22
+ hx-swap="innerHTML"
23
+ hx-push-url="true" />
24
+ <a href="{% url 'fossil:branch_protection_create' slug=project.slug %}"
25
+ class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
26
+ Add Rule
27
+ </a>
28
+ </div>
1729
</div>
1830
31
+<div id="protection-content">
1932
{% if rules %}
2033
<div class="space-y-3">
2134
{% for rule in rules %}
2235
<div class="rounded-lg bg-gray-800 border border-gray-700">
2336
<div class="px-5 py-4">
@@ -53,17 +66,21 @@
5366
</div>
5467
{% endfor %}
5568
</div>
5669
{% else %}
5770
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
58
- <p class="text-sm text-gray-500">No branch protection rules configured.</p>
71
+ <p class="text-sm text-gray-500">{% if search %}No rules matching "{{ search }}".{% else %}No branch protection rules configured.{% endif %}</p>
72
+ {% if not search %}
5973
<a href="{% url 'fossil:branch_protection_create' slug=project.slug %}"
6074
class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
6175
Add the first rule
6276
</a>
77
+ {% endif %}
6378
</div>
6479
{% endif %}
80
+{% include "includes/_pagination.html" %}
81
+</div>
6582
6683
<div class="mt-6 rounded-md bg-yellow-900/20 border border-yellow-800/50 px-4 py-3">
6784
<p class="text-xs text-yellow-300">Branch protection rules are currently advisory. Push enforcement via Fossil hooks is not yet implemented.</p>
6885
</div>
6986
{% endblock %}
7087
--- templates/fossil/branch_protection_list.html
+++ templates/fossil/branch_protection_list.html
@@ -8,16 +8,29 @@
8 <div class="flex items-center justify-between mb-6">
9 <div>
10 <h2 class="text-lg font-semibold text-gray-200">Branch Protection Rules</h2>
11 <p class="text-sm text-gray-500 mt-1">Protect branches from unreviewed changes and require CI checks to pass.</p>
12 </div>
13 <a href="{% url 'fossil:branch_protection_create' slug=project.slug %}"
14 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
15 Add Rule
16 </a>
 
 
 
 
 
 
 
 
 
 
 
 
17 </div>
18
 
19 {% if rules %}
20 <div class="space-y-3">
21 {% for rule in rules %}
22 <div class="rounded-lg bg-gray-800 border border-gray-700">
23 <div class="px-5 py-4">
@@ -53,17 +66,21 @@
53 </div>
54 {% endfor %}
55 </div>
56 {% else %}
57 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
58 <p class="text-sm text-gray-500">No branch protection rules configured.</p>
 
59 <a href="{% url 'fossil:branch_protection_create' slug=project.slug %}"
60 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
61 Add the first rule
62 </a>
 
63 </div>
64 {% endif %}
 
 
65
66 <div class="mt-6 rounded-md bg-yellow-900/20 border border-yellow-800/50 px-4 py-3">
67 <p class="text-xs text-yellow-300">Branch protection rules are currently advisory. Push enforcement via Fossil hooks is not yet implemented.</p>
68 </div>
69 {% endblock %}
70
--- templates/fossil/branch_protection_list.html
+++ templates/fossil/branch_protection_list.html
@@ -8,16 +8,29 @@
8 <div class="flex items-center justify-between mb-6">
9 <div>
10 <h2 class="text-lg font-semibold text-gray-200">Branch Protection Rules</h2>
11 <p class="text-sm text-gray-500 mt-1">Protect branches from unreviewed changes and require CI checks to pass.</p>
12 </div>
13 <div class="flex items-center gap-3">
14 <input type="search"
15 name="search"
16 value="{{ search }}"
17 placeholder="Search rules..."
18 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
19 hx-get="{% url 'fossil:branch_protections' slug=project.slug %}"
20 hx-trigger="input changed delay:300ms, search"
21 hx-target="#protection-content"
22 hx-swap="innerHTML"
23 hx-push-url="true" />
24 <a href="{% url 'fossil:branch_protection_create' slug=project.slug %}"
25 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
26 Add Rule
27 </a>
28 </div>
29 </div>
30
31 <div id="protection-content">
32 {% if rules %}
33 <div class="space-y-3">
34 {% for rule in rules %}
35 <div class="rounded-lg bg-gray-800 border border-gray-700">
36 <div class="px-5 py-4">
@@ -53,17 +66,21 @@
66 </div>
67 {% endfor %}
68 </div>
69 {% else %}
70 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
71 <p class="text-sm text-gray-500">{% if search %}No rules matching "{{ search }}".{% else %}No branch protection rules configured.{% endif %}</p>
72 {% if not search %}
73 <a href="{% url 'fossil:branch_protection_create' slug=project.slug %}"
74 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
75 Add the first rule
76 </a>
77 {% endif %}
78 </div>
79 {% endif %}
80 {% include "includes/_pagination.html" %}
81 </div>
82
83 <div class="mt-6 rounded-md bg-yellow-900/20 border border-yellow-800/50 px-4 py-3">
84 <p class="text-xs text-yellow-300">Branch protection rules are currently advisory. Push enforcement via Fossil hooks is not yet implemented.</p>
85 </div>
86 {% endblock %}
87
--- templates/fossil/forum_list.html
+++ templates/fossil/forum_list.html
@@ -5,18 +5,31 @@
55
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
66
{% include "fossil/_project_nav.html" %}
77
88
<div class="flex items-center justify-between mb-6">
99
<h2 class="text-lg font-semibold text-gray-200">Forum</h2>
10
- {% if has_write %}
11
- <a href="{% url 'fossil:forum_create' slug=project.slug %}"
12
- class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
13
- New Thread
14
- </a>
15
- {% endif %}
10
+ <div class="flex items-center gap-3">
11
+ <input type="search"
12
+ name="search"
13
+ value="{{ search }}"
14
+ placeholder="Search forum..."
15
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16
+ hx-get="{% url 'fossil:forum' slug=project.slug %}"
17
+ hx-trigger="input changed delay:300ms, search"
18
+ hx-target="#forum-content"
19
+ hx-swap="innerHTML"
20
+ hx-push-url="true" />
21
+ {% if has_write %}
22
+ <a href="{% url 'fossil:forum_create' slug=project.slug %}"
23
+ class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
24
+ New Thread
25
+ </a>
26
+ {% endif %}
27
+ </div>
1628
</div>
1729
30
+<div id="forum-content">
1831
<div class="space-y-3">
1932
{% for post in posts %}
2033
<div class="rounded-lg bg-gray-800 border border-gray-700 hover:border-gray-600 transition-colors">
2134
<div class="px-5 py-4">
2235
{% if post.source == "django" %}
@@ -46,16 +59,18 @@
4659
</div>
4760
</div>
4861
</div>
4962
{% empty %}
5063
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
51
- <p class="text-sm text-gray-500">No forum posts.</p>
52
- {% if has_write %}
64
+ <p class="text-sm text-gray-500">{% if search %}No forum posts matching "{{ search }}".{% else %}No forum posts.{% endif %}</p>
65
+ {% if has_write and not search %}
5366
<a href="{% url 'fossil:forum_create' slug=project.slug %}"
5467
class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
5568
Start the first thread
5669
</a>
5770
{% endif %}
5871
</div>
5972
{% endfor %}
73
+</div>
74
+{% include "includes/_pagination_manual.html" %}
6075
</div>
6176
{% endblock %}
6277
--- templates/fossil/forum_list.html
+++ templates/fossil/forum_list.html
@@ -5,18 +5,31 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Forum</h2>
10 {% if has_write %}
11 <a href="{% url 'fossil:forum_create' slug=project.slug %}"
12 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
13 New Thread
14 </a>
15 {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
16 </div>
17
 
18 <div class="space-y-3">
19 {% for post in posts %}
20 <div class="rounded-lg bg-gray-800 border border-gray-700 hover:border-gray-600 transition-colors">
21 <div class="px-5 py-4">
22 {% if post.source == "django" %}
@@ -46,16 +59,18 @@
46 </div>
47 </div>
48 </div>
49 {% empty %}
50 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
51 <p class="text-sm text-gray-500">No forum posts.</p>
52 {% if has_write %}
53 <a href="{% url 'fossil:forum_create' slug=project.slug %}"
54 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
55 Start the first thread
56 </a>
57 {% endif %}
58 </div>
59 {% endfor %}
 
 
60 </div>
61 {% endblock %}
62
--- templates/fossil/forum_list.html
+++ templates/fossil/forum_list.html
@@ -5,18 +5,31 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Forum</h2>
10 <div class="flex items-center gap-3">
11 <input type="search"
12 name="search"
13 value="{{ search }}"
14 placeholder="Search forum..."
15 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16 hx-get="{% url 'fossil:forum' slug=project.slug %}"
17 hx-trigger="input changed delay:300ms, search"
18 hx-target="#forum-content"
19 hx-swap="innerHTML"
20 hx-push-url="true" />
21 {% if has_write %}
22 <a href="{% url 'fossil:forum_create' slug=project.slug %}"
23 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
24 New Thread
25 </a>
26 {% endif %}
27 </div>
28 </div>
29
30 <div id="forum-content">
31 <div class="space-y-3">
32 {% for post in posts %}
33 <div class="rounded-lg bg-gray-800 border border-gray-700 hover:border-gray-600 transition-colors">
34 <div class="px-5 py-4">
35 {% if post.source == "django" %}
@@ -46,16 +59,18 @@
59 </div>
60 </div>
61 </div>
62 {% empty %}
63 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
64 <p class="text-sm text-gray-500">{% if search %}No forum posts matching "{{ search }}".{% else %}No forum posts.{% endif %}</p>
65 {% if has_write and not search %}
66 <a href="{% url 'fossil:forum_create' slug=project.slug %}"
67 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
68 Start the first thread
69 </a>
70 {% endif %}
71 </div>
72 {% endfor %}
73 </div>
74 {% include "includes/_pagination_manual.html" %}
75 </div>
76 {% endblock %}
77
--- templates/fossil/release_list.html
+++ templates/fossil/release_list.html
@@ -6,18 +6,31 @@
66
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
77
{% include "fossil/_project_nav.html" %}
88
99
<div class="flex items-center justify-between mb-6">
1010
<h2 class="text-lg font-semibold text-gray-200">Releases</h2>
11
- {% if has_write %}
12
- <a href="{% url 'fossil:release_create' slug=project.slug %}"
13
- class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
14
- Create Release
15
- </a>
16
- {% endif %}
11
+ <div class="flex items-center gap-3">
12
+ <input type="search"
13
+ name="search"
14
+ value="{{ search }}"
15
+ placeholder="Search releases..."
16
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
17
+ hx-get="{% url 'fossil:releases' slug=project.slug %}"
18
+ hx-trigger="input changed delay:300ms, search"
19
+ hx-target="#release-content"
20
+ hx-swap="innerHTML"
21
+ hx-push-url="true" />
22
+ {% if has_write %}
23
+ <a href="{% url 'fossil:release_create' slug=project.slug %}"
24
+ class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
25
+ Create Release
26
+ </a>
27
+ {% endif %}
28
+ </div>
1729
</div>
1830
31
+<div id="release-content">
1932
{% if releases %}
2033
<div class="space-y-4">
2134
{% for release in releases %}
2235
<div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
2336
<div class="px-6 py-4">
@@ -52,15 +65,17 @@
5265
</div>
5366
{% endfor %}
5467
</div>
5568
{% else %}
5669
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
57
- <p class="text-sm text-gray-500">No releases yet.</p>
58
- {% if has_write %}
70
+ <p class="text-sm text-gray-500">{% if search %}No releases matching "{{ search }}".{% else %}No releases yet.{% endif %}</p>
71
+ {% if has_write and not search %}
5972
<a href="{% url 'fossil:release_create' slug=project.slug %}"
6073
class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
6174
Create the first release
6275
</a>
6376
{% endif %}
6477
</div>
6578
{% endif %}
79
+{% include "includes/_pagination.html" %}
80
+</div>
6681
{% endblock %}
6782
--- templates/fossil/release_list.html
+++ templates/fossil/release_list.html
@@ -6,18 +6,31 @@
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7 {% include "fossil/_project_nav.html" %}
8
9 <div class="flex items-center justify-between mb-6">
10 <h2 class="text-lg font-semibold text-gray-200">Releases</h2>
11 {% if has_write %}
12 <a href="{% url 'fossil:release_create' slug=project.slug %}"
13 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
14 Create Release
15 </a>
16 {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
17 </div>
18
 
19 {% if releases %}
20 <div class="space-y-4">
21 {% for release in releases %}
22 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
23 <div class="px-6 py-4">
@@ -52,15 +65,17 @@
52 </div>
53 {% endfor %}
54 </div>
55 {% else %}
56 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
57 <p class="text-sm text-gray-500">No releases yet.</p>
58 {% if has_write %}
59 <a href="{% url 'fossil:release_create' slug=project.slug %}"
60 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
61 Create the first release
62 </a>
63 {% endif %}
64 </div>
65 {% endif %}
 
 
66 {% endblock %}
67
--- templates/fossil/release_list.html
+++ templates/fossil/release_list.html
@@ -6,18 +6,31 @@
6 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
7 {% include "fossil/_project_nav.html" %}
8
9 <div class="flex items-center justify-between mb-6">
10 <h2 class="text-lg font-semibold text-gray-200">Releases</h2>
11 <div class="flex items-center gap-3">
12 <input type="search"
13 name="search"
14 value="{{ search }}"
15 placeholder="Search releases..."
16 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
17 hx-get="{% url 'fossil:releases' slug=project.slug %}"
18 hx-trigger="input changed delay:300ms, search"
19 hx-target="#release-content"
20 hx-swap="innerHTML"
21 hx-push-url="true" />
22 {% if has_write %}
23 <a href="{% url 'fossil:release_create' slug=project.slug %}"
24 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
25 Create Release
26 </a>
27 {% endif %}
28 </div>
29 </div>
30
31 <div id="release-content">
32 {% if releases %}
33 <div class="space-y-4">
34 {% for release in releases %}
35 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
36 <div class="px-6 py-4">
@@ -52,15 +65,17 @@
65 </div>
66 {% endfor %}
67 </div>
68 {% else %}
69 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
70 <p class="text-sm text-gray-500">{% if search %}No releases matching "{{ search }}".{% else %}No releases yet.{% endif %}</p>
71 {% if has_write and not search %}
72 <a href="{% url 'fossil:release_create' slug=project.slug %}"
73 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
74 Create the first release
75 </a>
76 {% endif %}
77 </div>
78 {% endif %}
79 {% include "includes/_pagination.html" %}
80 </div>
81 {% endblock %}
82
--- templates/fossil/tag_list.html
+++ templates/fossil/tag_list.html
@@ -3,10 +3,26 @@
33
44
{% block content %}
55
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
66
{% include "fossil/_project_nav.html" %}
77
8
+<div class="flex items-center justify-between mb-4">
9
+ <div>
10
+ <input type="search"
11
+ name="search"
12
+ value="{{ search }}"
13
+ placeholder="Search tags..."
14
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
15
+ hx-get="{% url 'fossil:tags' slug=project.slug %}"
16
+ hx-trigger="input changed delay:300ms, search"
17
+ hx-target="#tag-content"
18
+ hx-swap="innerHTML"
19
+ hx-push-url="true" />
20
+ </div>
21
+</div>
22
+
23
+<div id="tag-content">
824
<div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
925
<table class="min-w-full divide-y divide-gray-700">
1026
<thead class="bg-gray-900">
1127
<tr>
1228
<th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Tag</th>
@@ -29,12 +45,14 @@
2945
</td>
3046
<td class="px-4 py-3 text-sm text-gray-500 text-right">{{ tag.timestamp|date:"Y-m-d" }}</td>
3147
</tr>
3248
{% empty %}
3349
<tr>
34
- <td colspan="4" class="px-4 py-8 text-center text-sm text-gray-500">No tags.</td>
50
+ <td colspan="4" class="px-4 py-8 text-center text-sm text-gray-500">{% if search %}No tags matching "{{ search }}".{% else %}No tags.{% endif %}</td>
3551
</tr>
3652
{% endfor %}
3753
</tbody>
3854
</table>
55
+</div>
56
+{% include "includes/_pagination_manual.html" %}
3957
</div>
4058
{% endblock %}
4159
--- templates/fossil/tag_list.html
+++ templates/fossil/tag_list.html
@@ -3,10 +3,26 @@
3
4 {% block content %}
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8 <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
9 <table class="min-w-full divide-y divide-gray-700">
10 <thead class="bg-gray-900">
11 <tr>
12 <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Tag</th>
@@ -29,12 +45,14 @@
29 </td>
30 <td class="px-4 py-3 text-sm text-gray-500 text-right">{{ tag.timestamp|date:"Y-m-d" }}</td>
31 </tr>
32 {% empty %}
33 <tr>
34 <td colspan="4" class="px-4 py-8 text-center text-sm text-gray-500">No tags.</td>
35 </tr>
36 {% endfor %}
37 </tbody>
38 </table>
 
 
39 </div>
40 {% endblock %}
41
--- templates/fossil/tag_list.html
+++ templates/fossil/tag_list.html
@@ -3,10 +3,26 @@
3
4 {% block content %}
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-4">
9 <div>
10 <input type="search"
11 name="search"
12 value="{{ search }}"
13 placeholder="Search tags..."
14 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
15 hx-get="{% url 'fossil:tags' slug=project.slug %}"
16 hx-trigger="input changed delay:300ms, search"
17 hx-target="#tag-content"
18 hx-swap="innerHTML"
19 hx-push-url="true" />
20 </div>
21 </div>
22
23 <div id="tag-content">
24 <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm">
25 <table class="min-w-full divide-y divide-gray-700">
26 <thead class="bg-gray-900">
27 <tr>
28 <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Tag</th>
@@ -29,12 +45,14 @@
45 </td>
46 <td class="px-4 py-3 text-sm text-gray-500 text-right">{{ tag.timestamp|date:"Y-m-d" }}</td>
47 </tr>
48 {% empty %}
49 <tr>
50 <td colspan="4" class="px-4 py-8 text-center text-sm text-gray-500">{% if search %}No tags matching "{{ search }}".{% else %}No tags.{% endif %}</td>
51 </tr>
52 {% endfor %}
53 </tbody>
54 </table>
55 </div>
56 {% include "includes/_pagination_manual.html" %}
57 </div>
58 {% endblock %}
59
--- templates/fossil/technote_list.html
+++ templates/fossil/technote_list.html
@@ -5,18 +5,31 @@
55
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
66
{% include "fossil/_project_nav.html" %}
77
88
<div class="flex items-center justify-between mb-6">
99
<h2 class="text-lg font-semibold text-gray-200">Technotes</h2>
10
- {% if has_write %}
11
- <a href="{% url 'fossil:technote_create' slug=project.slug %}"
12
- class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
13
- New Technote
14
- </a>
15
- {% endif %}
10
+ <div class="flex items-center gap-3">
11
+ <input type="search"
12
+ name="search"
13
+ value="{{ search }}"
14
+ placeholder="Search technotes..."
15
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16
+ hx-get="{% url 'fossil:technotes' slug=project.slug %}"
17
+ hx-trigger="input changed delay:300ms, search"
18
+ hx-target="#technote-content"
19
+ hx-swap="innerHTML"
20
+ hx-push-url="true" />
21
+ {% if has_write %}
22
+ <a href="{% url 'fossil:technote_create' slug=project.slug %}"
23
+ class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
24
+ New Technote
25
+ </a>
26
+ {% endif %}
27
+ </div>
1628
</div>
1729
30
+<div id="technote-content">
1831
{% if notes %}
1932
<div class="space-y-3">
2033
{% for note in notes %}
2134
<a href="{% url 'fossil:technote_detail' slug=project.slug technote_id=note.uuid %}"
2235
class="block rounded-lg bg-gray-800 border border-gray-700 px-5 py-4 hover:bg-gray-700/50 transition-colors">
@@ -33,15 +46,17 @@
3346
</a>
3447
{% endfor %}
3548
</div>
3649
{% else %}
3750
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
38
- <p class="text-sm text-gray-500">No technotes.</p>
39
- {% if has_write %}
51
+ <p class="text-sm text-gray-500">{% if search %}No technotes matching "{{ search }}".{% else %}No technotes.{% endif %}</p>
52
+ {% if has_write and not search %}
4053
<a href="{% url 'fossil:technote_create' slug=project.slug %}"
4154
class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
4255
Create the first technote
4356
</a>
4457
{% endif %}
4558
</div>
4659
{% endif %}
60
+{% include "includes/_pagination_manual.html" %}
61
+</div>
4762
{% endblock %}
4863
--- templates/fossil/technote_list.html
+++ templates/fossil/technote_list.html
@@ -5,18 +5,31 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Technotes</h2>
10 {% if has_write %}
11 <a href="{% url 'fossil:technote_create' slug=project.slug %}"
12 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
13 New Technote
14 </a>
15 {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
16 </div>
17
 
18 {% if notes %}
19 <div class="space-y-3">
20 {% for note in notes %}
21 <a href="{% url 'fossil:technote_detail' slug=project.slug technote_id=note.uuid %}"
22 class="block rounded-lg bg-gray-800 border border-gray-700 px-5 py-4 hover:bg-gray-700/50 transition-colors">
@@ -33,15 +46,17 @@
33 </a>
34 {% endfor %}
35 </div>
36 {% else %}
37 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
38 <p class="text-sm text-gray-500">No technotes.</p>
39 {% if has_write %}
40 <a href="{% url 'fossil:technote_create' slug=project.slug %}"
41 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
42 Create the first technote
43 </a>
44 {% endif %}
45 </div>
46 {% endif %}
 
 
47 {% endblock %}
48
--- templates/fossil/technote_list.html
+++ templates/fossil/technote_list.html
@@ -5,18 +5,31 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Technotes</h2>
10 <div class="flex items-center gap-3">
11 <input type="search"
12 name="search"
13 value="{{ search }}"
14 placeholder="Search technotes..."
15 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16 hx-get="{% url 'fossil:technotes' slug=project.slug %}"
17 hx-trigger="input changed delay:300ms, search"
18 hx-target="#technote-content"
19 hx-swap="innerHTML"
20 hx-push-url="true" />
21 {% if has_write %}
22 <a href="{% url 'fossil:technote_create' slug=project.slug %}"
23 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
24 New Technote
25 </a>
26 {% endif %}
27 </div>
28 </div>
29
30 <div id="technote-content">
31 {% if notes %}
32 <div class="space-y-3">
33 {% for note in notes %}
34 <a href="{% url 'fossil:technote_detail' slug=project.slug technote_id=note.uuid %}"
35 class="block rounded-lg bg-gray-800 border border-gray-700 px-5 py-4 hover:bg-gray-700/50 transition-colors">
@@ -33,15 +46,17 @@
46 </a>
47 {% endfor %}
48 </div>
49 {% else %}
50 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
51 <p class="text-sm text-gray-500">{% if search %}No technotes matching "{{ search }}".{% else %}No technotes.{% endif %}</p>
52 {% if has_write and not search %}
53 <a href="{% url 'fossil:technote_create' slug=project.slug %}"
54 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
55 Create the first technote
56 </a>
57 {% endif %}
58 </div>
59 {% endif %}
60 {% include "includes/_pagination_manual.html" %}
61 </div>
62 {% endblock %}
63
--- templates/fossil/ticket_fields_list.html
+++ templates/fossil/ticket_fields_list.html
@@ -5,16 +5,29 @@
55
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
66
{% include "fossil/_project_nav.html" %}
77
88
<div class="flex items-center justify-between mb-6">
99
<h2 class="text-lg font-semibold text-gray-200">Custom Ticket Fields</h2>
10
- <a href="{% url 'fossil:ticket_field_create' slug=project.slug %}"
11
- class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
12
- Add Field
13
- </a>
10
+ <div class="flex items-center gap-3">
11
+ <input type="search"
12
+ name="search"
13
+ value="{{ search }}"
14
+ placeholder="Search fields..."
15
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16
+ hx-get="{% url 'fossil:ticket_fields' slug=project.slug %}"
17
+ hx-trigger="input changed delay:300ms, search"
18
+ hx-target="#fields-content"
19
+ hx-swap="innerHTML"
20
+ hx-push-url="true" />
21
+ <a href="{% url 'fossil:ticket_field_create' slug=project.slug %}"
22
+ class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
23
+ Add Field
24
+ </a>
25
+ </div>
1426
</div>
1527
28
+<div id="fields-content">
1629
{% if fields %}
1730
<div class="space-y-3">
1831
{% for field in fields %}
1932
<div class="rounded-lg bg-gray-800 border border-gray-700">
2033
<div class="px-5 py-4">
@@ -49,14 +62,18 @@
4962
</div>
5063
{% endfor %}
5164
</div>
5265
{% else %}
5366
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
54
- <p class="text-sm text-gray-500">No custom ticket fields defined.</p>
67
+ <p class="text-sm text-gray-500">{% if search %}No fields matching "{{ search }}".{% else %}No custom ticket fields defined.{% endif %}</p>
68
+ {% if not search %}
5569
<p class="text-xs text-gray-600 mt-1">Custom fields extend the Fossil ticket schema with project-specific data.</p>
5670
<a href="{% url 'fossil:ticket_field_create' slug=project.slug %}"
5771
class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
5872
Add the first field
5973
</a>
74
+ {% endif %}
6075
</div>
6176
{% endif %}
77
+{% include "includes/_pagination.html" %}
78
+</div>
6279
{% endblock %}
6380
--- templates/fossil/ticket_fields_list.html
+++ templates/fossil/ticket_fields_list.html
@@ -5,16 +5,29 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Custom Ticket Fields</h2>
10 <a href="{% url 'fossil:ticket_field_create' slug=project.slug %}"
11 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
12 Add Field
13 </a>
 
 
 
 
 
 
 
 
 
 
 
 
14 </div>
15
 
16 {% if fields %}
17 <div class="space-y-3">
18 {% for field in fields %}
19 <div class="rounded-lg bg-gray-800 border border-gray-700">
20 <div class="px-5 py-4">
@@ -49,14 +62,18 @@
49 </div>
50 {% endfor %}
51 </div>
52 {% else %}
53 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
54 <p class="text-sm text-gray-500">No custom ticket fields defined.</p>
 
55 <p class="text-xs text-gray-600 mt-1">Custom fields extend the Fossil ticket schema with project-specific data.</p>
56 <a href="{% url 'fossil:ticket_field_create' slug=project.slug %}"
57 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
58 Add the first field
59 </a>
 
60 </div>
61 {% endif %}
 
 
62 {% endblock %}
63
--- templates/fossil/ticket_fields_list.html
+++ templates/fossil/ticket_fields_list.html
@@ -5,16 +5,29 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Custom Ticket Fields</h2>
10 <div class="flex items-center gap-3">
11 <input type="search"
12 name="search"
13 value="{{ search }}"
14 placeholder="Search fields..."
15 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16 hx-get="{% url 'fossil:ticket_fields' slug=project.slug %}"
17 hx-trigger="input changed delay:300ms, search"
18 hx-target="#fields-content"
19 hx-swap="innerHTML"
20 hx-push-url="true" />
21 <a href="{% url 'fossil:ticket_field_create' slug=project.slug %}"
22 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
23 Add Field
24 </a>
25 </div>
26 </div>
27
28 <div id="fields-content">
29 {% if fields %}
30 <div class="space-y-3">
31 {% for field in fields %}
32 <div class="rounded-lg bg-gray-800 border border-gray-700">
33 <div class="px-5 py-4">
@@ -49,14 +62,18 @@
62 </div>
63 {% endfor %}
64 </div>
65 {% else %}
66 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
67 <p class="text-sm text-gray-500">{% if search %}No fields matching "{{ search }}".{% else %}No custom ticket fields defined.{% endif %}</p>
68 {% if not search %}
69 <p class="text-xs text-gray-600 mt-1">Custom fields extend the Fossil ticket schema with project-specific data.</p>
70 <a href="{% url 'fossil:ticket_field_create' slug=project.slug %}"
71 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
72 Add the first field
73 </a>
74 {% endif %}
75 </div>
76 {% endif %}
77 {% include "includes/_pagination.html" %}
78 </div>
79 {% endblock %}
80
--- templates/fossil/ticket_reports_list.html
+++ templates/fossil/ticket_reports_list.html
@@ -5,18 +5,31 @@
55
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
66
{% include "fossil/_project_nav.html" %}
77
88
<div class="flex items-center justify-between mb-6">
99
<h2 class="text-lg font-semibold text-gray-200">Ticket Reports</h2>
10
- {% if can_admin %}
11
- <a href="{% url 'fossil:ticket_report_create' slug=project.slug %}"
12
- class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
13
- Create Report
14
- </a>
15
- {% endif %}
10
+ <div class="flex items-center gap-3">
11
+ <input type="search"
12
+ name="search"
13
+ value="{{ search }}"
14
+ placeholder="Search reports..."
15
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16
+ hx-get="{% url 'fossil:ticket_reports' slug=project.slug %}"
17
+ hx-trigger="input changed delay:300ms, search"
18
+ hx-target="#reports-content"
19
+ hx-swap="innerHTML"
20
+ hx-push-url="true" />
21
+ {% if can_admin %}
22
+ <a href="{% url 'fossil:ticket_report_create' slug=project.slug %}"
23
+ class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
24
+ Create Report
25
+ </a>
26
+ {% endif %}
27
+ </div>
1628
</div>
1729
30
+<div id="reports-content">
1831
{% if reports %}
1932
<div class="space-y-3">
2033
{% for report in reports %}
2134
<div class="rounded-lg bg-gray-800 border border-gray-700">
2235
<div class="px-5 py-4">
@@ -46,16 +59,20 @@
4659
</div>
4760
{% endfor %}
4861
</div>
4962
{% else %}
5063
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
51
- <p class="text-sm text-gray-500">No ticket reports defined.</p>
64
+ <p class="text-sm text-gray-500">{% if search %}No reports matching "{{ search }}".{% else %}No ticket reports defined.{% endif %}</p>
65
+ {% if not search %}
5266
<p class="text-xs text-gray-600 mt-1">Reports let you run custom SQL queries against the Fossil ticket database.</p>
5367
{% if can_admin %}
5468
<a href="{% url 'fossil:ticket_report_create' slug=project.slug %}"
5569
class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
5670
Create the first report
5771
</a>
5872
{% endif %}
73
+ {% endif %}
5974
</div>
6075
{% endif %}
76
+{% include "includes/_pagination.html" %}
77
+</div>
6178
{% endblock %}
6279
--- templates/fossil/ticket_reports_list.html
+++ templates/fossil/ticket_reports_list.html
@@ -5,18 +5,31 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Ticket Reports</h2>
10 {% if can_admin %}
11 <a href="{% url 'fossil:ticket_report_create' slug=project.slug %}"
12 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
13 Create Report
14 </a>
15 {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
16 </div>
17
 
18 {% if reports %}
19 <div class="space-y-3">
20 {% for report in reports %}
21 <div class="rounded-lg bg-gray-800 border border-gray-700">
22 <div class="px-5 py-4">
@@ -46,16 +59,20 @@
46 </div>
47 {% endfor %}
48 </div>
49 {% else %}
50 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
51 <p class="text-sm text-gray-500">No ticket reports defined.</p>
 
52 <p class="text-xs text-gray-600 mt-1">Reports let you run custom SQL queries against the Fossil ticket database.</p>
53 {% if can_admin %}
54 <a href="{% url 'fossil:ticket_report_create' slug=project.slug %}"
55 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
56 Create the first report
57 </a>
58 {% endif %}
 
59 </div>
60 {% endif %}
 
 
61 {% endblock %}
62
--- templates/fossil/ticket_reports_list.html
+++ templates/fossil/ticket_reports_list.html
@@ -5,18 +5,31 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Ticket Reports</h2>
10 <div class="flex items-center gap-3">
11 <input type="search"
12 name="search"
13 value="{{ search }}"
14 placeholder="Search reports..."
15 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16 hx-get="{% url 'fossil:ticket_reports' slug=project.slug %}"
17 hx-trigger="input changed delay:300ms, search"
18 hx-target="#reports-content"
19 hx-swap="innerHTML"
20 hx-push-url="true" />
21 {% if can_admin %}
22 <a href="{% url 'fossil:ticket_report_create' slug=project.slug %}"
23 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
24 Create Report
25 </a>
26 {% endif %}
27 </div>
28 </div>
29
30 <div id="reports-content">
31 {% if reports %}
32 <div class="space-y-3">
33 {% for report in reports %}
34 <div class="rounded-lg bg-gray-800 border border-gray-700">
35 <div class="px-5 py-4">
@@ -46,16 +59,20 @@
59 </div>
60 {% endfor %}
61 </div>
62 {% else %}
63 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
64 <p class="text-sm text-gray-500">{% if search %}No reports matching "{{ search }}".{% else %}No ticket reports defined.{% endif %}</p>
65 {% if not search %}
66 <p class="text-xs text-gray-600 mt-1">Reports let you run custom SQL queries against the Fossil ticket database.</p>
67 {% if can_admin %}
68 <a href="{% url 'fossil:ticket_report_create' slug=project.slug %}"
69 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
70 Create the first report
71 </a>
72 {% endif %}
73 {% endif %}
74 </div>
75 {% endif %}
76 {% include "includes/_pagination.html" %}
77 </div>
78 {% endblock %}
79
--- templates/fossil/unversioned_list.html
+++ templates/fossil/unversioned_list.html
@@ -5,12 +5,25 @@
55
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
66
{% include "fossil/_project_nav.html" %}
77
88
<div class="flex items-center justify-between mb-6">
99
<h2 class="text-lg font-semibold text-gray-200">Unversioned Files</h2>
10
+ <div>
11
+ <input type="search"
12
+ name="search"
13
+ value="{{ search }}"
14
+ placeholder="Search files..."
15
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16
+ hx-get="{% url 'fossil:unversioned' slug=project.slug %}"
17
+ hx-trigger="input changed delay:300ms, search"
18
+ hx-target="#unversioned-content"
19
+ hx-swap="innerHTML"
20
+ hx-push-url="true" />
21
+ </div>
1022
</div>
1123
24
+<div id="unversioned-content">
1225
{% if files %}
1326
<div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
1427
<table class="min-w-full divide-y divide-gray-700">
1528
<thead class="bg-gray-900/50">
1629
<tr>
@@ -37,13 +50,15 @@
3750
</tbody>
3851
</table>
3952
</div>
4053
{% else %}
4154
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
42
- <p class="text-sm text-gray-500">No unversioned files.</p>
55
+ <p class="text-sm text-gray-500">{% if search %}No files matching "{{ search }}".{% else %}No unversioned files.{% endif %}</p>
4356
</div>
4457
{% endif %}
58
+{% include "includes/_pagination_manual.html" %}
59
+</div>
4560
4661
{% if has_admin %}
4762
<div class="mt-6">
4863
<form method="post"
4964
action="{% url 'fossil:unversioned_upload' slug=project.slug %}"
5065
--- templates/fossil/unversioned_list.html
+++ templates/fossil/unversioned_list.html
@@ -5,12 +5,25 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Unversioned Files</h2>
 
 
 
 
 
 
 
 
 
 
 
 
10 </div>
11
 
12 {% if files %}
13 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
14 <table class="min-w-full divide-y divide-gray-700">
15 <thead class="bg-gray-900/50">
16 <tr>
@@ -37,13 +50,15 @@
37 </tbody>
38 </table>
39 </div>
40 {% else %}
41 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
42 <p class="text-sm text-gray-500">No unversioned files.</p>
43 </div>
44 {% endif %}
 
 
45
46 {% if has_admin %}
47 <div class="mt-6">
48 <form method="post"
49 action="{% url 'fossil:unversioned_upload' slug=project.slug %}"
50
--- templates/fossil/unversioned_list.html
+++ templates/fossil/unversioned_list.html
@@ -5,12 +5,25 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Unversioned Files</h2>
10 <div>
11 <input type="search"
12 name="search"
13 value="{{ search }}"
14 placeholder="Search files..."
15 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16 hx-get="{% url 'fossil:unversioned' slug=project.slug %}"
17 hx-trigger="input changed delay:300ms, search"
18 hx-target="#unversioned-content"
19 hx-swap="innerHTML"
20 hx-push-url="true" />
21 </div>
22 </div>
23
24 <div id="unversioned-content">
25 {% if files %}
26 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
27 <table class="min-w-full divide-y divide-gray-700">
28 <thead class="bg-gray-900/50">
29 <tr>
@@ -37,13 +50,15 @@
50 </tbody>
51 </table>
52 </div>
53 {% else %}
54 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
55 <p class="text-sm text-gray-500">{% if search %}No files matching "{{ search }}".{% else %}No unversioned files.{% endif %}</p>
56 </div>
57 {% endif %}
58 {% include "includes/_pagination_manual.html" %}
59 </div>
60
61 {% if has_admin %}
62 <div class="mt-6">
63 <form method="post"
64 action="{% url 'fossil:unversioned_upload' slug=project.slug %}"
65
--- templates/fossil/webhook_list.html
+++ templates/fossil/webhook_list.html
@@ -5,16 +5,29 @@
55
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
66
{% include "fossil/_project_nav.html" %}
77
88
<div class="flex items-center justify-between mb-6">
99
<h2 class="text-lg font-semibold text-gray-200">Webhooks</h2>
10
- <a href="{% url 'fossil:webhook_create' slug=project.slug %}"
11
- class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
12
- Add Webhook
13
- </a>
10
+ <div class="flex items-center gap-3">
11
+ <input type="search"
12
+ name="search"
13
+ value="{{ search }}"
14
+ placeholder="Search webhooks..."
15
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16
+ hx-get="{% url 'fossil:webhooks' slug=project.slug %}"
17
+ hx-trigger="input changed delay:300ms, search"
18
+ hx-target="#webhook-content"
19
+ hx-swap="innerHTML"
20
+ hx-push-url="true" />
21
+ <a href="{% url 'fossil:webhook_create' slug=project.slug %}"
22
+ class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
23
+ Add Webhook
24
+ </a>
25
+ </div>
1426
</div>
1527
28
+<div id="webhook-content">
1629
{% if webhooks %}
1730
<div class="space-y-3">
1831
{% for webhook in webhooks %}
1932
<div class="rounded-lg bg-gray-800 border border-gray-700">
2033
<div class="px-5 py-4">
@@ -54,13 +67,17 @@
5467
</div>
5568
{% endfor %}
5669
</div>
5770
{% else %}
5871
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
59
- <p class="text-sm text-gray-500">No webhooks configured.</p>
72
+ <p class="text-sm text-gray-500">{% if search %}No webhooks matching "{{ search }}".{% else %}No webhooks configured.{% endif %}</p>
73
+ {% if not search %}
6074
<a href="{% url 'fossil:webhook_create' slug=project.slug %}"
6175
class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
6276
Add the first webhook
6377
</a>
78
+ {% endif %}
6479
</div>
6580
{% endif %}
81
+{% include "includes/_pagination.html" %}
82
+</div>
6683
{% endblock %}
6784
--- templates/fossil/webhook_list.html
+++ templates/fossil/webhook_list.html
@@ -5,16 +5,29 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Webhooks</h2>
10 <a href="{% url 'fossil:webhook_create' slug=project.slug %}"
11 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
12 Add Webhook
13 </a>
 
 
 
 
 
 
 
 
 
 
 
 
14 </div>
15
 
16 {% if webhooks %}
17 <div class="space-y-3">
18 {% for webhook in webhooks %}
19 <div class="rounded-lg bg-gray-800 border border-gray-700">
20 <div class="px-5 py-4">
@@ -54,13 +67,17 @@
54 </div>
55 {% endfor %}
56 </div>
57 {% else %}
58 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
59 <p class="text-sm text-gray-500">No webhooks configured.</p>
 
60 <a href="{% url 'fossil:webhook_create' slug=project.slug %}"
61 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
62 Add the first webhook
63 </a>
 
64 </div>
65 {% endif %}
 
 
66 {% endblock %}
67
--- templates/fossil/webhook_list.html
+++ templates/fossil/webhook_list.html
@@ -5,16 +5,29 @@
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-6">
9 <h2 class="text-lg font-semibold text-gray-200">Webhooks</h2>
10 <div class="flex items-center gap-3">
11 <input type="search"
12 name="search"
13 value="{{ search }}"
14 placeholder="Search webhooks..."
15 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
16 hx-get="{% url 'fossil:webhooks' slug=project.slug %}"
17 hx-trigger="input changed delay:300ms, search"
18 hx-target="#webhook-content"
19 hx-swap="innerHTML"
20 hx-push-url="true" />
21 <a href="{% url 'fossil:webhook_create' slug=project.slug %}"
22 class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
23 Add Webhook
24 </a>
25 </div>
26 </div>
27
28 <div id="webhook-content">
29 {% if webhooks %}
30 <div class="space-y-3">
31 {% for webhook in webhooks %}
32 <div class="rounded-lg bg-gray-800 border border-gray-700">
33 <div class="px-5 py-4">
@@ -54,13 +67,17 @@
67 </div>
68 {% endfor %}
69 </div>
70 {% else %}
71 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
72 <p class="text-sm text-gray-500">{% if search %}No webhooks matching "{{ search }}".{% else %}No webhooks configured.{% endif %}</p>
73 {% if not search %}
74 <a href="{% url 'fossil:webhook_create' slug=project.slug %}"
75 class="mt-3 inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
76 Add the first webhook
77 </a>
78 {% endif %}
79 </div>
80 {% endif %}
81 {% include "includes/_pagination.html" %}
82 </div>
83 {% endblock %}
84
--- templates/fossil/wiki_list.html
+++ templates/fossil/wiki_list.html
@@ -4,20 +4,32 @@
44
{% block content %}
55
<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
66
{% include "fossil/_project_nav.html" %}
77
88
<div class="flex items-center justify-between mb-4">
9
- <div></div>
9
+ <div>
10
+ <input type="search"
11
+ name="search"
12
+ value="{{ search }}"
13
+ placeholder="Search wiki pages..."
14
+ class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
15
+ hx-get="{% url 'fossil:wiki' slug=project.slug %}"
16
+ hx-trigger="input changed delay:300ms, search"
17
+ hx-target="#wiki-content"
18
+ hx-swap="innerHTML"
19
+ hx-push-url="true" />
20
+ </div>
1021
{% if perms.projects.change_project %}
1122
<a href="{% url 'fossil:wiki_create' slug=project.slug %}" class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">New Page</a>
1223
{% endif %}
1324
</div>
1425
26
+<div id="wiki-content">
1527
<div class="flex gap-6">
1628
<!-- Main content: home page or page index -->
1729
<div class="flex-1 min-w-0">
18
- {% if home_page %}
30
+ {% if home_page and not search %}
1931
<div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
2032
<div class="px-6 py-4 border-b border-gray-700">
2133
<h2 class="text-lg font-semibold text-gray-100">{{ home_page.name }}</h2>
2234
</div>
2335
<div class="px-6 py-6">
@@ -24,13 +36,13 @@
2436
<div class="prose prose-invert prose-gray max-w-none">
2537
{{ home_content_html }}
2638
</div>
2739
</div>
2840
</div>
29
- {% else %}
41
+ {% elif not pages %}
3042
<div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
31
- <p class="text-sm text-gray-500">No wiki pages yet. Create a "Home" page to get started.</p>
43
+ <p class="text-sm text-gray-500">{% if search %}No wiki pages matching "{{ search }}".{% else %}No wiki pages yet. Create a "Home" page to get started.{% endif %}</p>
3244
</div>
3345
{% endif %}
3446
</div>
3547
3648
<!-- Right sidebar: all pages -->
@@ -48,7 +60,9 @@
4860
{% if not pages %}
4961
<p class="text-xs text-gray-600 px-3">No pages.</p>
5062
{% endif %}
5163
</div>
5264
</aside>
65
+</div>
66
+{% include "includes/_pagination_manual.html" %}
5367
</div>
5468
{% endblock %}
5569
5670
ADDED templates/includes/_pagination.html
5771
ADDED templates/includes/_pagination_manual.html
--- templates/fossil/wiki_list.html
+++ templates/fossil/wiki_list.html
@@ -4,20 +4,32 @@
4 {% block content %}
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-4">
9 <div></div>
 
 
 
 
 
 
 
 
 
 
 
10 {% if perms.projects.change_project %}
11 <a href="{% url 'fossil:wiki_create' slug=project.slug %}" class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">New Page</a>
12 {% endif %}
13 </div>
14
 
15 <div class="flex gap-6">
16 <!-- Main content: home page or page index -->
17 <div class="flex-1 min-w-0">
18 {% if home_page %}
19 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
20 <div class="px-6 py-4 border-b border-gray-700">
21 <h2 class="text-lg font-semibold text-gray-100">{{ home_page.name }}</h2>
22 </div>
23 <div class="px-6 py-6">
@@ -24,13 +36,13 @@
24 <div class="prose prose-invert prose-gray max-w-none">
25 {{ home_content_html }}
26 </div>
27 </div>
28 </div>
29 {% else %}
30 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
31 <p class="text-sm text-gray-500">No wiki pages yet. Create a "Home" page to get started.</p>
32 </div>
33 {% endif %}
34 </div>
35
36 <!-- Right sidebar: all pages -->
@@ -48,7 +60,9 @@
48 {% if not pages %}
49 <p class="text-xs text-gray-600 px-3">No pages.</p>
50 {% endif %}
51 </div>
52 </aside>
 
 
53 </div>
54 {% endblock %}
55
56 DDED templates/includes/_pagination.html
57 DDED templates/includes/_pagination_manual.html
--- templates/fossil/wiki_list.html
+++ templates/fossil/wiki_list.html
@@ -4,20 +4,32 @@
4 {% block content %}
5 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
6 {% include "fossil/_project_nav.html" %}
7
8 <div class="flex items-center justify-between mb-4">
9 <div>
10 <input type="search"
11 name="search"
12 value="{{ search }}"
13 placeholder="Search wiki pages..."
14 class="rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand text-sm px-3 py-1.5"
15 hx-get="{% url 'fossil:wiki' slug=project.slug %}"
16 hx-trigger="input changed delay:300ms, search"
17 hx-target="#wiki-content"
18 hx-swap="innerHTML"
19 hx-push-url="true" />
20 </div>
21 {% if perms.projects.change_project %}
22 <a href="{% url 'fossil:wiki_create' slug=project.slug %}" class="inline-flex items-center rounded-md bg-brand px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">New Page</a>
23 {% endif %}
24 </div>
25
26 <div id="wiki-content">
27 <div class="flex gap-6">
28 <!-- Main content: home page or page index -->
29 <div class="flex-1 min-w-0">
30 {% if home_page and not search %}
31 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
32 <div class="px-6 py-4 border-b border-gray-700">
33 <h2 class="text-lg font-semibold text-gray-100">{{ home_page.name }}</h2>
34 </div>
35 <div class="px-6 py-6">
@@ -24,13 +36,13 @@
36 <div class="prose prose-invert prose-gray max-w-none">
37 {{ home_content_html }}
38 </div>
39 </div>
40 </div>
41 {% elif not pages %}
42 <div class="rounded-lg bg-gray-800 border border-gray-700 p-8 text-center">
43 <p class="text-sm text-gray-500">{% if search %}No wiki pages matching "{{ search }}".{% else %}No wiki pages yet. Create a "Home" page to get started.{% endif %}</p>
44 </div>
45 {% endif %}
46 </div>
47
48 <!-- Right sidebar: all pages -->
@@ -48,7 +60,9 @@
60 {% if not pages %}
61 <p class="text-xs text-gray-600 px-3">No pages.</p>
62 {% endif %}
63 </div>
64 </aside>
65 </div>
66 {% include "includes/_pagination_manual.html" %}
67 </div>
68 {% endblock %}
69
70 DDED templates/includes/_pagination.html
71 DDED templates/includes/_pagination_manual.html
--- a/templates/includes/_pagination.html
+++ b/templates/includes/_pagination.html
@@ -0,0 +1,2 @@
1
+{% if page_obj.has_other__</div>
2
+ page_obj.has_other{% if page_obj.has_other_
--- a/templates/includes/_pagination.html
+++ b/templates/includes/_pagination.html
@@ -0,0 +1,2 @@
 
 
--- a/templates/includes/_pagination.html
+++ b/templates/includes/_pagination.html
@@ -0,0 +1,2 @@
1 {% if page_obj.has_other__</div>
2 page_obj.has_other{% if page_obj.has_other_
--- a/templates/includes/_pagination_manual.html
+++ b/templates/includes/_pagination_manual.html
@@ -0,0 +1,12 @@
1
+{% if pagination.num_pages > 1 es > 1 or per_page_options %}
2
+<nav class="flex items-center justify-between botext-xs text-gray-500">
3
+ ination.num_pages }}
4
+ ({{ pa</div>
5
+ <div class="flex gap-1">
6
+ {% if pagination.has_previous %}
7
+ <a href="?page={{ pagination.previous_page_number }}{% if er_page }}{% endif %}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
8
+ class="px-3 py-1 text-xs ">um_pages > 1 or per_page_options %}
9
+<nav class="flex items-center justify-between border-t bo{% if pagination.num_pages > 1 or per_page_options %}
10
+<nav class="flex items-center justify-between border-t border-gray-700 px-2 py-3 mt-4">
11
+ <div class="flex items-center gap-4">
12
+ <span class="text-xs text-gray
--- a/templates/includes/_pagination_manual.html
+++ b/templates/includes/_pagination_manual.html
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/includes/_pagination_manual.html
+++ b/templates/includes/_pagination_manual.html
@@ -0,0 +1,12 @@
1 {% if pagination.num_pages > 1 es > 1 or per_page_options %}
2 <nav class="flex items-center justify-between botext-xs text-gray-500">
3 ination.num_pages }}
4 ({{ pa</div>
5 <div class="flex gap-1">
6 {% if pagination.has_previous %}
7 <a href="?page={{ pagination.previous_page_number }}{% if er_page }}{% endif %}{% if search %}&search={{ search|urlencode }}{% endif %}{% if extra_params %}{{ extra_params }}{% endif %}"
8 class="px-3 py-1 text-xs ">um_pages > 1 or per_page_options %}
9 <nav class="flex items-center justify-between border-t bo{% if pagination.num_pages > 1 or per_page_options %}
10 <nav class="flex items-center justify-between border-t border-gray-700 px-2 py-3 mt-4">
11 <div class="flex items-center gap-4">
12 <span class="text-xs text-gray
--- templates/organization/audit_log.html
+++ templates/organization/audit_log.html
@@ -65,6 +65,9 @@
6565
</tr>
6666
{% endfor %}
6767
</tbody>
6868
</table>
6969
</div>
70
+{% with extra_params="&model="|add:model_filter %}
71
+{% include "includes/_pagination_manual.html" %}
72
+{% endwith %}
7073
{% endblock %}
7174
--- templates/organization/audit_log.html
+++ templates/organization/audit_log.html
@@ -65,6 +65,9 @@
65 </tr>
66 {% endfor %}
67 </tbody>
68 </table>
69 </div>
 
 
 
70 {% endblock %}
71
--- templates/organization/audit_log.html
+++ templates/organization/audit_log.html
@@ -65,6 +65,9 @@
65 </tr>
66 {% endfor %}
67 </tbody>
68 </table>
69 </div>
70 {% with extra_params="&model="|add:model_filter %}
71 {% include "includes/_pagination_manual.html" %}
72 {% endwith %}
73 {% endblock %}
74
--- templates/organization/member_list.html
+++ templates/organization/member_list.html
@@ -36,6 +36,7 @@
3636
hx-swap="outerHTML"
3737
hx-push-url="true" />
3838
</div>
3939
4040
{% include "organization/partials/member_table.html" %}
41
+{% include "includes/_pagination.html" %}
4142
{% endblock %}
4243
--- templates/organization/member_list.html
+++ templates/organization/member_list.html
@@ -36,6 +36,7 @@
36 hx-swap="outerHTML"
37 hx-push-url="true" />
38 </div>
39
40 {% include "organization/partials/member_table.html" %}
 
41 {% endblock %}
42
--- templates/organization/member_list.html
+++ templates/organization/member_list.html
@@ -36,6 +36,7 @@
36 hx-swap="outerHTML"
37 hx-push-url="true" />
38 </div>
39
40 {% include "organization/partials/member_table.html" %}
41 {% include "includes/_pagination.html" %}
42 {% endblock %}
43
--- templates/organization/team_list.html
+++ templates/organization/team_list.html
@@ -28,6 +28,7 @@
2828
hx-swap="outerHTML"
2929
hx-push-url="true" />
3030
</div>
3131
3232
{% include "organization/partials/team_table.html" %}
33
+{% include "includes/_pagination.html" %}
3334
{% endblock %}
3435
--- templates/organization/team_list.html
+++ templates/organization/team_list.html
@@ -28,6 +28,7 @@
28 hx-swap="outerHTML"
29 hx-push-url="true" />
30 </div>
31
32 {% include "organization/partials/team_table.html" %}
 
33 {% endblock %}
34
--- templates/organization/team_list.html
+++ templates/organization/team_list.html
@@ -28,6 +28,7 @@
28 hx-swap="outerHTML"
29 hx-push-url="true" />
30 </div>
31
32 {% include "organization/partials/team_table.html" %}
33 {% include "includes/_pagination.html" %}
34 {% endblock %}
35
--- templates/pages/page_list.html
+++ templates/pages/page_list.html
@@ -24,6 +24,7 @@
2424
hx-swap="outerHTML"
2525
hx-push-url="true" />
2626
</div>
2727
2828
{% include "pages/partials/page_table.html" %}
29
+{% include "includes/_pagination.html" %}
2930
{% endblock %}
3031
--- templates/pages/page_list.html
+++ templates/pages/page_list.html
@@ -24,6 +24,7 @@
24 hx-swap="outerHTML"
25 hx-push-url="true" />
26 </div>
27
28 {% include "pages/partials/page_table.html" %}
 
29 {% endblock %}
30
--- templates/pages/page_list.html
+++ templates/pages/page_list.html
@@ -24,6 +24,7 @@
24 hx-swap="outerHTML"
25 hx-push-url="true" />
26 </div>
27
28 {% include "pages/partials/page_table.html" %}
29 {% include "includes/_pagination.html" %}
30 {% endblock %}
31
--- templates/projects/group_list.html
+++ templates/projects/group_list.html
@@ -9,8 +9,22 @@
99
class="mt-4 md:mt-0 inline-flex items-center rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
1010
New Group
1111
</a>
1212
{% endif %}
1313
</div>
14
+
15
+<div class="mb-4">
16
+ <input type="search"
17
+ name="search"
18
+ value="{{ search }}"
19
+ placeholder="Search groups..."
20
+ class="w-full max-w-md rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
21
+ hx-get="{% url 'projects:group_list' %}"
22
+ hx-trigger="input changed delay:300ms, search"
23
+ hx-target="#group-table"
24
+ hx-swap="outerHTML"
25
+ hx-push-url="true" />
26
+</div>
1427
1528
{% include "projects/partials/group_table.html" %}
29
+{% include "includes/_pagination.html" %}
1630
{% endblock %}
1731
--- templates/projects/group_list.html
+++ templates/projects/group_list.html
@@ -9,8 +9,22 @@
9 class="mt-4 md:mt-0 inline-flex items-center rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
10 New Group
11 </a>
12 {% endif %}
13 </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
14
15 {% include "projects/partials/group_table.html" %}
 
16 {% endblock %}
17
--- templates/projects/group_list.html
+++ templates/projects/group_list.html
@@ -9,8 +9,22 @@
9 class="mt-4 md:mt-0 inline-flex items-center rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">
10 New Group
11 </a>
12 {% endif %}
13 </div>
14
15 <div class="mb-4">
16 <input type="search"
17 name="search"
18 value="{{ search }}"
19 placeholder="Search groups..."
20 class="w-full max-w-md rounded-md border-gray-700 bg-gray-800 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm"
21 hx-get="{% url 'projects:group_list' %}"
22 hx-trigger="input changed delay:300ms, search"
23 hx-target="#group-table"
24 hx-swap="outerHTML"
25 hx-push-url="true" />
26 </div>
27
28 {% include "projects/partials/group_table.html" %}
29 {% include "includes/_pagination.html" %}
30 {% endblock %}
31
--- templates/projects/project_list.html
+++ templates/projects/project_list.html
@@ -24,6 +24,7 @@
2424
hx-swap="outerHTML"
2525
hx-push-url="true" />
2626
</div>
2727
2828
{% include "projects/partials/project_table.html" %}
29
+{% include "includes/_pagination.html" %}
2930
{% endblock %}
3031
--- templates/projects/project_list.html
+++ templates/projects/project_list.html
@@ -24,6 +24,7 @@
24 hx-swap="outerHTML"
25 hx-push-url="true" />
26 </div>
27
28 {% include "projects/partials/project_table.html" %}
 
29 {% endblock %}
30
--- templates/projects/project_list.html
+++ templates/projects/project_list.html
@@ -24,6 +24,7 @@
24 hx-swap="outerHTML"
25 hx-push-url="true" />
26 </div>
27
28 {% include "projects/partials/project_table.html" %}
29 {% include "includes/_pagination.html" %}
30 {% endblock %}
31

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button