╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Patches.zig│ ╰───────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │713 } │ │714 } │ │715 const thread_comments_start = if (empty.comment_id.len == 0) comments_start else 0; │ │716 - var open_window = try thread.loadWindow(Self, .patch, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, open_set, open_root, conflict_set, empty.selected_id, thread_comments_start, empty.search, viewer); │ │717 - var closed_window = try thread.loadWindow(Self, .patch, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, closed_set, closed_root, conflict_set, empty.selected_id, thread_comments_start, empty.search, viewer); │ │718 - var merged_window = try thread.loadWindow(Self, .patch, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, merged_set, merged_root, conflict_set, empty.selected_id, thread_comments_start, empty.search, viewer); │ │719 - var conflicts_window = try thread.loadWindow(Self, .patch, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, conflict_set, conflicts_root, conflict_set, empty.selected_id, thread_comments_start, null, viewer); │ │716 + var open_window = try thread.loadWindow(Self, .patch, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, open_set, open_root, conflict_set, empty.selected_id, thread_comments_start, empty.search, null, viewer); │ │717 + var closed_window = try thread.loadWindow(Self, .patch, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, closed_set, closed_root, conflict_set, empty.selected_id, thread_comments_start, empty.search, null, viewer);│ │718 + var merged_window = try thread.loadWindow(Self, .patch, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, merged_set, merged_root, conflict_set, empty.selected_id, thread_comments_start, empty.search, null, viewer);│ │719 + var conflicts_window = try thread.loadWindow(Self, .patch, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, conflict_set, conflicts_root, conflict_set, empty.selected_id, thread_comments_start, null, null, viewer);│ │720 try setPatchDetails(repo_kind, repo_opts, io, arena, admin_moment, session.users_dir, haxy_moment, repo, &open_window); │ │721 try setPatchDetails(repo_kind, repo_opts, io, arena, admin_moment, session.users_dir, haxy_moment, repo, &closed_window); │ │722 try setPatchDetails(repo_kind, repo_opts, io, arena, admin_moment, session.users_dir, haxy_moment, repo, &merged_window); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1002 │ │1003 // tabs switching between the patches page's views │ │1004 pub fn initHeader(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header {│ │1005 - var header = try Header.init(allocator, session, data.search); │ │1005 + var header = try Header.init(allocator, session, data.search, null); │ │1006 errdefer header.deinit(allocator); │ │1007 const selected_index = View.viewIndex(data.view); │ │1008 const page_selected = std.meta.activeTag(session.data.current_page) == .repo_patches; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │237 comments_start: usize, │ │238 // the search the set is narrowed to, or null to list it whole │ │239 query: ?[]const u8, │ │240 + // a second set the listing is narrowed to, keyed like `set_maybe` │ │241 + extra_set: ?evt.EventDB(hash_kind).SortedSet(.read_only), │ │242 viewer: ?ui.Viewer, │ │243 ) !Data.Window { │ │242 - if (query) |text| return loadSearchWindow(Data, kind, hash_kind, arena, admin_moment, haxy_moment, records, set_maybe, root_key, conflict_set, selected_id, comments_start, text, viewer); │ │244 + if (query != null or extra_set != null) return loadSearchWindow(Data, kind, hash_kind, arena, admin_moment, haxy_moment, records, set_maybe, root_key, conflict_set, selected_id, comments_start, query orelse "", extra_set, viewer);│ │245 const set = set_maybe orelse return .empty; │ │246 const aa = arena.allocator(); │ │247 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │285 // how many matches one search counts before the count is marked as capped │ │286 const max_search_results = 1000; │ │287 │ │286 -// `set`'s entries matching `query_text`, windowed like `loadWindow`. one pass │ │288 +// `set`'s entries matching `query_text` (any when empty) and held by │ │289 +// `extra_set` when set, windowed like `loadWindow`. one pass │ │290 // yields the window, its neighbours' roots and the match count, which stops at│ │291 // the cap. │ │292 fn loadSearchWindow( │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │303 selected_id: []const u8, │ │304 comments_start: usize, │ │305 query_text: []const u8, │ │306 + extra_set: ?evt.EventDB(hash_kind).SortedSet(.read_only),│ │307 viewer: ?ui.Viewer, │ │308 ) !Data.Window { │ │309 const set = set_maybe orelse return .empty; │ ╰──────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │320 // matching ids are gathered and the listed set is walked in its order │ │321 // until every one has been found │ │322 var matches: std.AutoHashMapUnmanaged([evt.event_id_size]u8, void) = .empty; │ │319 - var results = try srch_thrd.query(DB, hash_kind, kind, haxy_moment, aa, query_text, null); │ │323 + var results = try srch_thrd.query(DB, hash_kind, kind, haxy_moment, aa, query_text, &.{extra_set});│ │324 while (try results.next()) |key| try matches.put(aa, (try resultKey(key))[@sizeOf(u64)..].*, {}); │ │325 │ │326 var iter = try set.iteratorFromIndex(0); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │335 } else { │ │336 // issues and patches list in the index's own order, so the listed set │ │337 // narrows the query and the results stream out already sorted │ │334 - var results = try srch_thrd.query(DB, hash_kind, kind, haxy_moment, aa, query_text, set); │ │338 + var results = try srch_thrd.query(DB, hash_kind, kind, haxy_moment, aa, query_text, &.{ set, extra_set });│ │339 while (try results.next()) |key| { │ │340 try keys.append(aa, try resultKey(key)); │ │341 if (windowKnown(Data, keys.items, root)) break; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │405 viewer, │ │406 ), │ │407 .attachments = try Attachment.load(hash_kind, arena, haxy_moment, &id_hex), │ │404 - .assignees = try evt.Assignment.load(hash_kind, arena, haxy_moment, &id_hex), │ │408 + .assignees = try loadAssignees(hash_kind, arena, admin_moment, haxy_moment, &id_hex), │ │409 .can_modify = if (viewer) |v| v.role.canModify(v.email, record.author_email) else false,│ │410 }; │ │411 if (comptime @hasField(Data.Entry, "conflicted")) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │412 entry.conflicted = if (conflict_set) |conflicts| try conflicts.contains(order_key) else false; │ │413 } │ │414 return entry; │ │415 +} │ │416 + │ │417 +// a thread's assignees, each named after the user holding its email when one does │ │418 +fn loadAssignees( │ │419 + comptime hash_kind: hash.HashKind, │ │420 + arena: *std.heap.ArenaAllocator, │ │421 + admin_moment: ?evt.AdminDB.HashMap(.read_only), │ │422 + haxy_moment: evt.EventDB(hash_kind).HashMap(.read_only), │ │423 + thread_id: []const u8, │ │424 +) ![]const ui.Assignee { │ │425 + const emails = try evt.Assignment.load(hash_kind, arena, haxy_moment, thread_id); │ │426 + const assignees = try arena.allocator().alloc(ui.Assignee, emails.len); │ │427 + for (assignees, emails) |*assignee, email| { │ │428 + const user = if (admin_moment) |moment| try evt.User.readByEmail(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, email) else null;│ │429 + assignee.* = .{ .email = email, .name = if (user) |u| u.name else null }; │ │430 + } │ │431 + return assignees; │ │432 } │ │433 │ │434 +// the url-encoded assignee filter a thread page carries, "" for kinds without one │ │435 +fn assigneeFilter(data: anytype) []const u8 { │ │436 + return if (@hasField(@TypeOf(data.*), "assignee")) data.assignee else ""; │ │437 +} │ │438 + │ │439 // a tab link's route, carrying the page's query when it has one │ │440 pub fn searchRoute(route_maybe: ?ui.RoutablePage, search: ?[]const u8) !ui.RoutablePage { │ │441 const route = route_maybe orelse return error.RouteTooLong; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │448 return std.fmt.bufPrint(buffer, "{s} ({d}{s})", .{ name, win.count, if (win.more) "+" else "" });│ │449 } │ │450 │ │425 -// the common sub-header used by each thread page: the search box, then the │ │426 -// tab strip │ │451 +// the common sub-header used by each thread page: the search box, the │ │452 +// assignee box on the issues page, then the tab strip │ │453 pub const Header = struct { │ │454 box: wgt.Box(Widget), │ │455 // focus id -> semantic stack index; some pages omit unavailable tabs │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯