╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Threads.zig│ ╰───────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │816 try box.children.put(allocator, description.getFocus().id, .{ .widget = .{ .text_input = description }, .rect = null, .min_size = null });│ │817 } │ │818 │ │456 - try addSubmitButton(allocator, &box); │ │819 + try addSubmitButtonLabeled(allocator, &box, if (kind == .patch and record == null) "submit private draft" else "submit"); │ │820 │ │821 box.getFocus().child_id = box.children.keys()[title_field_index]; │ │822 return box; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │907 │ │908 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │909 errdefer box.deinit(allocator); │ │547 - const route = resolveRoute(data.identity, data.selected_id, "") orelse return error.RouteTooLong; │ │910 + const route = resolveRoute(data.identity, data.selected_id, data.theirs_picks) orelse return error.RouteTooLong; │ │911 box.getFocus().kind = .{ .custom = try std.fmt.allocPrint(aa, "form:{s}", .{try route.toUrl(session.page_arena)}) };│ │912 │ │913 if (conflict.title) |*fc| { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │917 if (conflict.tags) |*fc| { │ │918 try addLabel(allocator, &box, "tags conflict:"); │ │919 try addFieldConflict(allocator, &box, session, data, "tags", fc); │ │920 + } │ │921 + if (comptime kind == .patch) { │ │922 + if (conflict.status) |*fc| { │ │923 + try addLabel(allocator, &box, "status conflict:"); │ │924 + try addAtomicConflict(allocator, &box, session, data, "status", fc); │ │925 + } │ │926 + if (conflict.revision) |*fc| { │ │927 + try addLabel(allocator, &box, "revision conflict:"); │ │928 + try addAtomicConflict(allocator, &box, session, data, "revision", fc);│ │929 + } │ │930 } │ │931 │ │932 if (conflict.description) |*desc| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 992 resolution_input.getFocus().focusable = true; │ │ 993 try resolution_input.setContent(allocator, if (data.theirsPicked(name)) fc.theirs.text else fc.ours.text); │ │ 994 try box.children.put(allocator, resolution_input.getFocus().id, .{ .widget = .{ .text_input = resolution_input }, .rect = null, .min_size = null }); │ │ 995 + } │ │ 996 + │ │ 997 + // atomic fields can only pick one complete side │ │ 998 + fn addAtomicConflict(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, data: *const Self, comptime name: []const u8, fc: *const FieldConflict) !void {│ │ 999 + const aa = session.page_arena.allocator(); │ │1000 + try addVersionRow(allocator, box, try sideLabel(aa, fc.ours.author, true), fc.ours.text, try useThisLink(session, data, name, false)); │ │1001 + try addVersionRow(allocator, box, try sideLabel(aa, fc.theirs.author, false), fc.theirs.text, try useThisLink(session, data, name, true)); │ │1002 + var selected = try wgt.TextBox(ui.Widget).init(allocator, if (data.theirsPicked(name)) fc.theirs.text else fc.ours.text, .{ │ │1003 + .border_style = .single, │ │1004 + .rounded_corners = true, │ │1005 + .wrap_kind = .word, │ │1006 + .label = " resolution ", │ │1007 + }); │ │1008 + errdefer selected.deinit(allocator); │ │1009 + selected.getFocus().focusable = true; │ │1010 + try box.children.put(allocator, selected.getFocus().id, .{ .widget = .{ .text_box = selected }, .rect = null, .min_size = null }); │ │1011 } │ │1012 │ │1013 // a blank row setting a conflict group apart from its neighbors │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1121 self.box.deinit(allocator); │ │1122 } │ │1123 │ │ 735 - fn header(self: *This) *Header { │ │1124 + fn header(self: *This) *HeaderType { │ │1125 return switch (kind) { │ │1126 .issue => &self.box.children.values()[header_index].widget.repo_issues_header, │ │1127 + .patch => &self.box.children.values()[header_index].widget.repo_patches_header, │ │1128 .discuss => &self.box.children.values()[header_index].widget.repo_discussions_header,│ │1129 else => @compileError("unsupported thread event kind"), │ │1130 }; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │1203 │ │1204 fn window(self: *This, index: usize) *const Window { │ │1205 if (has_conflicts and index == conflict_view_index) return &self.data.conflicts;│ │1206 + if (has_drafts and index == drafts_view_index) return &self.data.drafts; │ │1207 return self.data.window(splitStatus(index)); │ │1208 } │ │1209 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1217 // new-issue form shows). │ │1218 fn selectedSplitIndex(self: *This) ?usize { │ │1219 const idx = self.stackSelectedIndex() orelse return null; │ │ 829 - return if (idx < status_count or idx == conflict_view_index) idx else null; │ │1220 + return if (idx < status_count or (has_drafts and idx == drafts_view_index) or idx == conflict_view_index) idx else null;│ │1221 } │ │1222 │ │1223 fn detailActive(self: *This, index: usize) bool { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1280 }) │ │1281 else if (has_conflicts and index == conflict_view_index) │ │1282 conflictsRoute(self.data.identity, if (self.data.view == .conflicts) self.data.selected_id else "")│ │1283 + else if (has_drafts and index == drafts_view_index) │ │1284 + if (index == viewIndex(self.data.view) and self.data.selected_id.len != 0) │ │1285 + commentsRoute(self.data.identity, self.data.selected_id, 0) │ │1286 + else │ │1287 + draftsRoute(self.data.identity) │ │1288 else if (index == viewIndex(self.data.view) and self.data.selected_id.len != 0) │ │1289 (if (self.data.description_page) │ │1290 descriptionRoute(self.data.identity, self.data.selected_id) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │1419 inner.children.clearAndFree(allocator); │ │1420 inner.getFocus().child_id = null; │ │1421 self.author_id[index] = null; │ │1026 - self.status_button_id[index] = null; │ │1422 + self.primary_button_id[index] = null; │ │1423 │ │1028 - // the tool row: the open/close and edit buttons; the description page │ │1029 - // shows none. │ │1424 + // the tool row; the description page shows none. │ │1425 { │ │1426 const row = self.toolRow(index); │ │1427 for (row.children.values()) |*child| child.widget.deinit(allocator);│ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1437 var spacer = try ui.Spacer.init(allocator); │ │1438 errdefer spacer.deinit(allocator); │ │1439 try row.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │1045 - } │ │1046 - │ │1047 - // a terminal has no file picker, so attaching is web only. the │ │1048 - // renderer covers this button with a file input, so clicking it │ │1049 - // opens the browser's own picker and posts what it chose. │ │1050 - if (!self.session.is_terminal and !entryConflicted(entry) and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1051 - const label = "add attachment"; │ │1052 - var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1053 - errdefer button.deinit(allocator); │ │1054 - button.getFocus().focusable = true; │ │1055 - button.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │1056 - try std.fmt.allocPrint(pa, "{s}/{s}:{s}/attach", .{ ui.file_input_prefix, @tagName(kind), entry.id }) │ │1057 - else │ │1058 - try std.fmt.allocPrint(pa, "{s}/repo/{s}/{s}:{s}/attach", .{ ui.file_input_prefix, self.data.identity, @tagName(kind), entry.id }) }; │ │1059 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } }); │ │1440 } │ │1061 - │ │1062 - // a conflicted issue's only action is resolving it. the button │ │1063 - // stays visible logged out; the resolve page shows the │ │1064 - // unauthorized view then. │ │1065 - if (has_conflicts and entryConflicted(entry)) { │ │1066 - const label = "resolve conflict"; │ │1067 - var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1068 - errdefer button.deinit(allocator); │ │1069 - button.getFocus().focusable = true; │ │1070 - const route = resolveRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1071 - button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1072 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } }); │ │1073 - } else { │ │1074 - // a logged-out session can't flip the status, so it gets no │ │1075 - // open/close button │ │1076 - if (has_status and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1077 - const action: []const u8 = switch (entryStatus(entry)) { │ │1078 - .open => "close", │ │1079 - .closed => "open", │ │1080 - }; │ │1081 - const route = commentsRoute(self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │1082 - row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/{s}", .{ try route.toUrl(self.session.page_arena), action }) }; │ │1441 │ │1084 - var button = try wgt.TextBox(ui.Widget).init(allocator, action, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1442 + if (has_drafts and entryDraft(entry)) { │ │1443 + if (entry.revision_ready) { │ │1444 + const route = listRoute(self.data.identity, @enumFromInt(0), "", entry.id) orelse return error.RouteTooLong; │ │1445 + row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/post", .{try route.toUrl(self.session.page_arena)}) }; │ │1446 + var button = try wgt.TextBox(ui.Widget).init(allocator, "post", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1447 errdefer button.deinit(allocator); │ │1448 button.getFocus().focusable = true; │ │1087 - // the renderer distinguishes plain clickables from buttons │ │1088 - // that should POST to a server route by this kind. │ │1449 button.getFocus().kind = .{ .custom = "submit" }; │ │1090 - self.status_button_id[index] = button.getFocus().id; │ │1091 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = action.len + 2, .height = null } }); │ │1450 + self.primary_button_id[index] = button.getFocus().id; │ │1451 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "post".len + 2, .height = null } }); │ │1452 + } │ │1453 + if (row.children.count() > first_in_row_index) │ │1454 + row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ │1455 + } else { │ │1456 + // a terminal has no file picker, so attaching is web only. the │ │1457 + // renderer covers this button with a file input, so clicking it │ │1458 + // opens the browser's own picker and posts what it chose. │ │1459 + if (!self.session.is_terminal and !entryConflicted(entry) and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1460 + const label = "add attachment"; │ │1461 + var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1462 + errdefer button.deinit(allocator); │ │1463 + button.getFocus().focusable = true; │ │1464 + button.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │1465 + try std.fmt.allocPrint(pa, "{s}/{s}:{s}/attach", .{ ui.file_input_prefix, @tagName(kind), entry.id }) │ │1466 + else │ │1467 + try std.fmt.allocPrint(pa, "{s}/repo/{s}/{s}:{s}/attach", .{ ui.file_input_prefix, self.data.identity, @tagName(kind), entry.id }) }; │ │1468 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } }); │ │1469 } │ │1470 │ │1094 - // the edit button links to the issue's edit page. │ │1095 - { │ │1096 - var button = try wgt.TextBox(ui.Widget).init(allocator, "edit", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1471 + // a conflicted issue's only action is resolving it. the button │ │1472 + // stays visible logged out; the resolve page shows the │ │1473 + // unauthorized view then. │ │1474 + if (has_conflicts and entryConflicted(entry)) { │ │1475 + const label = "resolve conflict"; │ │1476 + var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1477 errdefer button.deinit(allocator); │ │1478 button.getFocus().focusable = true; │ │1099 - const route = editRoute(self.data.identity, entry.id) orelse return error.RouteTooLong; │ │1479 + const route = resolveRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1480 button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1101 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "edit".len + 2, .height = null } }); │ │1481 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } }); │ │1482 + } else { │ │1483 + // a logged-out session can't flip the status, so it gets no │ │1484 + // open/close button │ │1485 + if (has_status and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1486 + if (statusAction(entryStatus(entry))) |action| { │ │1487 + const route = commentsRoute(self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │1488 + row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/{s}", .{ try route.toUrl(self.session.page_arena), action }) }; │ │1489 + │ │1490 + var button = try wgt.TextBox(ui.Widget).init(allocator, action, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1491 + errdefer button.deinit(allocator); │ │1492 + button.getFocus().focusable = true; │ │1493 + // the renderer distinguishes plain clickables from buttons │ │1494 + // that should POST to a server route by this kind. │ │1495 + button.getFocus().kind = .{ .custom = "submit" }; │ │1496 + self.primary_button_id[index] = button.getFocus().id; │ │1497 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = action.len + 2, .height = null } });│ │1498 + } │ │1499 + } │ │1500 + │ │1501 + // the edit button links to the issue's edit page. │ │1502 + { │ │1503 + var button = try wgt.TextBox(ui.Widget).init(allocator, "edit", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1504 + errdefer button.deinit(allocator); │ │1505 + button.getFocus().focusable = true; │ │1506 + const route = editRoute(self.data.identity, entry.id) orelse return error.RouteTooLong; │ │1507 + button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1508 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "edit".len + 2, .height = null } }); │ │1509 + } │ │1510 } │ │1103 - } │ │1511 │ │1105 - if (self.session.data.is_local or self.session.data.user_id != null) { │ │1106 - var remove = try wgt.TextBox(ui.Widget).init(allocator, "✕", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1107 - errdefer remove.deinit(allocator); │ │1108 - remove.getFocus().focusable = true; │ │1109 - const route = removeRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1110 - remove.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1111 - try row.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } }); │ │1112 - } │ │1512 + if (self.session.data.is_local or self.session.data.user_id != null) { │ │1513 + var remove = try wgt.TextBox(ui.Widget).init(allocator, "✕", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1514 + errdefer remove.deinit(allocator); │ │1515 + remove.getFocus().focusable = true; │ │1516 + const route = removeRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1517 + remove.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1518 + try row.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } }); │ │1519 + } │ │1520 │ │1114 - // the leftmost button: the attachment button when the session has │ │1115 - // one, else the resolve button on a conflicted issue, the │ │1116 - // open/close button when present, the edit button otherwise │ │1117 - row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ │1521 + // the leftmost button: the attachment button when the session has │ │1522 + // one, else the resolve button on a conflicted issue, the │ │1523 + // open/close button when present, the edit button otherwise │ │1524 + row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ │1525 + } │ │1526 } │ │1527 │ │1528 if (comment_page) |page| { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯