╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │29 conflicted: bool = false, │ │30 comments: Comment.Window = .empty, │ │31 attachments: []const Attachment.WithId = &.{}, │ │32 - assignees: []const []const u8 = &.{}, │ │32 + assignees: []const ui.Assignee = &.{}, │ │33 // whether the viewer may change the thread, computed here so the web ui has it│ │34 can_modify: bool = false, │ │35 }; │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │80 identity: []const u8, │ │81 // the url-encoded label the lists are filtered to ("" = unfiltered). │ │82 label: []const u8, │ │83 +// the url-encoded name or email the lists are filtered to, as typed ("" = unfiltered).│ │84 +assignee: []const u8, │ │85 +// false when the assignee names no user or no assigned issue, which lists nothing │ │86 +assignee_found: bool = true, │ │87 // the decoded query the lists are filtered to, or null when unfiltered. │ │88 search: ?[]const u8 = null, │ │89 // the hex event id of the issue its status's window is rooted at ("" = the │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │171 } │ │172 │ │173 // an empty listing, for the wasm / no-repo paths. │ │170 -pub fn emptyResult(aa: std.mem.Allocator, identity: []const u8, label: []const u8, search: []const u8, selected_id: []const u8, comment_id: []const u8, comments_start: usize, theirs_picks: []const u8, view: ui.RoutablePage.IssuesView) !Self { │ │174 +pub fn emptyResult(aa: std.mem.Allocator, identity: []const u8, label: []const u8, assignee: []const u8, search: []const u8, selected_id: []const u8, comment_id: []const u8, comments_start: usize, theirs_picks: []const u8, view: ui.RoutablePage.IssuesView) !Self {│ │175 return .{ │ │176 .identity = try aa.dupe(u8, identity), │ │177 .label = try aa.dupe(u8, label), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │178 + .assignee = try aa.dupe(u8, assignee), │ │179 .search = if (search.len == 0) null else std.Uri.percentDecodeInPlace(try aa.dupe(u8, search)),│ │180 .selected_id = try aa.dupe(u8, selected_id), │ │181 .comment_id = try aa.dupe(u8, comment_id), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │197 } │ │198 │ │199 // read one window per status of an opened repo's issues (filtered to `label` │ │195 -// when set), ordered by creation (newest first). the window of the issue │ │196 -// `selected_id` names starts at it ("" = the beginning). a git repo reads the │ │200 +// and `assignee` when set), ordered by creation (newest first). the window of │ │201 +// the issue `selected_id` names starts at it ("" = the beginning). a git repo reads the│ │202 // event db next to it (synced from the events branch on each page build); a │ │203 // xit repo reads its own db. │ │204 pub fn init( │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────╮ │212 admin_moment: ?evt.AdminDB.HashMap(.read_only),│ │213 identity: []const u8, │ │214 label: []const u8, │ │215 + assignee: []const u8, │ │216 search: []const u8, │ │217 selected_id: []const u8, │ │218 comment_id: []const u8, │ ╰────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │221 view: ui.RoutablePage.IssuesView, │ │222 viewer: ?ui.Viewer, │ │223 ) !Self { │ │218 - const empty = try emptyResult(arena.allocator(), identity, label, search, selected_id, comment_id, comments_start, theirs_picks, view); │ │224 + const empty = try emptyResult(arena.allocator(), identity, label, assignee, search, selected_id, comment_id, comments_start, theirs_picks, view);│ │225 │ │226 const aa = arena.allocator(); │ │227 const DB = evt.EventDB(repo_opts.hash); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │263 closed_set = try thread.statusSet(Self, DB, status_to_issues, .closed); │ │264 } else if (rooted) return error.NotFound; │ │265 │ │266 + // the assignee's issues, intersected with each status's set when windowed. │ │267 + // a name resolves to its user's email. an unknown name or an email no │ │268 + // issue carries lists nothing and marks the box, like the assign input. │ │269 + var assignee_set: ?DB.SortedSet(.read_only) = null; │ │270 + const assignee_found = empty.assignee.len == 0 or found: { │ │271 + const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, empty.assignee)); │ │272 + const email = switch (try ui.assigneeEmail(admin_moment, arena, decoded)) { │ │273 + .email => |email| email, │ │274 + .failure => break :found false, │ │275 + }; │ │276 + const by_assignee_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Assignment.assignee_to_issue_id_set_key)) orelse break :found false;│ │277 + const by_assignee = try DB.HashMap(.read_only).init(by_assignee_cursor); │ │278 + const set_cursor = try by_assignee.getCursor(hash.hashInt(repo_opts.hash, email)) orelse break :found false; │ │279 + assignee_set = try DB.SortedSet(.read_only).init(set_cursor); │ │280 + break :found true; │ │281 + }; │ │282 + if (!assignee_found) { │ │283 + open_set = null; │ │284 + closed_set = null; │ │285 + } │ │286 + │ │287 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.record_map_key)) orelse { │ │288 if (strict) return error.NotFound; │ │289 return empty; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │326 .closed => closed_set, │ │327 }) orelse return error.NotFound; │ │328 if (!try set.contains(order_key)) return error.NotFound; │ │329 + if (assignee_set) |assignee_issues| if (!try assignee_issues.contains(order_key)) return error.NotFound;│ │330 │ │331 switch (issue_event.event.status) { │ │332 .open => open_root = order_key, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │357 } │ │358 │ │359 const thread_comments_start = if (empty.comment_id.len == 0) comments_start else 0; │ │332 - const open_window = try thread.loadWindow(Self, .issue, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_issue, open_set, open_root, conflict_set, empty.selected_id, thread_comments_start, empty.search, viewer); │ │333 - const closed_window = try thread.loadWindow(Self, .issue, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_issue, closed_set, closed_root, conflict_set, empty.selected_id, thread_comments_start, empty.search, viewer); │ │334 - const conflicts_window = try thread.loadWindow(Self, .issue, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_issue, conflict_set, conflicts_root, conflict_set, empty.selected_id, thread_comments_start, null, viewer); │ │360 + const open_window = try thread.loadWindow(Self, .issue, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_issue, open_set, open_root, conflict_set, empty.selected_id, thread_comments_start, empty.search, assignee_set, viewer); │ │361 + const closed_window = try thread.loadWindow(Self, .issue, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_issue, closed_set, closed_root, conflict_set, empty.selected_id, thread_comments_start, empty.search, assignee_set, viewer);│ │362 + const conflicts_window = try thread.loadWindow(Self, .issue, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_issue, conflict_set, conflicts_root, conflict_set, empty.selected_id, thread_comments_start, null, null, viewer); │ │363 if (view == .conflicts and conflicts_window.count > 0) resolved_view = .conflicts; │ │364 │ │365 const comment_page = if (empty.comment_id.len == 0) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯