╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │84 // the tag the list is filtered to, url-encoded ("" = unfiltered).│ │85 tag: Array(issue_tag_route_max_len) = .{}, │ │86 selected: Array(evt.event_id_size * 2) = .{}, │ │87 + view: enum { results, tags } = .results, │ │88 }, │ │89 repo_settings: Array(repo_route_max_len), │ │90 repo_auth: Array(repo_route_max_len), │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │239 } }; │ │240 } │ │241 │ │242 + // build a `.repo_issues` route showing "owner/name" (identity)'s tags view. │ │243 + pub fn repoIssuesTagsRoute(identity: []const u8) ?RoutablePage { │ │244 + return .{ .repo_issues = .{ │ │245 + .name = Array(repo_route_max_len).from(identity) orelse return null, │ │246 + .view = .tags, │ │247 + } }; │ │248 + } │ │249 + │ │250 // an inline, owned array of data. keeping it in the route (rather than a │ │251 // borrowed slice) makes RoutablePage a plain value: it can be copied, stored│ │252 // in history, and serialized without any arena tracking. │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │297 }, │ │298 .repo_issues => |i| blk: { │ │299 const prefix = try repoUrlPrefix(arena, i.name.slice()); │ │291 - break :blk if (i.tag.len == 0) │ │292 - (if (i.selected.len == 0) │ │293 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues", .{prefix}) │ │300 + break :blk switch (i.view) { │ │301 + .tags => try std.fmt.allocPrint(arena.allocator(), "{s}/issues/tags", .{prefix}), │ │302 + .results => if (i.tag.len == 0) │ │303 + (if (i.selected.len == 0) │ │304 + try std.fmt.allocPrint(arena.allocator(), "{s}/issues", .{prefix}) │ │305 + else │ │306 + try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}", .{ prefix, i.selected.slice() })) │ │307 + else if (i.selected.len == 0) │ │308 + try std.fmt.allocPrint(arena.allocator(), "{s}/issues/tags/{s}", .{ prefix, i.tag.slice() }) │ │309 else │ │295 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}", .{ prefix, i.selected.slice() })) │ │296 - else if (i.selected.len == 0) │ │297 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/tag/{s}", .{ prefix, i.tag.slice() }) │ │298 - else │ │299 - try std.fmt.allocPrint(arena.allocator(), "{s}/issues/tag/{s}/{s}", .{ prefix, i.tag.slice(), i.selected.slice() }); │ │310 + try std.fmt.allocPrint(arena.allocator(), "{s}/issues/tags/{s}/{s}", .{ prefix, i.tag.slice(), i.selected.slice() }),│ │311 + }; │ │312 }, │ │313 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/settings", .{try repoUrlPrefix(arena, name.slice())}), │ │314 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/auth", .{try repoUrlPrefix(arena, name.slice())}), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │400 .after = after, │ │401 } }; │ │402 if (std.mem.eql(u8, sub, "issues") or std.mem.startsWith(u8, sub, "issues/")) {│ │391 - // "issues[/tag/<tag>][/<issue event id>]" │ │403 + // "issues[/<issue event id>]", "issues/tags" (the tags view), or │ │404 + // "issues/tags/<tag>[/<issue event id>]" (the list filtered to <tag>) │ │405 var segments = std.mem.splitScalar(u8, sub, '/'); │ │406 _ = segments.next(); // "issues" │ │407 var tag_value: []const u8 = ""; │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │408 var id: []const u8 = ""; │ │409 if (segments.next()) |segment| { │ │397 - if (std.mem.eql(u8, segment, "tag")) { │ │398 - tag_value = segments.next() orelse return null; │ │410 + if (std.mem.eql(u8, segment, "tags")) { │ │411 + tag_value = segments.next() orelse return repoIssuesTagsRoute(pair);│ │412 if (tag_value.len == 0) return null; │ │413 id = segments.next() orelse ""; │ │414 } else { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │481 .repo_refs => |a_r| std.mem.eql(u8, a_r.name.slice(), b.repo_refs.name.slice()) and a_r.kind == b.repo_refs.kind and a_r.after == b.repo_refs.after,│ │482 .repo_issues => |a_i| std.mem.eql(u8, a_i.name.slice(), b.repo_issues.name.slice()) and │ │483 std.mem.eql(u8, a_i.tag.slice(), b.repo_issues.tag.slice()) and │ │471 - std.mem.eql(u8, a_i.selected.slice(), b.repo_issues.selected.slice()), │ │484 + std.mem.eql(u8, a_i.selected.slice(), b.repo_issues.selected.slice()) and │ │485 + a_i.view == b.repo_issues.view, │ │486 .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()), │ │487 .repo_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_auth.slice()), │ │488 else => true, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │559 try jw.write(i.tag.slice()); │ │560 try jw.objectField("selected"); │ │561 try jw.write(i.selected.slice());│ │562 + try jw.objectField("view"); │ │563 + try jw.write(@tagName(i.view)); │ │564 }, │ │565 .repo_files => |f| { │ │566 try jw.objectField("name"); │ ╰──────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │594 } │ │595 │ │596 pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !RoutablePage { │ │581 - const Helper = struct { kind: std.meta.Tag(RoutablePage), name: ?[]const u8 = null, after: usize = 0, ref_kind: ?[]const u8 = null, tag: ?[]const u8 = null, selected: ?[]const u8 = null }; │ │597 + const Helper = struct { kind: std.meta.Tag(RoutablePage), name: ?[]const u8 = null, after: usize = 0, ref_kind: ?[]const u8 = null, tag: ?[]const u8 = null, selected: ?[]const u8 = null, view: ?[]const u8 = null };│ │598 const helper = try std.json.innerParse(Helper, allocator, source, options); │ │599 const parseName = struct { │ │600 // errors must stay within std.json's ParseError set; ValueTooLong │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │622 .name = try parseName(repo_route_max_len, helper.name), │ │623 .tag = Array(issue_tag_route_max_len).from(helper.tag orelse "") orelse return error.ValueTooLong, │ │624 .selected = Array(evt.event_id_size * 2).from(helper.selected orelse "") orelse return error.ValueTooLong,│ │625 + .view = if (helper.view) |v| (if (std.mem.eql(u8, v, "tags")) .tags else .results) else .results, │ │626 } }, │ │627 .repo_settings => .{ .repo_settings = try parseName(repo_route_max_len, helper.name) }, │ │628 .repo_auth => .{ .repo_auth = try parseName(repo_route_max_len, helper.name) }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │236 │ │237 // the tabs at the top. │ │238 { │ │239 - var sub_header = try SubHeader.View.init(allocator, session, data.count, data.tag); │ │239 + var sub_header = try SubHeader.View.init(allocator, session, data.identity, data.count, data.tag); │ │240 errdefer sub_header.deinit(allocator); │ │241 try outer.children.put(allocator, sub_header.getFocus().id, .{ .widget = .{ .repo_issues_sub_header = sub_header }, .rect = null, .min_size = null });│ │242 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯