FossilRepo

P0-P1: DAG connectors at parent row, ticket edit/close/comment DAG graph (P0): - Connectors now drawn at the parent's row (where branch meets trunk) instead of only when parent is the immediate next row - Multiple connectors per row supported (multiple merges) - Precomputed connector map for all entries Ticket CRUD (P1): - Edit ticket: change title, status, type, severity, priority, resolution - Add comment: markdown textarea on ticket detail page - ticket_change() CLI method for fossil ticket change command - Edit button on ticket detail header - Comment form at bottom of ticket detail Groomed 16 backlog tickets with descriptions, acceptance criteria, priorities.

lmata 2026-04-07 01:51 UTC trunk
Commit 8c4ef0abc63a5bba2180c300a087d20532a3ca54e9da3331a67bf049ae0c7b06
--- fossil/cli.py
+++ fossil/cli.py
@@ -151,8 +151,17 @@
151151
def ticket_add(self, repo_path: Path, fields: dict) -> bool:
152152
"""Add a new ticket. Fields dict maps field names to values."""
153153
cmd = [self.binary, "ticket", "add", "-R", str(repo_path)]
154154
for key, value in fields.items():
155155
cmd.append(f"{key}")
156
+ cmd.append(f"{value}")
157
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
158
+ return result.returncode == 0
159
+
160
+ def ticket_change(self, repo_path: Path, uuid: str, fields: dict) -> bool:
161
+ """Update an existing ticket."""
162
+ cmd = [self.binary, "ticket", "change", uuid, "-R", str(repo_path)]
163
+ for key, value in fields.items():
164
+ cmd.append(f"{key}")
156165
cmd.append(f"{value}")
157166
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
158167
return result.returncode == 0
159168
--- fossil/cli.py
+++ fossil/cli.py
@@ -151,8 +151,17 @@
151 def ticket_add(self, repo_path: Path, fields: dict) -> bool:
152 """Add a new ticket. Fields dict maps field names to values."""
153 cmd = [self.binary, "ticket", "add", "-R", str(repo_path)]
154 for key, value in fields.items():
155 cmd.append(f"{key}")
 
 
 
 
 
 
 
 
 
156 cmd.append(f"{value}")
157 result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
158 return result.returncode == 0
159
--- fossil/cli.py
+++ fossil/cli.py
@@ -151,8 +151,17 @@
151 def ticket_add(self, repo_path: Path, fields: dict) -> bool:
152 """Add a new ticket. Fields dict maps field names to values."""
153 cmd = [self.binary, "ticket", "add", "-R", str(repo_path)]
154 for key, value in fields.items():
155 cmd.append(f"{key}")
156 cmd.append(f"{value}")
157 result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
158 return result.returncode == 0
159
160 def ticket_change(self, repo_path: Path, uuid: str, fields: dict) -> bool:
161 """Update an existing ticket."""
162 cmd = [self.binary, "ticket", "change", uuid, "-R", str(repo_path)]
163 for key, value in fields.items():
164 cmd.append(f"{key}")
165 cmd.append(f"{value}")
166 result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
167 return result.returncode == 0
168
--- fossil/urls.py
+++ fossil/urls.py
@@ -10,10 +10,12 @@
1010
path("code/file/<path:filepath>", views.code_file, name="code_file"),
1111
path("timeline/", views.timeline, name="timeline"),
1212
path("checkin/<str:checkin_uuid>/", views.checkin_detail, name="checkin_detail"),
1313
path("tickets/", views.ticket_list, name="tickets"),
1414
path("tickets/<str:ticket_uuid>/", views.ticket_detail, name="ticket_detail"),
15
+ path("tickets/<str:ticket_uuid>/edit/", views.ticket_edit, name="ticket_edit"),
16
+ path("tickets/<str:ticket_uuid>/comment/", views.ticket_comment, name="ticket_comment"),
1517
path("wiki/", views.wiki_list, name="wiki"),
1618
path("wiki/create/", views.wiki_create, name="wiki_create"),
1719
path("wiki/page/<path:page_name>", views.wiki_page, name="wiki_page"),
1820
path("wiki/edit/<path:page_name>", views.wiki_edit, name="wiki_edit"),
1921
path("tickets/create/", views.ticket_create, name="ticket_create"),
2022
--- fossil/urls.py
+++ fossil/urls.py
@@ -10,10 +10,12 @@
10 path("code/file/<path:filepath>", views.code_file, name="code_file"),
11 path("timeline/", views.timeline, name="timeline"),
12 path("checkin/<str:checkin_uuid>/", views.checkin_detail, name="checkin_detail"),
13 path("tickets/", views.ticket_list, name="tickets"),
14 path("tickets/<str:ticket_uuid>/", views.ticket_detail, name="ticket_detail"),
 
 
15 path("wiki/", views.wiki_list, name="wiki"),
16 path("wiki/create/", views.wiki_create, name="wiki_create"),
17 path("wiki/page/<path:page_name>", views.wiki_page, name="wiki_page"),
18 path("wiki/edit/<path:page_name>", views.wiki_edit, name="wiki_edit"),
19 path("tickets/create/", views.ticket_create, name="ticket_create"),
20
--- fossil/urls.py
+++ fossil/urls.py
@@ -10,10 +10,12 @@
10 path("code/file/<path:filepath>", views.code_file, name="code_file"),
11 path("timeline/", views.timeline, name="timeline"),
12 path("checkin/<str:checkin_uuid>/", views.checkin_detail, name="checkin_detail"),
13 path("tickets/", views.ticket_list, name="tickets"),
14 path("tickets/<str:ticket_uuid>/", views.ticket_detail, name="ticket_detail"),
15 path("tickets/<str:ticket_uuid>/edit/", views.ticket_edit, name="ticket_edit"),
16 path("tickets/<str:ticket_uuid>/comment/", views.ticket_comment, name="ticket_comment"),
17 path("wiki/", views.wiki_list, name="wiki"),
18 path("wiki/create/", views.wiki_create, name="wiki_create"),
19 path("wiki/page/<path:page_name>", views.wiki_page, name="wiki_page"),
20 path("wiki/edit/<path:page_name>", views.wiki_edit, name="wiki_edit"),
21 path("tickets/create/", views.ticket_create, name="ticket_create"),
22
+77 -15
--- fossil/views.py
+++ fossil/views.py
@@ -868,10 +868,67 @@
868868
869869
return redirect("fossil:tickets", slug=slug)
870870
871871
return render(request, "fossil/ticket_form.html", {"project": project, "active_tab": "tickets", "title": "New Ticket"})
872872
873
+
874
+@login_required
875
+def ticket_edit(request, slug, ticket_uuid):
876
+ P.PROJECT_CHANGE.check(request.user)
877
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
878
+
879
+ with reader:
880
+ ticket = reader.get_ticket_detail(ticket_uuid)
881
+ if not ticket:
882
+ raise Http404("Ticket not found")
883
+
884
+ if request.method == "POST":
885
+ from fossil.cli import FossilCLI
886
+
887
+ cli = FossilCLI()
888
+ fields = {}
889
+ for field in ["title", "status", "type", "severity", "priority", "resolution", "subsystem"]:
890
+ val = request.POST.get(field, "").strip()
891
+ if val:
892
+ fields[field] = val
893
+ if fields:
894
+ success = cli.ticket_change(fossil_repo.full_path, ticket.uuid, fields)
895
+ if success:
896
+ from django.contrib import messages
897
+
898
+ messages.success(request, f'Ticket "{ticket.title}" updated.')
899
+ from django.shortcuts import redirect
900
+
901
+ return redirect("fossil:ticket_detail", slug=slug, ticket_uuid=ticket.uuid)
902
+
903
+ return render(
904
+ request,
905
+ "fossil/ticket_edit.html",
906
+ {"project": project, "ticket": ticket, "active_tab": "tickets"},
907
+ )
908
+
909
+
910
+@login_required
911
+def ticket_comment(request, slug, ticket_uuid):
912
+ P.PROJECT_CHANGE.check(request.user)
913
+ project, fossil_repo, reader = _get_repo_and_reader(slug)
914
+
915
+ if request.method == "POST":
916
+ comment = request.POST.get("comment", "").strip()
917
+ if comment:
918
+ from fossil.cli import FossilCLI
919
+
920
+ cli = FossilCLI()
921
+ success = cli.ticket_change(fossil_repo.full_path, ticket_uuid, {"icomment": comment})
922
+ if success:
923
+ from django.contrib import messages
924
+
925
+ messages.success(request, "Comment added.")
926
+ from django.shortcuts import redirect
927
+
928
+ return redirect("fossil:ticket_detail", slug=slug, ticket_uuid=ticket_uuid)
929
+
873930
874931
# --- User Activity ---
875932
876933
877934
@login_required
@@ -1479,10 +1536,28 @@
14791536
parent_idx = rid_to_idx[entry.parent_rid]
14801537
if parent_idx > i:
14811538
rail = max(entry.rail, 0)
14821539
active_spans.append((rail, i, parent_idx))
14831540
1541
+ # Precompute connectors: for each row, collect all horizontal connections
1542
+ # A connector appears when a child on one rail connects to a parent on a different rail
1543
+ # We draw the connector at BOTH the child row (fork out) and on every row where
1544
+ # a branch line needs to cross from one rail to another
1545
+ row_connectors: dict[int, list[dict]] = {}
1546
+ for entry in entries:
1547
+ if entry.event_type != "ci" or entry.parent_rid not in rid_to_idx:
1548
+ continue
1549
+ parent_idx = rid_to_idx[entry.parent_rid]
1550
+ child_rail = max(entry.rail, 0)
1551
+ parent_rail = rid_to_rail.get(entry.parent_rid, 0)
1552
+ if child_rail != parent_rail:
1553
+ child_x = rail_offset + child_rail * rail_pitch
1554
+ parent_x = rail_offset + parent_rail * rail_pitch
1555
+ conn = {"left": min(child_x, parent_x), "width": abs(child_x - parent_x)}
1556
+ # Draw at the parent's row (where branch meets trunk)
1557
+ row_connectors.setdefault(parent_idx, []).append(conn)
1558
+
14841559
result = []
14851560
for i, entry in enumerate(entries):
14861561
rail = max(entry.rail, 0) if entry.rail >= 0 else 0
14871562
node_x = rail_offset + rail * rail_pitch
14881563
@@ -1491,31 +1566,18 @@
14911566
for span_rail, span_start, span_end in active_spans:
14921567
if span_start <= i <= span_end:
14931568
active_rails.add(span_rail)
14941569
14951570
lines = [{"x": rail_offset + r * rail_pitch} for r in sorted(active_rails)]
1496
-
1497
- # Fork/merge connector: if this entry's parent is on a different rail,
1498
- # draw a horizontal connector at the parent's row (where the line joins)
1499
- connector = None
1500
- if entry.event_type == "ci" and entry.parent_rid in rid_to_idx:
1501
- parent_idx = rid_to_idx[entry.parent_rid]
1502
- parent_rail = rid_to_rail.get(entry.parent_rid, 0)
1503
- if parent_rail != rail and parent_idx == i + 1:
1504
- # Connector at this row going to parent's rail
1505
- parent_x = rail_offset + parent_rail * rail_pitch
1506
- connector = {
1507
- "left": min(node_x, parent_x),
1508
- "width": abs(node_x - parent_x),
1509
- }
1571
+ connectors = row_connectors.get(i, [])
15101572
15111573
result.append(
15121574
{
15131575
"entry": entry,
15141576
"node_x": node_x,
15151577
"lines": lines,
1516
- "connector": connector,
1578
+ "connectors": connectors,
15171579
"graph_width": graph_width,
15181580
}
15191581
)
15201582
15211583
return result
15221584
--- fossil/views.py
+++ fossil/views.py
@@ -868,10 +868,67 @@
868
869 return redirect("fossil:tickets", slug=slug)
870
871 return render(request, "fossil/ticket_form.html", {"project": project, "active_tab": "tickets", "title": "New Ticket"})
872
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
873
874 # --- User Activity ---
875
876
877 @login_required
@@ -1479,10 +1536,28 @@
1479 parent_idx = rid_to_idx[entry.parent_rid]
1480 if parent_idx > i:
1481 rail = max(entry.rail, 0)
1482 active_spans.append((rail, i, parent_idx))
1483
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1484 result = []
1485 for i, entry in enumerate(entries):
1486 rail = max(entry.rail, 0) if entry.rail >= 0 else 0
1487 node_x = rail_offset + rail * rail_pitch
1488
@@ -1491,31 +1566,18 @@
1491 for span_rail, span_start, span_end in active_spans:
1492 if span_start <= i <= span_end:
1493 active_rails.add(span_rail)
1494
1495 lines = [{"x": rail_offset + r * rail_pitch} for r in sorted(active_rails)]
1496
1497 # Fork/merge connector: if this entry's parent is on a different rail,
1498 # draw a horizontal connector at the parent's row (where the line joins)
1499 connector = None
1500 if entry.event_type == "ci" and entry.parent_rid in rid_to_idx:
1501 parent_idx = rid_to_idx[entry.parent_rid]
1502 parent_rail = rid_to_rail.get(entry.parent_rid, 0)
1503 if parent_rail != rail and parent_idx == i + 1:
1504 # Connector at this row going to parent's rail
1505 parent_x = rail_offset + parent_rail * rail_pitch
1506 connector = {
1507 "left": min(node_x, parent_x),
1508 "width": abs(node_x - parent_x),
1509 }
1510
1511 result.append(
1512 {
1513 "entry": entry,
1514 "node_x": node_x,
1515 "lines": lines,
1516 "connector": connector,
1517 "graph_width": graph_width,
1518 }
1519 )
1520
1521 return result
1522
--- fossil/views.py
+++ fossil/views.py
@@ -868,10 +868,67 @@
868
869 return redirect("fossil:tickets", slug=slug)
870
871 return render(request, "fossil/ticket_form.html", {"project": project, "active_tab": "tickets", "title": "New Ticket"})
872
873
874 @login_required
875 def ticket_edit(request, slug, ticket_uuid):
876 P.PROJECT_CHANGE.check(request.user)
877 project, fossil_repo, reader = _get_repo_and_reader(slug)
878
879 with reader:
880 ticket = reader.get_ticket_detail(ticket_uuid)
881 if not ticket:
882 raise Http404("Ticket not found")
883
884 if request.method == "POST":
885 from fossil.cli import FossilCLI
886
887 cli = FossilCLI()
888 fields = {}
889 for field in ["title", "status", "type", "severity", "priority", "resolution", "subsystem"]:
890 val = request.POST.get(field, "").strip()
891 if val:
892 fields[field] = val
893 if fields:
894 success = cli.ticket_change(fossil_repo.full_path, ticket.uuid, fields)
895 if success:
896 from django.contrib import messages
897
898 messages.success(request, f'Ticket "{ticket.title}" updated.')
899 from django.shortcuts import redirect
900
901 return redirect("fossil:ticket_detail", slug=slug, ticket_uuid=ticket.uuid)
902
903 return render(
904 request,
905 "fossil/ticket_edit.html",
906 {"project": project, "ticket": ticket, "active_tab": "tickets"},
907 )
908
909
910 @login_required
911 def ticket_comment(request, slug, ticket_uuid):
912 P.PROJECT_CHANGE.check(request.user)
913 project, fossil_repo, reader = _get_repo_and_reader(slug)
914
915 if request.method == "POST":
916 comment = request.POST.get("comment", "").strip()
917 if comment:
918 from fossil.cli import FossilCLI
919
920 cli = FossilCLI()
921 success = cli.ticket_change(fossil_repo.full_path, ticket_uuid, {"icomment": comment})
922 if success:
923 from django.contrib import messages
924
925 messages.success(request, "Comment added.")
926 from django.shortcuts import redirect
927
928 return redirect("fossil:ticket_detail", slug=slug, ticket_uuid=ticket_uuid)
929
930
931 # --- User Activity ---
932
933
934 @login_required
@@ -1479,10 +1536,28 @@
1536 parent_idx = rid_to_idx[entry.parent_rid]
1537 if parent_idx > i:
1538 rail = max(entry.rail, 0)
1539 active_spans.append((rail, i, parent_idx))
1540
1541 # Precompute connectors: for each row, collect all horizontal connections
1542 # A connector appears when a child on one rail connects to a parent on a different rail
1543 # We draw the connector at BOTH the child row (fork out) and on every row where
1544 # a branch line needs to cross from one rail to another
1545 row_connectors: dict[int, list[dict]] = {}
1546 for entry in entries:
1547 if entry.event_type != "ci" or entry.parent_rid not in rid_to_idx:
1548 continue
1549 parent_idx = rid_to_idx[entry.parent_rid]
1550 child_rail = max(entry.rail, 0)
1551 parent_rail = rid_to_rail.get(entry.parent_rid, 0)
1552 if child_rail != parent_rail:
1553 child_x = rail_offset + child_rail * rail_pitch
1554 parent_x = rail_offset + parent_rail * rail_pitch
1555 conn = {"left": min(child_x, parent_x), "width": abs(child_x - parent_x)}
1556 # Draw at the parent's row (where branch meets trunk)
1557 row_connectors.setdefault(parent_idx, []).append(conn)
1558
1559 result = []
1560 for i, entry in enumerate(entries):
1561 rail = max(entry.rail, 0) if entry.rail >= 0 else 0
1562 node_x = rail_offset + rail * rail_pitch
1563
@@ -1491,31 +1566,18 @@
1566 for span_rail, span_start, span_end in active_spans:
1567 if span_start <= i <= span_end:
1568 active_rails.add(span_rail)
1569
1570 lines = [{"x": rail_offset + r * rail_pitch} for r in sorted(active_rails)]
1571 connectors = row_connectors.get(i, [])
 
 
 
 
 
 
 
 
 
 
 
 
 
1572
1573 result.append(
1574 {
1575 "entry": entry,
1576 "node_x": node_x,
1577 "lines": lines,
1578 "connectors": connectors,
1579 "graph_width": graph_width,
1580 }
1581 )
1582
1583 return result
1584
--- templates/fossil/partials/timeline_entries.html
+++ templates/fossil/partials/timeline_entries.html
@@ -49,13 +49,13 @@
4949
{% for line in item.lines %}
5050
<div class="tl-vline tl-vline-ci" style="left: {{ line.x }}px;"></div>
5151
{% endfor %}
5252
<div class="tl-node {% if e.is_merge %}tl-node-merge{% elif e.event_type == 'ci' %}tl-node-ci{% elif e.event_type == 'w' %}tl-node-w{% elif e.event_type == 't' %}tl-node-t{% elif e.event_type == 'f' %}tl-node-f{% else %}tl-node-other{% endif %}"
5353
style="left: {{ item.node_x }}px;"></div>
54
- {% if item.connector %}
55
- <div style="position:absolute; top:75%; height:25%; left:{{ item.connector.left }}px; width:{{ item.connector.width }}px; border-bottom:2px solid rgba(220,57,76,0.3); border-left:2px solid rgba(220,57,76,0.3); border-right:2px solid rgba(220,57,76,0.3); border-radius:0 0 4px 4px; z-index:1;"></div>
56
- {% endif %}
54
+ {% for conn in item.connectors %}
55
+ <div style="position:absolute; top:40%; height:30%; left:{{ conn.left }}px; width:{{ conn.width }}px; border-bottom:2px solid rgba(220,57,76,0.35); border-left:2px solid rgba(220,57,76,0.35); border-right:2px solid rgba(220,57,76,0.35); border-radius:0 0 4px 4px; z-index:1;"></div>
56
+ {% endfor %}
5757
</div>
5858
5959
{# Time #}
6060
<div class="tl-time">{{ e.timestamp|date:"H:i" }}</div>
6161
6262
--- templates/fossil/partials/timeline_entries.html
+++ templates/fossil/partials/timeline_entries.html
@@ -49,13 +49,13 @@
49 {% for line in item.lines %}
50 <div class="tl-vline tl-vline-ci" style="left: {{ line.x }}px;"></div>
51 {% endfor %}
52 <div class="tl-node {% if e.is_merge %}tl-node-merge{% elif e.event_type == 'ci' %}tl-node-ci{% elif e.event_type == 'w' %}tl-node-w{% elif e.event_type == 't' %}tl-node-t{% elif e.event_type == 'f' %}tl-node-f{% else %}tl-node-other{% endif %}"
53 style="left: {{ item.node_x }}px;"></div>
54 {% if item.connector %}
55 <div style="position:absolute; top:75%; height:25%; left:{{ item.connector.left }}px; width:{{ item.connector.width }}px; border-bottom:2px solid rgba(220,57,76,0.3); border-left:2px solid rgba(220,57,76,0.3); border-right:2px solid rgba(220,57,76,0.3); border-radius:0 0 4px 4px; z-index:1;"></div>
56 {% endif %}
57 </div>
58
59 {# Time #}
60 <div class="tl-time">{{ e.timestamp|date:"H:i" }}</div>
61
62
--- templates/fossil/partials/timeline_entries.html
+++ templates/fossil/partials/timeline_entries.html
@@ -49,13 +49,13 @@
49 {% for line in item.lines %}
50 <div class="tl-vline tl-vline-ci" style="left: {{ line.x }}px;"></div>
51 {% endfor %}
52 <div class="tl-node {% if e.is_merge %}tl-node-merge{% elif e.event_type == 'ci' %}tl-node-ci{% elif e.event_type == 'w' %}tl-node-w{% elif e.event_type == 't' %}tl-node-t{% elif e.event_type == 'f' %}tl-node-f{% else %}tl-node-other{% endif %}"
53 style="left: {{ item.node_x }}px;"></div>
54 {% for conn in item.connectors %}
55 <div style="position:absolute; top:40%; height:30%; left:{{ conn.left }}px; width:{{ conn.width }}px; border-bottom:2px solid rgba(220,57,76,0.35); border-left:2px solid rgba(220,57,76,0.35); border-right:2px solid rgba(220,57,76,0.35); border-radius:0 0 4px 4px; z-index:1;"></div>
56 {% endfor %}
57 </div>
58
59 {# Time #}
60 <div class="tl-time">{{ e.timestamp|date:"H:i" }}</div>
61
62
--- templates/fossil/ticket_detail.html
+++ templates/fossil/ticket_detail.html
@@ -10,18 +10,25 @@
1010
</div>
1111
1212
<div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
1313
<div class="px-6 py-5 border-b border-gray-700">
1414
<div class="flex items-start justify-between gap-4">
15
- <h2 class="text-xl font-bold text-gray-100">{{ ticket.title|default:"(untitled)" }}</h2>
16
- {% if ticket.status == "Open" %}
17
- <span class="inline-flex rounded-full bg-green-900/50 px-3 py-1 text-xs font-semibold text-green-300 flex-shrink-0">{{ ticket.status }}</span>
18
- {% elif ticket.status == "Closed" or ticket.status == "Fixed" %}
19
- <span class="inline-flex rounded-full bg-gray-700 px-3 py-1 text-xs font-semibold text-gray-300 flex-shrink-0">{{ ticket.status }}</span>
20
- {% else %}
21
- <span class="inline-flex rounded-full bg-yellow-900/50 px-3 py-1 text-xs font-semibold text-yellow-300 flex-shrink-0">{{ ticket.status }}</span>
22
- {% endif %}
15
+ <div class="flex-1">
16
+ <h2 class="text-xl font-bold text-gray-100">{{ ticket.title|default:"(untitled)" }}</h2>
17
+ </div>
18
+ <div class="flex items-center gap-2 flex-shrink-0">
19
+ {% if perms.projects.change_project %}
20
+ <a href="{% url 'fossil:ticket_edit' slug=project.slug ticket_uuid=ticket.uuid %}" class="rounded-md bg-gray-700 px-3 py-1.5 text-xs font-semibold text-gray-300 hover:bg-gray-600">Edit</a>
21
+ {% endif %}
22
+ {% if ticket.status == "Open" %}
23
+ <span class="inline-flex rounded-full bg-green-900/50 px-3 py-1 text-xs font-semibold text-green-300">{{ ticket.status }}</span>
24
+ {% elif ticket.status == "Closed" or ticket.status == "Fixed" %}
25
+ <span class="inline-flex rounded-full bg-gray-700 px-3 py-1 text-xs font-semibold text-gray-300">{{ ticket.status }}</span>
26
+ {% else %}
27
+ <span class="inline-flex rounded-full bg-yellow-900/50 px-3 py-1 text-xs font-semibold text-yellow-300">{{ ticket.status }}</span>
28
+ {% endif %}
29
+ </div>
2330
</div>
2431
<p class="mt-1 text-xs text-gray-500">
2532
<code class="font-mono">{{ ticket.uuid|truncatechars:16 }}</code>
2633
&middot; opened {{ ticket.created|timesince }} ago
2734
</p>
@@ -83,8 +90,22 @@
8390
</div>
8491
</div>
8592
</div>
8693
{% endfor %}
8794
</div>
95
+</div>
96
+{% endif %}
97
+
98
+{% if perms.projects.change_project %}
99
+<div class="mt-6">
100
+ <h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-3">Add Comment</h3>
101
+ <form method="post" action="{% url 'fossil:ticket_comment' slug=project.slug ticket_uuid=ticket.uuid %}" class="rounded-lg bg-gray-800 border border-gray-700 p-4">
102
+ {% csrf_token %}
103
+ <textarea name="comment" rows="4" required placeholder="Write a comment (Markdown supported)..."
104
+ class="w-full rounded-md border-gray-700 bg-gray-900 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm font-mono"></textarea>
105
+ <div class="mt-3 flex justify-end">
106
+ <button type="submit" class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">Comment</button>
107
+ </div>
108
+ </form>
88109
</div>
89110
{% endif %}
90111
{% endblock %}
91112
92113
ADDED templates/fossil/ticket_edit.html
--- templates/fossil/ticket_detail.html
+++ templates/fossil/ticket_detail.html
@@ -10,18 +10,25 @@
10 </div>
11
12 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
13 <div class="px-6 py-5 border-b border-gray-700">
14 <div class="flex items-start justify-between gap-4">
15 <h2 class="text-xl font-bold text-gray-100">{{ ticket.title|default:"(untitled)" }}</h2>
16 {% if ticket.status == "Open" %}
17 <span class="inline-flex rounded-full bg-green-900/50 px-3 py-1 text-xs font-semibold text-green-300 flex-shrink-0">{{ ticket.status }}</span>
18 {% elif ticket.status == "Closed" or ticket.status == "Fixed" %}
19 <span class="inline-flex rounded-full bg-gray-700 px-3 py-1 text-xs font-semibold text-gray-300 flex-shrink-0">{{ ticket.status }}</span>
20 {% else %}
21 <span class="inline-flex rounded-full bg-yellow-900/50 px-3 py-1 text-xs font-semibold text-yellow-300 flex-shrink-0">{{ ticket.status }}</span>
22 {% endif %}
 
 
 
 
 
 
 
23 </div>
24 <p class="mt-1 text-xs text-gray-500">
25 <code class="font-mono">{{ ticket.uuid|truncatechars:16 }}</code>
26 &middot; opened {{ ticket.created|timesince }} ago
27 </p>
@@ -83,8 +90,22 @@
83 </div>
84 </div>
85 </div>
86 {% endfor %}
87 </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88 </div>
89 {% endif %}
90 {% endblock %}
91
92 DDED templates/fossil/ticket_edit.html
--- templates/fossil/ticket_detail.html
+++ templates/fossil/ticket_detail.html
@@ -10,18 +10,25 @@
10 </div>
11
12 <div class="overflow-hidden rounded-lg bg-gray-800 shadow border border-gray-700">
13 <div class="px-6 py-5 border-b border-gray-700">
14 <div class="flex items-start justify-between gap-4">
15 <div class="flex-1">
16 <h2 class="text-xl font-bold text-gray-100">{{ ticket.title|default:"(untitled)" }}</h2>
17 </div>
18 <div class="flex items-center gap-2 flex-shrink-0">
19 {% if perms.projects.change_project %}
20 <a href="{% url 'fossil:ticket_edit' slug=project.slug ticket_uuid=ticket.uuid %}" class="rounded-md bg-gray-700 px-3 py-1.5 text-xs font-semibold text-gray-300 hover:bg-gray-600">Edit</a>
21 {% endif %}
22 {% if ticket.status == "Open" %}
23 <span class="inline-flex rounded-full bg-green-900/50 px-3 py-1 text-xs font-semibold text-green-300">{{ ticket.status }}</span>
24 {% elif ticket.status == "Closed" or ticket.status == "Fixed" %}
25 <span class="inline-flex rounded-full bg-gray-700 px-3 py-1 text-xs font-semibold text-gray-300">{{ ticket.status }}</span>
26 {% else %}
27 <span class="inline-flex rounded-full bg-yellow-900/50 px-3 py-1 text-xs font-semibold text-yellow-300">{{ ticket.status }}</span>
28 {% endif %}
29 </div>
30 </div>
31 <p class="mt-1 text-xs text-gray-500">
32 <code class="font-mono">{{ ticket.uuid|truncatechars:16 }}</code>
33 &middot; opened {{ ticket.created|timesince }} ago
34 </p>
@@ -83,8 +90,22 @@
90 </div>
91 </div>
92 </div>
93 {% endfor %}
94 </div>
95 </div>
96 {% endif %}
97
98 {% if perms.projects.change_project %}
99 <div class="mt-6">
100 <h3 class="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-3">Add Comment</h3>
101 <form method="post" action="{% url 'fossil:ticket_comment' slug=project.slug ticket_uuid=ticket.uuid %}" class="rounded-lg bg-gray-800 border border-gray-700 p-4">
102 {% csrf_token %}
103 <textarea name="comment" rows="4" required placeholder="Write a comment (Markdown supported)..."
104 class="w-full rounded-md border-gray-700 bg-gray-900 text-gray-100 shadow-sm focus:border-brand focus:ring-brand sm:text-sm font-mono"></textarea>
105 <div class="mt-3 flex justify-end">
106 <button type="submit" class="rounded-md bg-brand px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-brand-hover">Comment</button>
107 </div>
108 </form>
109 </div>
110 {% endif %}
111 {% endblock %}
112
113 DDED templates/fossil/ticket_edit.html
--- a/templates/fossil/ticket_edit.html
+++ b/templates/fossil/ticket_edit.html
@@ -0,0 +1,37 @@
1
+{% extends "base.html" %}
2
+{% block title %}Edit: {{ ticket.title }} — {{ project.name }} — Fossilrepo{% endblock %}
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="mb-4">
9
+ <a href="{% url 'fossil:ticket_detail' slug=project.slug ticket_uuid=ticket.uuid %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to ticket</a>
10
+</div>
11
+
12
+<div class="mx-auto max-w-2xl">
13
+ <h2 class="text-xl font-bold text-gray-100 mb-4">Edit Ticket</h2>
14
+
15
+ <form method="post" class="space-y-4 roundeg bg-gray-800 p-6 shadow-sm border border-gray-700">
16
+ {% csrf_token %}
17
+
18
+ <div>
19
+ <label class="block text-sm font-medium text-gray-300 mb-1">Title</label>
20
+ <input type="text" name="title" value="{{ ticket.title 7sm focus:border-brand focus:ring-brand
21
+{% block title %}Edit: {{ ticket.title }} — {{ project.name }} — Fossilrepo{</div> <option valuet %}
22
+<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
23
+{% include "fossil{sm focus:border-brand focus:ring-brand sm:text-sm"ay-100 shadow-inner focus:bOpen <option value="Deferren" {% if ticket.status == "Open" %}selected{% endif %}{% for s in "Open,Fixed,Closease.html" %}
24
+{% block title %}Edit: {{ ticket.title }} — {{ project.name }} — Fossilrepo{% endblock %}
25
+
26
+{% block content %}
27
+<h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
28
+{% include "fossil/_project_nav.html" %}
29
+
30
+<div class="mb-4">
31
+ <a href="{% url 'fossil:ticket_detail' slug=project.slug ticket_uuid=ticket.uuid %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to ticket</a>
32
+</div>
33
+
34
+<div class="mx-auto max-w-2xl">
35
+ <h2 class="text-xl font-bold text-gray-100 mb-4">Edit Ticket</h2>
36
+
37
+ <form method="post" class="spa
--- a/templates/fossil/ticket_edit.html
+++ b/templates/fossil/ticket_edit.html
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/templates/fossil/ticket_edit.html
+++ b/templates/fossil/ticket_edit.html
@@ -0,0 +1,37 @@
1 {% extends "base.html" %}
2 {% block title %}Edit: {{ ticket.title }} — {{ project.name }} — Fossilrepo{% endblock %}
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="mb-4">
9 <a href="{% url 'fossil:ticket_detail' slug=project.slug ticket_uuid=ticket.uuid %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to ticket</a>
10 </div>
11
12 <div class="mx-auto max-w-2xl">
13 <h2 class="text-xl font-bold text-gray-100 mb-4">Edit Ticket</h2>
14
15 <form method="post" class="space-y-4 roundeg bg-gray-800 p-6 shadow-sm border border-gray-700">
16 {% csrf_token %}
17
18 <div>
19 <label class="block text-sm font-medium text-gray-300 mb-1">Title</label>
20 <input type="text" name="title" value="{{ ticket.title 7sm focus:border-brand focus:ring-brand
21 {% block title %}Edit: {{ ticket.title }} — {{ project.name }} — Fossilrepo{</div> <option valuet %}
22 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
23 {% include "fossil{sm focus:border-brand focus:ring-brand sm:text-sm"ay-100 shadow-inner focus:bOpen <option value="Deferren" {% if ticket.status == "Open" %}selected{% endif %}{% for s in "Open,Fixed,Closease.html" %}
24 {% block title %}Edit: {{ ticket.title }} — {{ project.name }} — Fossilrepo{% endblock %}
25
26 {% block content %}
27 <h1 class="text-2xl font-bold text-gray-100 mb-2">{{ project.name }}</h1>
28 {% include "fossil/_project_nav.html" %}
29
30 <div class="mb-4">
31 <a href="{% url 'fossil:ticket_detail' slug=project.slug ticket_uuid=ticket.uuid %}" class="text-sm text-brand-light hover:text-brand">&larr; Back to ticket</a>
32 </div>
33
34 <div class="mx-auto max-w-2xl">
35 <h2 class="text-xl font-bold text-gray-100 mb-4">Edit Ticket</h2>
36
37 <form method="post" class="spa

Keyboard Shortcuts

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