╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │1086 } │ │1087 │ │1088 fn window(self: *View, index: usize) *const Window { │ │1089 + if (index == conflicts_view_index) return &self.data.conflicts;│ │1090 return self.data.window(splitStatus(index)); │ │1091 } │ │1092 │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │1100 // new-issue form shows). │ │1101 fn selectedSplitIndex(self: *View) ?usize { │ │1102 const idx = self.stackSelectedIndex() orelse return null; │ │ 669 - return if (idx < split_count) idx else null; │ │1103 + return if (idx < split_count or idx == conflicts_view_index) idx else null;│ │1104 } │ │1105 │ │1106 fn detailActive(self: *View, index: usize) bool { │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1146 if (self.header().getSelectedIndex()) |index| { │ │1147 const stack = self.viewStack(); │ │1148 stack.getFocus().child_id = stack.children.keys()[index]; │ │ 715 - self.session.data.current_page = (if (index == form_view_index and self.data.view == .edit) │ │ 716 - ui.RoutablePage.repoIssuesEditRoute(self.data.identity, self.data.selected_id) │ │1149 + self.session.data.current_page = (if (index == form_view_index) │ │1150 + (switch (self.data.view) { │ │1151 + .edit => ui.RoutablePage.repoIssuesEditRoute(self.data.identity, self.data.selected_id), │ │1152 + .resolve => ui.RoutablePage.repoIssuesResolveRoute(self.data.identity, self.data.selected_id, self.data.theirs_picks), │ │1153 + else => ui.RoutablePage.repoIssuesNewRoute(self.data.identity), │ │1154 + }) │ │1155 + else if (index == conflicts_view_index) │ │1156 + ui.RoutablePage.repoIssuesConflictsRoute(self.data.identity, if (self.data.view == .conflicts) self.data.selected_id else "")│ │1157 else if (index == viewIndex(self.data.view) and self.data.selected_id.len != 0) │ │1158 (if (self.data.description_page) │ │1159 ui.RoutablePage.repoIssuesDescriptionRoute(self.data.identity, self.data.selected_id) │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1161 ui.RoutablePage.repoIssuesRoute(self.data.identity, .open, self.data.tag, self.data.selected_id)) │ │1162 else if (index == tags_view_index) │ │1163 ui.RoutablePage.repoIssuesTagsRoute(self.data.identity, self.data.tag) │ │ 724 - else if (index == form_view_index) │ │ 725 - ui.RoutablePage.repoIssuesNewRoute(self.data.identity) │ │1164 else │ │1165 ui.RoutablePage.repoIssuesRoute(self.data.identity, splitStatus(index), self.data.tag, "")) orelse self.session.data.current_page;│ │1166 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1177 if (self.selectedIssueIndex(i)) |sel| { │ │1178 // selecting another issue leaves the description page │ │1179 // behind, like it leaves the tag filter behind. │ │ 742 - const id = self.window(i).issues[sel].id; │ │ 743 - const mirrored = if (self.data.description_page and std.mem.eql(u8, id, self.data.selected_id)) │ │ 744 - ui.RoutablePage.repoIssuesDescriptionRoute(self.data.identity, id) │ │1180 + const entry = &self.window(i).issues[sel]; │ │1181 + const mirrored = if (self.data.description_page and std.mem.eql(u8, entry.id, self.data.selected_id))│ │1182 + ui.RoutablePage.repoIssuesDescriptionRoute(self.data.identity, entry.id) │ │1183 else │ │ 746 - ui.RoutablePage.repoIssuesRoute(self.data.identity, splitStatus(i), "", id); │ │1184 + ui.RoutablePage.repoIssuesRoute(self.data.identity, entry.issue.event.status, "", entry.id); │ │1185 if (mirrored) |route| self.session.data.current_page = route; │ │1186 } │ │1187 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1237 try self.session.input_values.put(inputs_arena, fields[description_field_index].widget.text_input.getFocus().id, entry.issue.event.description);│ │1238 } │ │1239 } │ │1240 + │ │1241 + // the resolve form's inputs prefill from the picked side, which │ │1242 + // is url state and so page-constant like the edit values above │ │1243 + if (self.data.view == .resolve) { │ │1244 + for (form.children.values()) |*child| switch (child.widget) { │ │1245 + .text_input => |*ti| if (self.resolvePrefill(ti.options.name)) |value| { │ │1246 + try self.session.input_values.put(inputs_arena, ti.getFocus().id, value); │ │1247 + }, │ │1248 + else => {}, │ │1249 + }; │ │1250 + } │ │1251 } │ │1252 │ │1253 try self.box.build(allocator, constraint, root_focus); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1289 var spacer = try ui.Spacer.init(allocator); │ │1290 errdefer spacer.deinit(allocator); │ │1291 try row.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │ 843 - } │ │ 844 - │ │ 845 - // a logged-out session can't flip the status, so it gets no │ │ 846 - // open/close button (and no form action on the row) │ │ 847 - if (self.session.data.is_local or self.session.data.user_id != null) { │ │ 848 - const action: []const u8 = switch (entry.issue.event.status) { │ │ 849 - .open => "close", │ │ 850 - .closed => "open", │ │ 851 - }; │ │ 852 - row.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │ 853 - try std.fmt.allocPrint(pa, "form:/issues/{s}/{s}", .{ entry.id, action }) │ │ 854 - else │ │ 855 - try std.fmt.allocPrint(pa, "form:/repo/{s}/issues/{s}/{s}", .{ self.data.identity, entry.id, action }) }; │ │ 856 - │ │ 857 - var button = try wgt.TextBox(ui.Widget).init(allocator, action, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │ 858 - errdefer button.deinit(allocator); │ │ 859 - button.getFocus().focusable = true; │ │ 860 - // the renderer distinguishes plain clickables from buttons that │ │ 861 - // should POST to a server route by this kind. │ │ 862 - button.getFocus().kind = .{ .custom = "submit" }; │ │ 863 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = action.len + 2, .height = null } }); │ │1292 } │ │1293 │ │ 866 - // the edit button links to the issue's edit page. │ │ 867 - { │ │ 868 - var button = try wgt.TextBox(ui.Widget).init(allocator, "edit", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1294 + // a conflicted issue's only action is resolving it. the button │ │1295 + // stays visible logged out; the resolve page shows the │ │1296 + // unauthorized view then. │ │1297 + if (entry.conflicted) { │ │1298 + var button = try wgt.TextBox(ui.Widget).init(allocator, "resolve", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1299 errdefer button.deinit(allocator); │ │1300 button.getFocus().focusable = true; │ │ 871 - const route = ui.RoutablePage.repoIssuesEditRoute(self.data.identity, entry.id) orelse return error.RouteTooLong; │ │1301 + const route = ui.RoutablePage.repoIssuesResolveRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1302 button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │ 873 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "edit".len + 2, .height = null } }); │ │1303 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "resolve".len + 2, .height = null } }); │ │1304 + } else { │ │1305 + // a logged-out session can't flip the status, so it gets no │ │1306 + // open/close button (and no form action on the row) │ │1307 + if (self.session.data.is_local or self.session.data.user_id != null) { │ │1308 + const action: []const u8 = switch (entry.issue.event.status) { │ │1309 + .open => "close", │ │1310 + .closed => "open", │ │1311 + }; │ │1312 + row.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │1313 + try std.fmt.allocPrint(pa, "form:/issues/{s}/{s}", .{ entry.id, action }) │ │1314 + else │ │1315 + try std.fmt.allocPrint(pa, "form:/repo/{s}/issues/{s}/{s}", .{ self.data.identity, entry.id, action }) }; │ │1316 + │ │1317 + var button = try wgt.TextBox(ui.Widget).init(allocator, action, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1318 + errdefer button.deinit(allocator); │ │1319 + button.getFocus().focusable = true; │ │1320 + // the renderer distinguishes plain clickables from buttons that │ │1321 + // should POST to a server route by this kind. │ │1322 + button.getFocus().kind = .{ .custom = "submit" }; │ │1323 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = action.len + 2, .height = null } });│ │1324 + } │ │1325 + │ │1326 + // the edit button links to the issue's edit page. │ │1327 + { │ │1328 + var button = try wgt.TextBox(ui.Widget).init(allocator, "edit", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1329 + errdefer button.deinit(allocator); │ │1330 + button.getFocus().focusable = true; │ │1331 + const route = ui.RoutablePage.repoIssuesEditRoute(self.data.identity, entry.id) orelse return error.RouteTooLong; │ │1332 + button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1333 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "edit".len + 2, .height = null } });│ │1334 + } │ │1335 } │ │1336 │ │ 876 - // the open/close button when present, the edit button otherwise │ │1337 + // the resolve button on a conflicted issue, else the open/close │ │1338 + // button when present, the edit button otherwise │ │1339 row.getFocus().child_id = row.children.keys()[button_in_row_index]; │ │1340 } │ │1341 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1371 var tag_iter = evt.Issue.tagIterator(entry.issue.event.tags); │ │1372 while (tag_iter.next()) |tag| { │ │1373 if (tag.len == 0) continue; │ │ 912 - try items.append(allocator, .{ .text = tag, .link = try tagLink(self.session.page_arena, self.data.identity, splitStatus(index), tag) }); │ │1374 + try items.append(allocator, .{ .text = tag, .link = try tagLink(self.session.page_arena, self.data.identity, entry.issue.event.status, tag) });│ │1375 } │ │1376 if (items.items.len > 0) { │ │1377 var tf = try ui.TagFlow.init(allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │1442 return; │ │1443 } │ │1444 if (self.formViewActive()) { │ │ 983 - try self.formInput(allocator, key, root_focus); │ │1445 + if (self.data.view == .resolve) { │ │1446 + try self.resolveInput(allocator, key, root_focus);│ │1447 + } else { │ │1448 + try self.formInput(allocator, key, root_focus); │ │1449 + } │ │1450 return; │ │1451 } │ │1452 const i = self.selectedSplitIndex() orelse return; │ ╰────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1507 } │ │1508 │ │1509 // the tool row: enter or a click on the open/close button flips the │ │1044 - // issue's status; the edit button is an a: link the host follows. │ │1045 - // arrows cross between the buttons and to the neighboring widgets. │ │1510 + // issue's status; the edit and resolve buttons are a: links the host │ │1511 + // follows. arrows cross between the buttons and to the neighboring │ │1512 + // widgets. │ │1513 fn toolRowInput(self: *View, allocator: std.mem.Allocator, index: usize, key: Key, root_focus: *Focus) !void { │ │1514 const row = self.toolRow(index); │ │1048 - // the edit button is always the row's last child; the open/close │ │1049 - // button before it is only there when the session may author events │ │1050 - const edit_index = row.children.count() - 1; │ │1051 - const on_edit = if (row.getFocus().child_id) |cid| row.children.getIndex(cid) == edit_index else false; │ │1515 + const cur = if (row.getFocus().child_id) |cid| row.children.getIndex(cid) orelse return else return; │ │1516 + const on_status = cur == button_in_row_index and self.statusButton(index) != null; │ │1517 switch (key) { │ │1053 - .arrow_left => if (on_edit and self.statusButton(index) != null) root_focus.setFocus(row.children.keys()[button_in_row_index]) else try self.focusList(index, root_focus),│ │1054 - .arrow_right => if (!on_edit) root_focus.setFocus(row.children.keys()[edit_index]), │ │1518 + .arrow_left => if (cur > button_in_row_index) root_focus.setFocus(row.children.keys()[cur - 1]) else try self.focusList(index, root_focus), │ │1519 + .arrow_right => if (cur + 1 < row.children.count()) root_focus.setFocus(row.children.keys()[cur + 1]), │ │1520 .arrow_up => self.focusHeader(root_focus), │ │1521 .arrow_down => self.focusTitle(index, root_focus), │ │1057 - .enter => if (!on_edit) try self.toggleIssueStatus(allocator, index), │ │1522 + .enter => if (on_status) try self.toggleIssueStatus(allocator, index), │ │1523 .mouse => |mouse| switch (mouse.action) { │ │1524 .scroll => |dir| { │ │1525 const sc = self.detailScroll(index); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯