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.
Commit
4e1dde0f6daf98a49ebe7324af56749aaaaf05247adae068fd5b4e4d724cc2ec
Parent
0cf03d9c9a79cf8…
24 files changed
+175
-12
+38
-6
+6
-2
+15
-4
+22
-5
+19
-1
+22
-5
+23
-8
+23
-8
+19
-1
+23
-8
+22
-5
+24
-7
+16
-1
+22
-5
+18
-4
+2
+12
+3
+1
+1
+1
+14
+1
~
fossil/views.py
~
organization/views.py
~
pages/views.py
~
projects/views.py
~
templates/fossil/api_token_list.html
~
templates/fossil/branch_list.html
~
templates/fossil/branch_protection_list.html
~
templates/fossil/forum_list.html
~
templates/fossil/release_list.html
~
templates/fossil/tag_list.html
~
templates/fossil/technote_list.html
~
templates/fossil/ticket_fields_list.html
~
templates/fossil/ticket_reports_list.html
~
templates/fossil/unversioned_list.html
~
templates/fossil/webhook_list.html
~
templates/fossil/wiki_list.html
+
templates/includes/_pagination.html
+
templates/includes/_pagination_manual.html
~
templates/organization/audit_log.html
~
templates/organization/member_list.html
~
templates/organization/team_list.html
~
templates/pages/page_list.html
~
templates/projects/group_list.html
~
templates/projects/project_list.html
+175
-12
| --- fossil/views.py | ||
| +++ fossil/views.py | ||
| @@ -1,11 +1,13 @@ | ||
| 1 | 1 | import contextlib |
| 2 | +import math | |
| 2 | 3 | import re |
| 3 | 4 | from datetime import datetime |
| 4 | 5 | |
| 5 | 6 | import markdown as md |
| 6 | 7 | from django.contrib.auth.decorators import login_required |
| 8 | +from django.core.paginator import Paginator | |
| 7 | 9 | from django.http import Http404, HttpResponse, JsonResponse |
| 8 | 10 | from django.shortcuts import get_object_or_404, redirect, render |
| 9 | 11 | from django.utils.safestring import mark_safe |
| 10 | 12 | from django.views.decorators.csrf import csrf_exempt |
| 11 | 13 | |
| @@ -13,10 +15,37 @@ | ||
| 13 | 15 | from projects.models import Project |
| 14 | 16 | |
| 15 | 17 | from .models import FossilRepository |
| 16 | 18 | from .reader import FossilReader |
| 17 | 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 | + | |
| 18 | 47 | |
| 19 | 48 | def _render_fossil_content(content: str, project_slug: str = "", base_path: str = "") -> str: |
| 20 | 49 | """Render content that may be Fossil wiki markup, HTML, or Markdown. |
| 21 | 50 | |
| 22 | 51 | Fossil wiki pages can contain: |
| @@ -682,12 +711,10 @@ | ||
| 682 | 711 | |
| 683 | 712 | if search: |
| 684 | 713 | tickets = [t for t in tickets if search.lower() in t.title.lower()] |
| 685 | 714 | |
| 686 | 715 | total = len(tickets) |
| 687 | - import math | |
| 688 | - | |
| 689 | 716 | total_pages = max(1, math.ceil(total / per_page)) |
| 690 | 717 | page = min(page, total_pages) |
| 691 | 718 | tickets = tickets[(page - 1) * per_page : page * per_page] |
| 692 | 719 | has_next = page < total_pages |
| 693 | 720 | has_prev = page > 1 |
| @@ -759,13 +786,35 @@ | ||
| 759 | 786 | |
| 760 | 787 | with reader: |
| 761 | 788 | pages = reader.get_wiki_pages() |
| 762 | 789 | home_page = reader.get_wiki_page("Home") |
| 763 | 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 | + | |
| 764 | 797 | home_content_html = "" |
| 765 | 798 | if home_page: |
| 766 | 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 | + ) | |
| 767 | 816 | |
| 768 | 817 | return render( |
| 769 | 818 | request, |
| 770 | 819 | "fossil/wiki_list.html", |
| 771 | 820 | { |
| @@ -772,10 +821,12 @@ | ||
| 772 | 821 | "project": project, |
| 773 | 822 | "fossil_repo": fossil_repo, |
| 774 | 823 | "pages": pages, |
| 775 | 824 | "home_page": home_page, |
| 776 | 825 | "home_content_html": home_content_html, |
| 826 | + "search": search, | |
| 827 | + "pagination": pagination, | |
| 777 | 828 | "active_tab": "wiki", |
| 778 | 829 | }, |
| 779 | 830 | ) |
| 780 | 831 | |
| 781 | 832 | |
| @@ -844,10 +895,17 @@ | ||
| 844 | 895 | ) |
| 845 | 896 | |
| 846 | 897 | # Sort merged list by timestamp descending |
| 847 | 898 | merged.sort(key=lambda x: x["timestamp"], reverse=True) |
| 848 | 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 | + | |
| 849 | 907 | has_write = can_write_project(request.user, project) |
| 850 | 908 | |
| 851 | 909 | return render( |
| 852 | 910 | request, |
| 853 | 911 | "fossil/forum_list.html", |
| @@ -854,10 +912,12 @@ | ||
| 854 | 912 | { |
| 855 | 913 | "project": project, |
| 856 | 914 | "fossil_repo": fossil_repo, |
| 857 | 915 | "posts": merged, |
| 858 | 916 | "has_write": has_write, |
| 917 | + "search": search, | |
| 918 | + "pagination": pagination, | |
| 859 | 919 | "active_tab": "forum", |
| 860 | 920 | }, |
| 861 | 921 | ) |
| 862 | 922 | |
| 863 | 923 | |
| @@ -1026,17 +1086,26 @@ | ||
| 1026 | 1086 | |
| 1027 | 1087 | from fossil.webhooks import Webhook |
| 1028 | 1088 | |
| 1029 | 1089 | webhooks = Webhook.objects.filter(repository=fossil_repo) |
| 1030 | 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 | + | |
| 1031 | 1098 | return render( |
| 1032 | 1099 | request, |
| 1033 | 1100 | "fossil/webhook_list.html", |
| 1034 | 1101 | { |
| 1035 | 1102 | "project": project, |
| 1036 | 1103 | "fossil_repo": fossil_repo, |
| 1037 | - "webhooks": webhooks, | |
| 1104 | + "webhooks": page_obj, | |
| 1105 | + "page_obj": page_obj, | |
| 1106 | + "search": search, | |
| 1038 | 1107 | "active_tab": "settings", |
| 1039 | 1108 | }, |
| 1040 | 1109 | ) |
| 1041 | 1110 | |
| 1042 | 1111 | |
| @@ -1888,16 +1957,30 @@ | ||
| 1888 | 1957 | project, fossil_repo, reader = _get_repo_and_reader(slug, request) |
| 1889 | 1958 | |
| 1890 | 1959 | with reader: |
| 1891 | 1960 | notes = reader.get_technotes() |
| 1892 | 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 | + | |
| 1893 | 1969 | has_write = can_write_project(request.user, project) |
| 1894 | 1970 | |
| 1895 | 1971 | return render( |
| 1896 | 1972 | request, |
| 1897 | 1973 | "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 | + }, | |
| 1899 | 1982 | ) |
| 1900 | 1983 | |
| 1901 | 1984 | |
| 1902 | 1985 | @login_required |
| 1903 | 1986 | def technote_create(request, slug): |
| @@ -2010,19 +2093,28 @@ | ||
| 2010 | 2093 | project, fossil_repo, reader = _get_repo_and_reader(slug, request) |
| 2011 | 2094 | |
| 2012 | 2095 | with reader: |
| 2013 | 2096 | files = reader.get_unversioned_files() |
| 2014 | 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 | + | |
| 2015 | 2105 | has_admin = can_admin_project(request.user, project) |
| 2016 | 2106 | |
| 2017 | 2107 | return render( |
| 2018 | 2108 | request, |
| 2019 | 2109 | "fossil/unversioned_list.html", |
| 2020 | 2110 | { |
| 2021 | 2111 | "project": project, |
| 2022 | 2112 | "files": files, |
| 2023 | 2113 | "has_admin": has_admin, |
| 2114 | + "search": search, | |
| 2115 | + "pagination": pagination, | |
| 2024 | 2116 | "active_tab": "files", |
| 2025 | 2117 | }, |
| 2026 | 2118 | ) |
| 2027 | 2119 | |
| 2028 | 2120 | |
| @@ -2331,17 +2423,26 @@ | ||
| 2331 | 2423 | project, fossil_repo, reader = _get_repo_and_reader(slug, request) |
| 2332 | 2424 | |
| 2333 | 2425 | with reader: |
| 2334 | 2426 | branches = reader.get_branches() |
| 2335 | 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 | + | |
| 2336 | 2435 | return render( |
| 2337 | 2436 | request, |
| 2338 | 2437 | "fossil/branch_list.html", |
| 2339 | 2438 | { |
| 2340 | 2439 | "project": project, |
| 2341 | 2440 | "fossil_repo": fossil_repo, |
| 2342 | 2441 | "branches": branches, |
| 2442 | + "search": search, | |
| 2443 | + "pagination": pagination, | |
| 2343 | 2444 | "active_tab": "code", |
| 2344 | 2445 | }, |
| 2345 | 2446 | ) |
| 2346 | 2447 | |
| 2347 | 2448 | |
| @@ -2352,14 +2453,27 @@ | ||
| 2352 | 2453 | project, fossil_repo, reader = _get_repo_and_reader(slug, request) |
| 2353 | 2454 | |
| 2354 | 2455 | with reader: |
| 2355 | 2456 | tags = reader.get_tags() |
| 2356 | 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 | + | |
| 2357 | 2465 | return render( |
| 2358 | 2466 | request, |
| 2359 | 2467 | "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 | + }, | |
| 2361 | 2475 | ) |
| 2362 | 2476 | |
| 2363 | 2477 | |
| 2364 | 2478 | # --- Raw File Download --- |
| 2365 | 2479 | |
| @@ -2793,18 +2907,28 @@ | ||
| 2793 | 2907 | |
| 2794 | 2908 | has_write = can_write_project(request.user, project) |
| 2795 | 2909 | if not has_write: |
| 2796 | 2910 | releases = releases.filter(is_draft=False) |
| 2797 | 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 | + | |
| 2798 | 2920 | return render( |
| 2799 | 2921 | request, |
| 2800 | 2922 | "fossil/release_list.html", |
| 2801 | 2923 | { |
| 2802 | 2924 | "project": project, |
| 2803 | 2925 | "fossil_repo": fossil_repo, |
| 2804 | - "releases": releases, | |
| 2926 | + "releases": page_obj, | |
| 2927 | + "page_obj": page_obj, | |
| 2805 | 2928 | "has_write": has_write, |
| 2929 | + "search": search, | |
| 2806 | 2930 | "active_tab": "releases", |
| 2807 | 2931 | }, |
| 2808 | 2932 | ) |
| 2809 | 2933 | |
| 2810 | 2934 | |
| @@ -3197,17 +3321,26 @@ | ||
| 3197 | 3321 | |
| 3198 | 3322 | from fossil.api_tokens import APIToken |
| 3199 | 3323 | |
| 3200 | 3324 | tokens = APIToken.objects.filter(repository=fossil_repo) |
| 3201 | 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 | + | |
| 3202 | 3333 | return render( |
| 3203 | 3334 | request, |
| 3204 | 3335 | "fossil/api_token_list.html", |
| 3205 | 3336 | { |
| 3206 | 3337 | "project": project, |
| 3207 | 3338 | "fossil_repo": fossil_repo, |
| 3208 | - "tokens": tokens, | |
| 3339 | + "tokens": page_obj, | |
| 3340 | + "page_obj": page_obj, | |
| 3341 | + "search": search, | |
| 3209 | 3342 | "active_tab": "settings", |
| 3210 | 3343 | }, |
| 3211 | 3344 | ) |
| 3212 | 3345 | |
| 3213 | 3346 | |
| @@ -3284,17 +3417,26 @@ | ||
| 3284 | 3417 | |
| 3285 | 3418 | from fossil.branch_protection import BranchProtection |
| 3286 | 3419 | |
| 3287 | 3420 | rules = BranchProtection.objects.filter(repository=fossil_repo) |
| 3288 | 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 | + | |
| 3289 | 3429 | return render( |
| 3290 | 3430 | request, |
| 3291 | 3431 | "fossil/branch_protection_list.html", |
| 3292 | 3432 | { |
| 3293 | 3433 | "project": project, |
| 3294 | 3434 | "fossil_repo": fossil_repo, |
| 3295 | - "rules": rules, | |
| 3435 | + "rules": page_obj, | |
| 3436 | + "page_obj": page_obj, | |
| 3437 | + "search": search, | |
| 3296 | 3438 | "active_tab": "settings", |
| 3297 | 3439 | }, |
| 3298 | 3440 | ) |
| 3299 | 3441 | |
| 3300 | 3442 | |
| @@ -3421,17 +3563,27 @@ | ||
| 3421 | 3563 | |
| 3422 | 3564 | from fossil.ticket_fields import TicketFieldDefinition |
| 3423 | 3565 | |
| 3424 | 3566 | fields = TicketFieldDefinition.objects.filter(repository=fossil_repo) |
| 3425 | 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 | + | |
| 3426 | 3576 | return render( |
| 3427 | 3577 | request, |
| 3428 | 3578 | "fossil/ticket_fields_list.html", |
| 3429 | 3579 | { |
| 3430 | 3580 | "project": project, |
| 3431 | 3581 | "fossil_repo": fossil_repo, |
| 3432 | - "fields": fields, | |
| 3582 | + "fields": page_obj, | |
| 3583 | + "page_obj": page_obj, | |
| 3584 | + "search": search, | |
| 3433 | 3585 | "active_tab": "settings", |
| 3434 | 3586 | }, |
| 3435 | 3587 | ) |
| 3436 | 3588 | |
| 3437 | 3589 | |
| @@ -3563,21 +3715,32 @@ | ||
| 3563 | 3715 | project, fossil_repo = _get_project_and_repo(slug, request, "read") |
| 3564 | 3716 | |
| 3565 | 3717 | from fossil.ticket_reports import TicketReport |
| 3566 | 3718 | |
| 3567 | 3719 | 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: | |
| 3569 | 3722 | reports = reports.filter(is_public=True) |
| 3570 | 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 | + | |
| 3571 | 3732 | return render( |
| 3572 | 3733 | request, |
| 3573 | 3734 | "fossil/ticket_reports_list.html", |
| 3574 | 3735 | { |
| 3575 | 3736 | "project": project, |
| 3576 | 3737 | "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, | |
| 3579 | 3742 | "active_tab": "tickets", |
| 3580 | 3743 | }, |
| 3581 | 3744 | ) |
| 3582 | 3745 | |
| 3583 | 3746 | |
| 3584 | 3747 |
| --- 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 |
+38
-6
| --- organization/views.py | ||
| +++ organization/views.py | ||
| @@ -1,8 +1,9 @@ | ||
| 1 | 1 | from django.contrib import messages |
| 2 | 2 | from django.contrib.auth.decorators import login_required |
| 3 | 3 | from django.contrib.auth.models import User |
| 4 | +from django.core.paginator import Paginator | |
| 4 | 5 | from django.db import models |
| 5 | 6 | from django.http import HttpResponse |
| 6 | 7 | from django.shortcuts import get_object_or_404, redirect, render |
| 7 | 8 | |
| 8 | 9 | from core.permissions import P |
| @@ -63,14 +64,19 @@ | ||
| 63 | 64 | |
| 64 | 65 | search = request.GET.get("search", "").strip() |
| 65 | 66 | if search: |
| 66 | 67 | members = members.filter(member__username__icontains=search) |
| 67 | 68 | |
| 69 | + paginator = Paginator(members, 25) | |
| 70 | + page_obj = paginator.get_page(request.GET.get("page", 1)) | |
| 71 | + | |
| 68 | 72 | 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 | + ) | |
| 70 | 76 | |
| 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}) | |
| 72 | 78 | |
| 73 | 79 | |
| 74 | 80 | @login_required |
| 75 | 81 | def member_add(request): |
| 76 | 82 | P.ORGANIZATION_MEMBER_ADD.check(request.user) |
| @@ -118,14 +124,17 @@ | ||
| 118 | 124 | |
| 119 | 125 | search = request.GET.get("search", "").strip() |
| 120 | 126 | if search: |
| 121 | 127 | teams = teams.filter(name__icontains=search) |
| 122 | 128 | |
| 129 | + paginator = Paginator(teams, 25) | |
| 130 | + page_obj = paginator.get_page(request.GET.get("page", 1)) | |
| 131 | + | |
| 123 | 132 | 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}) | |
| 125 | 134 | |
| 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}) | |
| 127 | 136 | |
| 128 | 137 | |
| 129 | 138 | @login_required |
| 130 | 139 | def team_create(request): |
| 131 | 140 | P.TEAM_ADD.check(request.user) |
| @@ -389,10 +398,12 @@ | ||
| 389 | 398 | |
| 390 | 399 | |
| 391 | 400 | @login_required |
| 392 | 401 | def audit_log(request): |
| 393 | 402 | """Unified audit log across all tracked models. Requires superuser or org admin.""" |
| 403 | + import math | |
| 404 | + | |
| 394 | 405 | if not request.user.is_superuser: |
| 395 | 406 | P.ORGANIZATION_CHANGE.check(request.user) |
| 396 | 407 | |
| 397 | 408 | from fossil.models import FossilRepository |
| 398 | 409 | from projects.models import Project |
| @@ -409,11 +420,11 @@ | ||
| 409 | 420 | |
| 410 | 421 | for label, model in trackable_models: |
| 411 | 422 | if model_filter and label.lower() != model_filter.lower(): |
| 412 | 423 | continue |
| 413 | 424 | 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] | |
| 415 | 426 | for h in qs: |
| 416 | 427 | entries.append( |
| 417 | 428 | { |
| 418 | 429 | "date": h.history_date, |
| 419 | 430 | "user": h.history_user, |
| @@ -423,11 +434,31 @@ | ||
| 423 | 434 | "object_id": h.pk, |
| 424 | 435 | } |
| 425 | 436 | ) |
| 426 | 437 | |
| 427 | 438 | 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 | + } | |
| 429 | 460 | |
| 430 | 461 | available_models = [label for label, _ in trackable_models] |
| 431 | 462 | |
| 432 | 463 | return render( |
| 433 | 464 | request, |
| @@ -434,10 +465,11 @@ | ||
| 434 | 465 | "organization/audit_log.html", |
| 435 | 466 | { |
| 436 | 467 | "entries": entries, |
| 437 | 468 | "model_filter": model_filter, |
| 438 | 469 | "available_models": available_models, |
| 470 | + "pagination": pagination, | |
| 439 | 471 | }, |
| 440 | 472 | ) |
| 441 | 473 | |
| 442 | 474 | |
| 443 | 475 | @login_required |
| 444 | 476 |
| --- 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 @@ | ||
| 1 | 1 | import markdown |
| 2 | 2 | from django.contrib import messages |
| 3 | 3 | from django.contrib.auth.decorators import login_required |
| 4 | +from django.core.paginator import Paginator | |
| 4 | 5 | from django.http import HttpResponse |
| 5 | 6 | from django.shortcuts import get_object_or_404, redirect, render |
| 6 | 7 | from django.utils.safestring import mark_safe |
| 7 | 8 | |
| 8 | 9 | from core.permissions import P |
| @@ -23,14 +24,17 @@ | ||
| 23 | 24 | |
| 24 | 25 | search = request.GET.get("search", "").strip() |
| 25 | 26 | if search: |
| 26 | 27 | pages = pages.filter(name__icontains=search) |
| 27 | 28 | |
| 29 | + paginator = Paginator(pages, 25) | |
| 30 | + page_obj = paginator.get_page(request.GET.get("page", 1)) | |
| 31 | + | |
| 28 | 32 | 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}) | |
| 30 | 34 | |
| 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}) | |
| 32 | 36 | |
| 33 | 37 | |
| 34 | 38 | @login_required |
| 35 | 39 | def page_create(request): |
| 36 | 40 | P.PAGE_ADD.check(request.user) |
| 37 | 41 |
| --- 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 |
+15
-4
| --- projects/views.py | ||
| +++ projects/views.py | ||
| @@ -1,7 +1,8 @@ | ||
| 1 | 1 | from django.contrib import messages |
| 2 | 2 | from django.contrib.auth.decorators import login_required |
| 3 | +from django.core.paginator import Paginator | |
| 3 | 4 | from django.db.models import Count |
| 4 | 5 | from django.http import HttpResponse |
| 5 | 6 | from django.shortcuts import get_object_or_404, redirect, render |
| 6 | 7 | |
| 7 | 8 | from core.permissions import P |
| @@ -19,14 +20,17 @@ | ||
| 19 | 20 | |
| 20 | 21 | search = request.GET.get("search", "").strip() |
| 21 | 22 | if search: |
| 22 | 23 | projects = projects.filter(name__icontains=search) |
| 23 | 24 | |
| 25 | + paginator = Paginator(projects, 25) | |
| 26 | + page_obj = paginator.get_page(request.GET.get("page", 1)) | |
| 27 | + | |
| 24 | 28 | 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}) | |
| 26 | 30 | |
| 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}) | |
| 28 | 32 | |
| 29 | 33 | |
| 30 | 34 | @login_required |
| 31 | 35 | def project_create(request): |
| 32 | 36 | P.PROJECT_ADD.check(request.user) |
| @@ -250,14 +254,21 @@ | ||
| 250 | 254 | @login_required |
| 251 | 255 | def group_list(request): |
| 252 | 256 | P.PROJECT_GROUP_VIEW.check(request.user) |
| 253 | 257 | groups = ProjectGroup.objects.all().prefetch_related("projects") |
| 254 | 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 | + | |
| 255 | 266 | 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}) | |
| 257 | 268 | |
| 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}) | |
| 259 | 270 | |
| 260 | 271 | |
| 261 | 272 | @login_required |
| 262 | 273 | def group_create(request): |
| 263 | 274 | P.PROJECT_GROUP_ADD.check(request.user) |
| 264 | 275 |
| --- 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 |
+22
-5
| --- templates/fossil/api_token_list.html | ||
| +++ templates/fossil/api_token_list.html | ||
| @@ -8,16 +8,29 @@ | ||
| 8 | 8 | <div class="flex items-center justify-between mb-6"> |
| 9 | 9 | <div> |
| 10 | 10 | <h2 class="text-lg font-semibold text-gray-200">API Tokens</h2> |
| 11 | 11 | <p class="text-sm text-gray-500 mt-1">Tokens for CI/CD systems and automation. Used with Bearer authentication.</p> |
| 12 | 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> | |
| 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> | |
| 17 | 29 | </div> |
| 18 | 30 | |
| 31 | +<div id="token-content"> | |
| 19 | 32 | {% if tokens %} |
| 20 | 33 | <div class="space-y-3"> |
| 21 | 34 | {% for token in tokens %} |
| 22 | 35 | <div class="rounded-lg bg-gray-800 border border-gray-700"> |
| 23 | 36 | <div class="px-5 py-4"> |
| @@ -54,17 +67,21 @@ | ||
| 54 | 67 | </div> |
| 55 | 68 | {% endfor %} |
| 56 | 69 | </div> |
| 57 | 70 | {% else %} |
| 58 | 71 | <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 %} | |
| 60 | 74 | <a href="{% url 'fossil:api_token_create' slug=project.slug %}" |
| 61 | 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"> |
| 62 | 76 | Generate the first token |
| 63 | 77 | </a> |
| 78 | + {% endif %} | |
| 64 | 79 | </div> |
| 65 | 80 | {% endif %} |
| 81 | +{% include "includes/_pagination.html" %} | |
| 82 | +</div> | |
| 66 | 83 | |
| 67 | 84 | <div class="mt-6 rounded-lg bg-gray-800/50 border border-gray-700 p-4"> |
| 68 | 85 | <h3 class="text-sm font-semibold text-gray-300 mb-2">Usage</h3> |
| 69 | 86 | <p class="text-xs text-gray-500 mb-2">Use the token with the CI Status API:</p> |
| 70 | 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 \ |
| 71 | 88 |
| --- 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 |
+19
-1
| --- templates/fossil/branch_list.html | ||
| +++ templates/fossil/branch_list.html | ||
| @@ -3,10 +3,26 @@ | ||
| 3 | 3 | |
| 4 | 4 | {% block content %} |
| 5 | 5 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 6 | 6 | {% include "fossil/_project_nav.html" %} |
| 7 | 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"> | |
| 8 | 24 | <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm"> |
| 9 | 25 | <table class="min-w-full divide-y divide-gray-700"> |
| 10 | 26 | <thead class="bg-gray-900"> |
| 11 | 27 | <tr> |
| 12 | 28 | <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Branch</th> |
| @@ -35,12 +51,14 @@ | ||
| 35 | 51 | <td class="px-4 py-3 text-sm text-gray-400 text-right">{{ branch.checkin_count }}</td> |
| 36 | 52 | <td class="px-4 py-3 text-sm text-gray-500 text-right">{{ branch.last_checkin|timesince }} ago</td> |
| 37 | 53 | </tr> |
| 38 | 54 | {% empty %} |
| 39 | 55 | <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> | |
| 41 | 57 | </tr> |
| 42 | 58 | {% endfor %} |
| 43 | 59 | </tbody> |
| 44 | 60 | </table> |
| 61 | +</div> | |
| 62 | +{% include "includes/_pagination_manual.html" %} | |
| 45 | 63 | </div> |
| 46 | 64 | {% endblock %} |
| 47 | 65 |
| --- 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 @@ | ||
| 8 | 8 | <div class="flex items-center justify-between mb-6"> |
| 9 | 9 | <div> |
| 10 | 10 | <h2 class="text-lg font-semibold text-gray-200">Branch Protection Rules</h2> |
| 11 | 11 | <p class="text-sm text-gray-500 mt-1">Protect branches from unreviewed changes and require CI checks to pass.</p> |
| 12 | 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> | |
| 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> | |
| 17 | 29 | </div> |
| 18 | 30 | |
| 31 | +<div id="protection-content"> | |
| 19 | 32 | {% if rules %} |
| 20 | 33 | <div class="space-y-3"> |
| 21 | 34 | {% for rule in rules %} |
| 22 | 35 | <div class="rounded-lg bg-gray-800 border border-gray-700"> |
| 23 | 36 | <div class="px-5 py-4"> |
| @@ -53,17 +66,21 @@ | ||
| 53 | 66 | </div> |
| 54 | 67 | {% endfor %} |
| 55 | 68 | </div> |
| 56 | 69 | {% else %} |
| 57 | 70 | <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 %} | |
| 59 | 73 | <a href="{% url 'fossil:branch_protection_create' slug=project.slug %}" |
| 60 | 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"> |
| 61 | 75 | Add the first rule |
| 62 | 76 | </a> |
| 77 | + {% endif %} | |
| 63 | 78 | </div> |
| 64 | 79 | {% endif %} |
| 80 | +{% include "includes/_pagination.html" %} | |
| 81 | +</div> | |
| 65 | 82 | |
| 66 | 83 | <div class="mt-6 rounded-md bg-yellow-900/20 border border-yellow-800/50 px-4 py-3"> |
| 67 | 84 | <p class="text-xs text-yellow-300">Branch protection rules are currently advisory. Push enforcement via Fossil hooks is not yet implemented.</p> |
| 68 | 85 | </div> |
| 69 | 86 | {% endblock %} |
| 70 | 87 |
| --- 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 |
+23
-8
| --- templates/fossil/forum_list.html | ||
| +++ templates/fossil/forum_list.html | ||
| @@ -5,18 +5,31 @@ | ||
| 5 | 5 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 6 | 6 | {% include "fossil/_project_nav.html" %} |
| 7 | 7 | |
| 8 | 8 | <div class="flex items-center justify-between mb-6"> |
| 9 | 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 %} | |
| 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> | |
| 16 | 28 | </div> |
| 17 | 29 | |
| 30 | +<div id="forum-content"> | |
| 18 | 31 | <div class="space-y-3"> |
| 19 | 32 | {% for post in posts %} |
| 20 | 33 | <div class="rounded-lg bg-gray-800 border border-gray-700 hover:border-gray-600 transition-colors"> |
| 21 | 34 | <div class="px-5 py-4"> |
| 22 | 35 | {% if post.source == "django" %} |
| @@ -46,16 +59,18 @@ | ||
| 46 | 59 | </div> |
| 47 | 60 | </div> |
| 48 | 61 | </div> |
| 49 | 62 | {% empty %} |
| 50 | 63 | <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 %} | |
| 53 | 66 | <a href="{% url 'fossil:forum_create' slug=project.slug %}" |
| 54 | 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"> |
| 55 | 68 | Start the first thread |
| 56 | 69 | </a> |
| 57 | 70 | {% endif %} |
| 58 | 71 | </div> |
| 59 | 72 | {% endfor %} |
| 73 | +</div> | |
| 74 | +{% include "includes/_pagination_manual.html" %} | |
| 60 | 75 | </div> |
| 61 | 76 | {% endblock %} |
| 62 | 77 |
| --- 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 |
+23
-8
| --- templates/fossil/release_list.html | ||
| +++ templates/fossil/release_list.html | ||
| @@ -6,18 +6,31 @@ | ||
| 6 | 6 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 7 | 7 | {% include "fossil/_project_nav.html" %} |
| 8 | 8 | |
| 9 | 9 | <div class="flex items-center justify-between mb-6"> |
| 10 | 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 %} | |
| 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> | |
| 17 | 29 | </div> |
| 18 | 30 | |
| 31 | +<div id="release-content"> | |
| 19 | 32 | {% if releases %} |
| 20 | 33 | <div class="space-y-4"> |
| 21 | 34 | {% for release in releases %} |
| 22 | 35 | <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700"> |
| 23 | 36 | <div class="px-6 py-4"> |
| @@ -52,15 +65,17 @@ | ||
| 52 | 65 | </div> |
| 53 | 66 | {% endfor %} |
| 54 | 67 | </div> |
| 55 | 68 | {% else %} |
| 56 | 69 | <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 %} | |
| 59 | 72 | <a href="{% url 'fossil:release_create' slug=project.slug %}" |
| 60 | 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"> |
| 61 | 74 | Create the first release |
| 62 | 75 | </a> |
| 63 | 76 | {% endif %} |
| 64 | 77 | </div> |
| 65 | 78 | {% endif %} |
| 79 | +{% include "includes/_pagination.html" %} | |
| 80 | +</div> | |
| 66 | 81 | {% endblock %} |
| 67 | 82 |
| --- 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 |
+19
-1
| --- templates/fossil/tag_list.html | ||
| +++ templates/fossil/tag_list.html | ||
| @@ -3,10 +3,26 @@ | ||
| 3 | 3 | |
| 4 | 4 | {% block content %} |
| 5 | 5 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 6 | 6 | {% include "fossil/_project_nav.html" %} |
| 7 | 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"> | |
| 8 | 24 | <div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800 shadow-sm"> |
| 9 | 25 | <table class="min-w-full divide-y divide-gray-700"> |
| 10 | 26 | <thead class="bg-gray-900"> |
| 11 | 27 | <tr> |
| 12 | 28 | <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-400">Tag</th> |
| @@ -29,12 +45,14 @@ | ||
| 29 | 45 | </td> |
| 30 | 46 | <td class="px-4 py-3 text-sm text-gray-500 text-right">{{ tag.timestamp|date:"Y-m-d" }}</td> |
| 31 | 47 | </tr> |
| 32 | 48 | {% empty %} |
| 33 | 49 | <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> | |
| 35 | 51 | </tr> |
| 36 | 52 | {% endfor %} |
| 37 | 53 | </tbody> |
| 38 | 54 | </table> |
| 55 | +</div> | |
| 56 | +{% include "includes/_pagination_manual.html" %} | |
| 39 | 57 | </div> |
| 40 | 58 | {% endblock %} |
| 41 | 59 |
| --- 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 |
+23
-8
| --- templates/fossil/technote_list.html | ||
| +++ templates/fossil/technote_list.html | ||
| @@ -5,18 +5,31 @@ | ||
| 5 | 5 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 6 | 6 | {% include "fossil/_project_nav.html" %} |
| 7 | 7 | |
| 8 | 8 | <div class="flex items-center justify-between mb-6"> |
| 9 | 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 %} | |
| 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> | |
| 16 | 28 | </div> |
| 17 | 29 | |
| 30 | +<div id="technote-content"> | |
| 18 | 31 | {% if notes %} |
| 19 | 32 | <div class="space-y-3"> |
| 20 | 33 | {% for note in notes %} |
| 21 | 34 | <a href="{% url 'fossil:technote_detail' slug=project.slug technote_id=note.uuid %}" |
| 22 | 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 @@ | ||
| 33 | 46 | </a> |
| 34 | 47 | {% endfor %} |
| 35 | 48 | </div> |
| 36 | 49 | {% else %} |
| 37 | 50 | <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 %} | |
| 40 | 53 | <a href="{% url 'fossil:technote_create' slug=project.slug %}" |
| 41 | 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"> |
| 42 | 55 | Create the first technote |
| 43 | 56 | </a> |
| 44 | 57 | {% endif %} |
| 45 | 58 | </div> |
| 46 | 59 | {% endif %} |
| 60 | +{% include "includes/_pagination_manual.html" %} | |
| 61 | +</div> | |
| 47 | 62 | {% endblock %} |
| 48 | 63 |
| --- 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 @@ | ||
| 5 | 5 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 6 | 6 | {% include "fossil/_project_nav.html" %} |
| 7 | 7 | |
| 8 | 8 | <div class="flex items-center justify-between mb-6"> |
| 9 | 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> | |
| 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> | |
| 14 | 26 | </div> |
| 15 | 27 | |
| 28 | +<div id="fields-content"> | |
| 16 | 29 | {% if fields %} |
| 17 | 30 | <div class="space-y-3"> |
| 18 | 31 | {% for field in fields %} |
| 19 | 32 | <div class="rounded-lg bg-gray-800 border border-gray-700"> |
| 20 | 33 | <div class="px-5 py-4"> |
| @@ -49,14 +62,18 @@ | ||
| 49 | 62 | </div> |
| 50 | 63 | {% endfor %} |
| 51 | 64 | </div> |
| 52 | 65 | {% else %} |
| 53 | 66 | <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 %} | |
| 55 | 69 | <p class="text-xs text-gray-600 mt-1">Custom fields extend the Fossil ticket schema with project-specific data.</p> |
| 56 | 70 | <a href="{% url 'fossil:ticket_field_create' slug=project.slug %}" |
| 57 | 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"> |
| 58 | 72 | Add the first field |
| 59 | 73 | </a> |
| 74 | + {% endif %} | |
| 60 | 75 | </div> |
| 61 | 76 | {% endif %} |
| 77 | +{% include "includes/_pagination.html" %} | |
| 78 | +</div> | |
| 62 | 79 | {% endblock %} |
| 63 | 80 |
| --- 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 @@ | ||
| 5 | 5 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 6 | 6 | {% include "fossil/_project_nav.html" %} |
| 7 | 7 | |
| 8 | 8 | <div class="flex items-center justify-between mb-6"> |
| 9 | 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 %} | |
| 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> | |
| 16 | 28 | </div> |
| 17 | 29 | |
| 30 | +<div id="reports-content"> | |
| 18 | 31 | {% if reports %} |
| 19 | 32 | <div class="space-y-3"> |
| 20 | 33 | {% for report in reports %} |
| 21 | 34 | <div class="rounded-lg bg-gray-800 border border-gray-700"> |
| 22 | 35 | <div class="px-5 py-4"> |
| @@ -46,16 +59,20 @@ | ||
| 46 | 59 | </div> |
| 47 | 60 | {% endfor %} |
| 48 | 61 | </div> |
| 49 | 62 | {% else %} |
| 50 | 63 | <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 %} | |
| 52 | 66 | <p class="text-xs text-gray-600 mt-1">Reports let you run custom SQL queries against the Fossil ticket database.</p> |
| 53 | 67 | {% if can_admin %} |
| 54 | 68 | <a href="{% url 'fossil:ticket_report_create' slug=project.slug %}" |
| 55 | 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"> |
| 56 | 70 | Create the first report |
| 57 | 71 | </a> |
| 58 | 72 | {% endif %} |
| 73 | + {% endif %} | |
| 59 | 74 | </div> |
| 60 | 75 | {% endif %} |
| 76 | +{% include "includes/_pagination.html" %} | |
| 77 | +</div> | |
| 61 | 78 | {% endblock %} |
| 62 | 79 |
| --- 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 @@ | ||
| 5 | 5 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 6 | 6 | {% include "fossil/_project_nav.html" %} |
| 7 | 7 | |
| 8 | 8 | <div class="flex items-center justify-between mb-6"> |
| 9 | 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> | |
| 10 | 22 | </div> |
| 11 | 23 | |
| 24 | +<div id="unversioned-content"> | |
| 12 | 25 | {% if files %} |
| 13 | 26 | <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700"> |
| 14 | 27 | <table class="min-w-full divide-y divide-gray-700"> |
| 15 | 28 | <thead class="bg-gray-900/50"> |
| 16 | 29 | <tr> |
| @@ -37,13 +50,15 @@ | ||
| 37 | 50 | </tbody> |
| 38 | 51 | </table> |
| 39 | 52 | </div> |
| 40 | 53 | {% else %} |
| 41 | 54 | <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> | |
| 43 | 56 | </div> |
| 44 | 57 | {% endif %} |
| 58 | +{% include "includes/_pagination_manual.html" %} | |
| 59 | +</div> | |
| 45 | 60 | |
| 46 | 61 | {% if has_admin %} |
| 47 | 62 | <div class="mt-6"> |
| 48 | 63 | <form method="post" |
| 49 | 64 | action="{% url 'fossil:unversioned_upload' slug=project.slug %}" |
| 50 | 65 |
| --- 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 |
+22
-5
| --- templates/fossil/webhook_list.html | ||
| +++ templates/fossil/webhook_list.html | ||
| @@ -5,16 +5,29 @@ | ||
| 5 | 5 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 6 | 6 | {% include "fossil/_project_nav.html" %} |
| 7 | 7 | |
| 8 | 8 | <div class="flex items-center justify-between mb-6"> |
| 9 | 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> | |
| 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> | |
| 14 | 26 | </div> |
| 15 | 27 | |
| 28 | +<div id="webhook-content"> | |
| 16 | 29 | {% if webhooks %} |
| 17 | 30 | <div class="space-y-3"> |
| 18 | 31 | {% for webhook in webhooks %} |
| 19 | 32 | <div class="rounded-lg bg-gray-800 border border-gray-700"> |
| 20 | 33 | <div class="px-5 py-4"> |
| @@ -54,13 +67,17 @@ | ||
| 54 | 67 | </div> |
| 55 | 68 | {% endfor %} |
| 56 | 69 | </div> |
| 57 | 70 | {% else %} |
| 58 | 71 | <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 %} | |
| 60 | 74 | <a href="{% url 'fossil:webhook_create' slug=project.slug %}" |
| 61 | 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"> |
| 62 | 76 | Add the first webhook |
| 63 | 77 | </a> |
| 78 | + {% endif %} | |
| 64 | 79 | </div> |
| 65 | 80 | {% endif %} |
| 81 | +{% include "includes/_pagination.html" %} | |
| 82 | +</div> | |
| 66 | 83 | {% endblock %} |
| 67 | 84 |
| --- 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 |
+18
-4
| --- templates/fossil/wiki_list.html | ||
| +++ templates/fossil/wiki_list.html | ||
| @@ -4,20 +4,32 @@ | ||
| 4 | 4 | {% block content %} |
| 5 | 5 | <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1> |
| 6 | 6 | {% include "fossil/_project_nav.html" %} |
| 7 | 7 | |
| 8 | 8 | <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> | |
| 10 | 21 | {% if perms.projects.change_project %} |
| 11 | 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> |
| 12 | 23 | {% endif %} |
| 13 | 24 | </div> |
| 14 | 25 | |
| 26 | +<div id="wiki-content"> | |
| 15 | 27 | <div class="flex gap-6"> |
| 16 | 28 | <!-- Main content: home page or page index --> |
| 17 | 29 | <div class="flex-1 min-w-0"> |
| 18 | - {% if home_page %} | |
| 30 | + {% if home_page and not search %} | |
| 19 | 31 | <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700"> |
| 20 | 32 | <div class="px-6 py-4 border-b border-gray-700"> |
| 21 | 33 | <h2 class="text-lg font-semibold text-gray-100">{{ home_page.name }}</h2> |
| 22 | 34 | </div> |
| 23 | 35 | <div class="px-6 py-6"> |
| @@ -24,13 +36,13 @@ | ||
| 24 | 36 | <div class="prose prose-invert prose-gray max-w-none"> |
| 25 | 37 | {{ home_content_html }} |
| 26 | 38 | </div> |
| 27 | 39 | </div> |
| 28 | 40 | </div> |
| 29 | - {% else %} | |
| 41 | + {% elif not pages %} | |
| 30 | 42 | <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> | |
| 32 | 44 | </div> |
| 33 | 45 | {% endif %} |
| 34 | 46 | </div> |
| 35 | 47 | |
| 36 | 48 | <!-- Right sidebar: all pages --> |
| @@ -48,7 +60,9 @@ | ||
| 48 | 60 | {% if not pages %} |
| 49 | 61 | <p class="text-xs text-gray-600 px-3">No pages.</p> |
| 50 | 62 | {% endif %} |
| 51 | 63 | </div> |
| 52 | 64 | </aside> |
| 65 | +</div> | |
| 66 | +{% include "includes/_pagination_manual.html" %} | |
| 53 | 67 | </div> |
| 54 | 68 | {% endblock %} |
| 55 | 69 | |
| 56 | 70 | ADDED templates/includes/_pagination.html |
| 57 | 71 | 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 @@ | ||
| 65 | 65 | </tr> |
| 66 | 66 | {% endfor %} |
| 67 | 67 | </tbody> |
| 68 | 68 | </table> |
| 69 | 69 | </div> |
| 70 | +{% with extra_params="&model="|add:model_filter %} | |
| 71 | +{% include "includes/_pagination_manual.html" %} | |
| 72 | +{% endwith %} | |
| 70 | 73 | {% endblock %} |
| 71 | 74 |
| --- 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 @@ | ||
| 36 | 36 | hx-swap="outerHTML" |
| 37 | 37 | hx-push-url="true" /> |
| 38 | 38 | </div> |
| 39 | 39 | |
| 40 | 40 | {% include "organization/partials/member_table.html" %} |
| 41 | +{% include "includes/_pagination.html" %} | |
| 41 | 42 | {% endblock %} |
| 42 | 43 |
| --- 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 @@ | ||
| 28 | 28 | hx-swap="outerHTML" |
| 29 | 29 | hx-push-url="true" /> |
| 30 | 30 | </div> |
| 31 | 31 | |
| 32 | 32 | {% include "organization/partials/team_table.html" %} |
| 33 | +{% include "includes/_pagination.html" %} | |
| 33 | 34 | {% endblock %} |
| 34 | 35 |
| --- 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 @@ | ||
| 24 | 24 | hx-swap="outerHTML" |
| 25 | 25 | hx-push-url="true" /> |
| 26 | 26 | </div> |
| 27 | 27 | |
| 28 | 28 | {% include "pages/partials/page_table.html" %} |
| 29 | +{% include "includes/_pagination.html" %} | |
| 29 | 30 | {% endblock %} |
| 30 | 31 |
| --- 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 @@ | ||
| 9 | 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 | 10 | New Group |
| 11 | 11 | </a> |
| 12 | 12 | {% endif %} |
| 13 | 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> | |
| 14 | 27 | |
| 15 | 28 | {% include "projects/partials/group_table.html" %} |
| 29 | +{% include "includes/_pagination.html" %} | |
| 16 | 30 | {% endblock %} |
| 17 | 31 |
| --- 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 @@ | ||
| 24 | 24 | hx-swap="outerHTML" |
| 25 | 25 | hx-push-url="true" /> |
| 26 | 26 | </div> |
| 27 | 27 | |
| 28 | 28 | {% include "projects/partials/project_table.html" %} |
| 29 | +{% include "includes/_pagination.html" %} | |
| 29 | 30 | {% endblock %} |
| 30 | 31 |
| --- 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 |