╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │203 const start_seg = @tagName(Params.ParamKey.start) ++ ":"; │ │204 const line_seg = @tagName(Params.ParamKey.line) ++ ":"; │ │205 const theirs_seg = @tagName(Params.ParamKey.theirs) ++ ":"; │ │206 + const issue_seg = "issue:"; │ │207 + const comment_seg = "comment:"; │ │208 // marks a files or commits route's trailing path: the value is the raw│ │209 // remainder of the url, so it must come last │ │210 const path_seg = "path:"; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │513 }, │ │514 .repo_issues => |i| blk: { │ │515 const prefix = try repoUrlPrefix(arena, i.name.slice()); │ │514 - if (i.view == .edit) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/edit", .{ prefix, i.selected.slice() }); │ │515 - if (i.view == .description) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/description", .{ prefix, i.selected.slice() }); │ │516 + if (i.view == .edit) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/edit", .{ prefix, i.selected.slice() }); │ │517 + if (i.view == .description) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/description", .{ prefix, i.selected.slice() }); │ │518 if (i.view == .resolve) break :blk if (i.theirs.len == 0) │ │517 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/resolve", .{ prefix, i.selected.slice() }) │ │519 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/resolve", .{ prefix, i.selected.slice() }) │ │520 else │ │519 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/" ++ theirs_seg ++ "{s}/{s}/resolve", .{ prefix, i.theirs.slice(), i.selected.slice() }); │ │521 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/" ++ theirs_seg ++ "{s}/resolve", .{ prefix, i.selected.slice(), i.theirs.slice() });│ │522 if (i.view == .conflicts) break :blk if (i.selected.len == 0) │ │523 try std.fmt.allocPrint(arena.allocator(), "{s}/issues/conflicts", .{prefix}) │ │524 else │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │525 try std.fmt.allocPrint(arena.allocator(), "{s}/issues/" ++ start_seg ++ "{s}/conflicts", .{ prefix, i.selected.slice() }); │ │524 - // a selected issue's url carries its id instead of a view │ │525 - // name, so it survives status changes │ │526 - const word = if (i.selected.len != 0) i.selected.slice() else @tagName(i.view); │ │527 - break :blk if (i.tag.len == 0 and i.comments_start == 0) │ │528 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}", .{ prefix, word }) │ │529 - else if (i.tag.len == 0) │ │530 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/" ++ start_seg ++ "{d}", .{ prefix, word, i.comments_start }) │ │526 + if (i.selected.len != 0) break :blk if (i.comments_start == 0) │ │527 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}", .{ prefix, i.selected.slice() }) │ │528 else │ │532 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/" ++ tag_filter_seg ++ "{s}", .{ prefix, word, i.tag.slice() }); │ │529 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/" ++ start_seg ++ "{d}", .{ prefix, i.selected.slice(), i.comments_start });│ │530 + break :blk if (i.tag.len == 0) │ │531 + try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}", .{ prefix, @tagName(i.view) }) │ │532 + else │ │533 + try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/" ++ tag_filter_seg ++ "{s}", .{ prefix, @tagName(i.view), i.tag.slice() }); │ │534 }, │ │535 .repo_comments => |c| blk: { │ │536 const prefix = try repoUrlPrefix(arena, c.name.slice()); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │537 break :blk if (c.start == 0) │ │537 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/comments/{s}", .{ prefix, c.thread.slice(), c.selected.slice() }) │ │538 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/" ++ comment_seg ++ "{s}", .{ prefix, c.thread.slice(), c.selected.slice() }) │ │539 else │ │539 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/comments/{s}/" ++ start_seg ++ "{d}", .{ prefix, c.thread.slice(), c.selected.slice(), c.start }); │ │540 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ issue_seg ++ "{s}/" ++ comment_seg ++ "{s}/" ++ start_seg ++ "{d}", .{ prefix, c.thread.slice(), c.selected.slice(), c.start });│ │541 }, │ │542 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/settings", .{try repoUrlPrefix(arena, name.slice())}), │ │543 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/auth", .{try repoUrlPrefix(arena, name.slice())}), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │676 const kind = std.meta.stringToEnum(RefKind, @tagName(ref.kind)) orelse return null; │ │677 return repoRefsRoute(pair, kind, ref.value); │ │678 } │ │678 - if (std.mem.eql(u8, tab, "issues")) { │ │679 - // "<id>/edit" is the edit form for that issue, with no params; │ │680 - // "<id>/description" is its whole-description page. │ │681 - const after_tab = segments.rest(); │ │682 - // "<thread-id>/comments/<comment-id>[/start:<n>]" is a comment │ │683 - // permalink with one window of its immediate replies. │ │684 - { │ │685 - var comment_segments = std.mem.splitScalar(u8, after_tab, '/'); │ │686 - const thread = comment_segments.next() orelse return null; │ │687 - const marker = comment_segments.next(); │ │688 - if (marker) |word| { │ │689 - if (std.mem.eql(u8, word, "comments")) { │ │690 - const comment = comment_segments.next() orelse return null; │ │691 - if (thread.len == 0 or comment.len == 0) return null; │ │692 - return repoCommentsRoute(pair, thread, comment, listStart(&comment_segments) orelse return null); │ │693 - } │ │679 + if (std.mem.startsWith(u8, tab, issue_seg)) { │ │680 + const issue_id = tab[issue_seg.len..]; │ │681 + if (issue_id.len == 0) return null; │ │682 + const word = params.scanTail(&segments) catch return null; │ │683 + if (word) |tail| { │ │684 + if (std.mem.startsWith(u8, tail, comment_seg)) { │ │685 + if (!params.only(&.{.start})) return null; │ │686 + const comment_id = tail[comment_seg.len..]; │ │687 + if (comment_id.len == 0) return null; │ │688 + return repoCommentsRoute(pair, issue_id, comment_id, params.start() orelse return null); │ │689 } │ │695 - } │ │696 - if (std.mem.endsWith(u8, after_tab, "/edit")) { │ │697 - const id = after_tab[0 .. after_tab.len - "/edit".len]; │ │698 - if (id.len == 0 or std.mem.indexOfScalar(u8, id, '/') != null) return null; │ │699 - return repoIssuesEditRoute(pair, id); │ │700 - } │ │701 - if (std.mem.endsWith(u8, after_tab, "/description")) { │ │702 - const id = after_tab[0 .. after_tab.len - "/description".len]; │ │703 - if (id.len == 0 or std.mem.indexOfScalar(u8, id, '/') != null) return null; │ │704 - return repoIssuesDescriptionRoute(pair, id); │ │705 - } │ │706 - // "[theirs:<list>/]<id>/resolve" is the conflict resolve form. │ │707 - if (std.mem.endsWith(u8, after_tab, "/resolve")) { │ │708 - var resolve_segments = std.mem.splitScalar(u8, after_tab[0 .. after_tab.len - "/resolve".len], '/'); │ │709 - const id = (params.scanTail(&resolve_segments) catch return null) orelse return null; │ │710 - if (!params.only(&.{.theirs})) return null; │ │711 - return repoIssuesResolveRoute(pair, id, params.values.get(.theirs) orelse ""); │ │690 + if (std.mem.eql(u8, tail, "edit")) return if (params.only(&.{})) repoIssuesEditRoute(pair, issue_id) else null; │ │691 + if (std.mem.eql(u8, tail, "description")) return if (params.only(&.{})) repoIssuesDescriptionRoute(pair, issue_id) else null;│ │692 + if (std.mem.eql(u8, tail, "resolve")) { │ │693 + if (!params.only(&.{.theirs})) return null; │ │694 + return repoIssuesResolveRoute(pair, issue_id, params.values.get(.theirs) orelse ""); │ │695 + } │ │696 + return null; │ │697 } │ │713 - // the word is a view name or an issue event id (whose status │ │714 - // picks the view); a filter with neither is never emitted. │ │698 + if (!params.only(&.{.start})) return null; │ │699 + return repoIssueCommentsRoute(pair, issue_id, params.start() orelse return null); │ │700 + } │ │701 + if (std.mem.eql(u8, tab, "issues")) { │ │702 + // the word is a list view; a filter with neither is never emitted. │ │703 const word = params.scanTail(&segments) catch return null; │ │704 const tag_value = params.values.get(.tag) orelse ""; │ │705 const w = word orelse return if (params.only(&.{})) repoIssuesRoute(pair, .open, "", "") else null; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │709 if (!params.only(&.{.start})) return null; │ │710 return repoIssuesConflictsRoute(pair, params.values.get(.start) orelse ""); │ │711 } │ │724 - if (params.values.get(.start) != null) { │ │725 - if (!params.only(&.{.start})) return null; │ │726 - return repoIssueCommentsRoute(pair, w, params.start() orelse return null); │ │727 - } │ │712 if (!params.only(&.{.tag})) return null; │ │713 if (std.meta.stringToEnum(evt.Issue.Status, w)) |status| return repoIssuesRoute(pair, status, tag_value, "");│ │714 if (std.mem.eql(u8, w, "tags")) return repoIssuesTagsRoute(pair, tag_value); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │715 if (std.mem.eql(u8, w, "new")) return if (tag_value.len == 0) repoIssuesNewRoute(pair) else null;│ │732 - return repoIssuesRoute(pair, .open, tag_value, w); │ │716 + return null; │ │717 } │ │718 if (std.mem.eql(u8, tab, files_seg)) { │ │719 params.scanPairs(&segments) catch return null; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │665 const aa = session.page_arena.allocator(); │ │666 const action = if (data.view == .edit) │ │667 (if (data.identity.len == 0) │ │668 - try std.fmt.allocPrint(aa, "form:/issues/{s}/edit", .{data.selected_id}) │ │668 + try std.fmt.allocPrint(aa, "form:/issue:{s}/edit", .{data.selected_id}) │ │669 else │ │670 - try std.fmt.allocPrint(aa, "form:/repo/{s}/issues/{s}/edit", .{ data.identity, data.selected_id }))│ │670 + try std.fmt.allocPrint(aa, "form:/repo/{s}/issue:{s}/edit", .{ data.identity, data.selected_id })) │ │671 else if (data.identity.len == 0) │ │672 "form:/issue" │ │673 else │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │853 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │854 errdefer box.deinit(allocator); │ │855 box.getFocus().kind = .{ .custom = if (data.identity.len == 0) │ │856 - try std.fmt.allocPrint(aa, "form:/issues/{s}/resolve", .{data.selected_id}) │ │856 + try std.fmt.allocPrint(aa, "form:/issue:{s}/resolve", .{data.selected_id}) │ │857 else │ │858 - try std.fmt.allocPrint(aa, "form:/repo/{s}/issues/{s}/resolve", .{ data.identity, data.selected_id }) };│ │858 + try std.fmt.allocPrint(aa, "form:/repo/{s}/issue:{s}/resolve", .{ data.identity, data.selected_id }) }; │ │859 │ │860 if (conflict.title) |*fc| { │ │861 try addLabel(allocator, &box, "title conflict:"); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1335 .closed => "open", │ │1336 }; │ │1337 row.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │1338 - try std.fmt.allocPrint(pa, "form:/issues/{s}/{s}", .{ entry.id, action }) │ │1338 + try std.fmt.allocPrint(pa, "form:/issue:{s}/{s}", .{ entry.id, action }) │ │1339 else │ │1340 - try std.fmt.allocPrint(pa, "form:/repo/{s}/issues/{s}/{s}", .{ self.data.identity, entry.id, action }) }; │ │1340 + try std.fmt.allocPrint(pa, "form:/repo/{s}/issue:{s}/{s}", .{ self.data.identity, entry.id, action }) }; │ │1341 │ │1342 var button = try wgt.TextBox(ui.Widget).init(allocator, action, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │1343 errdefer button.deinit(allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯